Tests: Add deep recursive test of copy method (#10756)

* Add recursive test of copy

* no need to print those unless debugging

* WIP

* comment debug print

* more looping on copyable objects.

* Allow for smarter recursion sub Cards

* check that the special fields were encountered

* better comments for fixing the copy errors
This commit is contained in:
Susucre 2023-08-16 04:39:40 +02:00 committed by GitHub
parent 205f61703d
commit 461fe71314
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -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<Card> 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<Field> 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 + "]");
}
}
}