diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index 5a0f021a9a2..409d4823e95 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -2,10 +2,15 @@ package mage.verify; import com.google.common.base.CharMatcher; import mage.MageObject; +import mage.Mana; import mage.ObjectColor; import mage.abilities.Ability; +import mage.abilities.AbilityImpl; import mage.abilities.Mode; import mage.abilities.common.*; +import mage.abilities.condition.Condition; +import mage.abilities.costs.Cost; +import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; import mage.abilities.effects.common.FightTargetsEffect; import mage.abilities.effects.common.counter.ProliferateEffect; @@ -16,9 +21,11 @@ import mage.cards.decks.CardNameUtil; import mage.cards.decks.DeckCardLists; import mage.cards.decks.importer.DeckImporter; import mage.cards.repository.*; +import mage.choices.Choice; import mage.constants.CardType; import mage.constants.Rarity; import mage.constants.SubType; +import mage.filter.Filter; import mage.game.command.Dungeon; import mage.game.command.Plane; import mage.game.draft.DraftCube; @@ -28,6 +35,7 @@ import mage.game.permanent.token.TokenImpl; import mage.game.permanent.token.custom.CreatureToken; import mage.server.util.SystemUtil; import mage.sets.TherosBeyondDeath; +import mage.target.targetpointer.TargetPointer; import mage.util.CardUtil; import mage.verify.mtgjson.MtgJsonCard; import mage.verify.mtgjson.MtgJsonService; @@ -41,10 +49,7 @@ import org.mage.plugins.card.dl.sources.ScryfallImageSupportCards; import org.reflections.Reflections; import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; +import java.lang.reflect.*; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; @@ -1643,6 +1648,9 @@ public class VerifyCardDataTest { } } + // "copy" fails means that the copy constructor are not correct inside a card. + // To fix those, try to find the class that did trigger the copy failure, and check + // that copy() exists, a copy constructor exists, and the copy constructor is right. private void checkCardCanBeCopied(Card card1) { Card card2; try { @@ -1652,18 +1660,163 @@ public class VerifyCardDataTest { return; } - // Checks that ability and effect are of the same class when copied. - for (int i = 0; i < card1.getAbilities().size(); i++) { - Ability ability1 = card1.getAbilities().get(i); - Ability ability2 = card2.getAbilities().get(i); - if (!ability1.getClass().equals(ability2.getClass())) { - fail(card1, "copy", " miss copy in ability " + ability1.getClass().getName()); + compareClassRecursive(card1, card2, card1, "[Card", 10, new HashSet<>(), true); + } + + /** + * @param obj1 first object to compare. Initially the original card. + * @param obj2 second object to compare. Initially the copy of the original card. + * @param originalCard the original card, used to print a nice message on fail. + * @param msg the recursively built message to explain what is different. + * @param maxDepth the maximum allowed recursion. A safety mesure for the test to end. + * @param alreadyChecked Map of all Cards obj1 already compared. + * @param useRecursive When false, do not recursively compare Cards. + */ + private void compareClassRecursive(Object obj1, Object obj2, Card originalCard, String msg, int maxDepth, + Set alreadyChecked, boolean useRecursive) { + if (obj1 == null && obj2 == null) { + return; + } else if (obj1 == null || obj2 == null) { + fail(originalCard, "copy", "not same class for " + msg + "]"); + } else if (obj1.getClass() != obj2.getClass()) { + fail(originalCard, "copy", "not same class for " + msg + "<" + obj1.getClass() + ">" + "]"); + } else if (obj1 == obj2) { // for instances mostly + return; + } else { + // Only recurse so much. + if (maxDepth == 0) { + return; } - for (int j = 0; j < ability1.getEffects().size(); j++) { - Effect effect1 = ability1.getEffects().get(j); - Effect effect2 = ability2.getEffects().get(j); - if (!effect1.getClass().equals(effect2.getClass())) { - fail(card1, "copy", "miss copy in effect " + effect1.getClass().getName()); + // Only recurse on those objects + if (obj1 instanceof MageObject || obj1 instanceof Filter || obj1 instanceof Condition || obj1 instanceof Effect + || obj1 instanceof Ability || obj1 instanceof Mana || obj1 instanceof Cost || obj1 instanceof DynamicValue + || obj1 instanceof Choice || obj1 instanceof TargetPointer) { + + boolean doRecurse = useRecursive; + if (obj1 instanceof Card) { + if (alreadyChecked.contains(obj1)) { + if (!doRecurse) { + return; // we already checked that obj1 and do not want to recurse. stop there. + } else { + doRecurse = false; + } + } else { + alreadyChecked.add((Card) obj1); + } + } + + //System.out.println(msg); + Class class1 = obj1.getClass(); + Class class2 = obj2.getClass(); + do { + if (class1 == null && class2 == null) { + return; + } + if (class1 == null || class2 == null) { + fail(originalCard, "copy", "not same class for " + msg + "<" + obj1.getClass() + ">" + "]"); + return; + } + List ability2Fields = Arrays.stream(class2.getDeclaredFields()).collect(Collectors.toList()); + + // Special fields for CardImpl.class + boolean hasSpellAbilityField = false; + boolean hasMeldField = false; + boolean hasSecondSideCardField = false; + // Special fields for AbilityImpl.class + boolean hasWatchersField = false; + boolean hasModesField = false; + + int fieldIndex = 0; + for (Field field1 : class1.getDeclaredFields()) { + Field field2 = ability2Fields.get(fieldIndex); + field1.setAccessible(true); + field2.setAccessible(true); + try { + Object value1 = field1.get(obj1); + Object value2 = field2.get(obj2); + + boolean doFieldRecurse = true; + if (class1 == CardImpl.class) { + if (field1.getName() == "spellAbility") { + compareClassRecursive(((CardImpl) obj1).getSpellAbility(), ((CardImpl) obj2).getSpellAbility(), originalCard, msg + "<" + obj1.getClass() + ">" + "::" + field1.getName(), maxDepth - 1, alreadyChecked, doRecurse); + doFieldRecurse = false; + hasSpellAbilityField = true; + } else if (field1.getName() == "meldsToCard") { + compareClassRecursive(((CardImpl) obj1).getMeldsToCard(), ((CardImpl) obj2).getMeldsToCard(), originalCard, msg + "::" + field1.getName(), maxDepth - 1, alreadyChecked, doRecurse); + doFieldRecurse = false; + hasMeldField = true; + } else if (field1.getName() == "secondSideCard") { + compareClassRecursive(((CardImpl) obj1).getSecondCardFace(), ((CardImpl) obj2).getSecondCardFace(), originalCard, msg + "::" + field1.getName(), maxDepth - 1, alreadyChecked, doRecurse); + doFieldRecurse = false; + hasSecondSideCardField = true; + } + } + if (class1 == AbilityImpl.class) { + if (field1.getName() == "watchers") { + // Watchers are only used on initialization, they are not copied. + doFieldRecurse = false; + hasWatchersField = true; + } + if (field1.getName() == "modes") { + //compareClassRecursive(((AbilityImpl) obj1).getModes(), ((AbilityImpl) obj2).getModes(), originalCard, msg + "<" + obj1.getClass() + ">" + "::" + field1.getName(), maxDepth - 1); + compareClassRecursive(((AbilityImpl) obj1).getEffects(), ((AbilityImpl) obj2).getEffects(), originalCard, msg + "<" + obj1.getClass() + ">" + "::" + field1.getName(), maxDepth - 1, alreadyChecked, doRecurse); + doFieldRecurse = false; + hasModesField = true; + } + } + if (doFieldRecurse) { + compareClassRecursive(value1, value2, originalCard, msg + "<" + obj1.getClass() + ">" + "::" + field1.getName(), maxDepth - 1, alreadyChecked, doRecurse); + } + } catch (IllegalArgumentException | IllegalAccessException e) { + } + fieldIndex++; + } + + // Do check that the expected special fields were encountered. + // If those field are no relevant anymore, or were renamed, please modify the matching code + // block above on how to loop into those fields. + if (class1 == CardImpl.class) { + if (!hasSpellAbilityField) { + fail(originalCard, "copy", "was expecting a spellAbility field, but found none " + msg + "]"); + } + if (!hasMeldField) { + fail(originalCard, "copy", "was expecting a meldsToCard field, but found none " + msg + "]"); + } + if (!hasSecondSideCardField) { + fail(originalCard, "copy", "was expecting a secondSideCard field, but found none " + msg + "]"); + } + } else if (class1 == AbilityImpl.class) { + if (!hasWatchersField) { + fail(originalCard, "copy", "was expecting a watchers field, but found none " + msg + "]"); + } + if (!hasModesField) { + fail(originalCard, "copy", "was expecting a modes field, but found none " + msg + "]"); + } + } + + class1 = class1.getSuperclass(); + class2 = class2.getSuperclass(); + } while (class1 != Object.class && class1 != null); + } else if (obj1 instanceof Collection) { + Collection col1 = (Collection) obj1; + Collection col2 = (Collection) obj2; + Iterator it1 = col1.iterator(); + Iterator it2 = col2.iterator(); + int i = 0; + while (it1.hasNext() && it2.hasNext()) { + compareClassRecursive(it1.next(), it2.next(), originalCard, msg + "<" + obj1.getClass() + ">" + "[" + i++ + "]", maxDepth - 1, alreadyChecked, useRecursive); + } + if (it1.hasNext() || it2.hasNext()) { + fail(originalCard, "copy", "not same size for " + msg + "]"); + } + } else if (obj1 instanceof Map) { + Map map1 = (Map) obj1; + Map map2 = (Map) obj2; + map1.forEach((i, el1) -> { + compareClassRecursive(el1, ((Map) obj2).get(i), originalCard, msg + "<" + obj1.getClass() + ">" + ".(" + i + ")", maxDepth - 1, alreadyChecked, useRecursive); + }); + if (map1.size() != map2.size()) { + fail(originalCard, "copy", "not same size for " + msg + "]"); } } }