Tests: added many verify checks for missing cards, names, numbers, download settings:

* check wrong card numbers in sets;
* check missing cards from set;
* check wrong full art settings;
* check missing and unknown sets in scryfall download settings;
* check missing and unknown direct download links in scryfall download settings;
* improved ability text check results;
* removed unused tests for word checks;
This commit is contained in:
Oleg Agafonov 2020-08-22 15:16:30 +04:00
parent b083dd48e6
commit 7aac355f4a
7 changed files with 533 additions and 128 deletions

View file

@ -1,25 +1,12 @@
package mage.verify;
import com.google.common.base.CharMatcher;
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.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.effects.keyword.ScryEffect;
import mage.abilities.keyword.MenaceAbility;
import mage.abilities.keyword.MultikickerAbility;
import mage.cards.*;
import mage.cards.basiclands.BasicLand;
import mage.cards.decks.DeckCardLists;
import mage.cards.decks.importer.DeckImporter;
import mage.cards.repository.CardInfo;
@ -47,6 +34,16 @@ import org.mage.plugins.card.images.CardDownloadData;
import org.mage.plugins.card.images.DownloadPicturesService;
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.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author JayDi85
*/
@ -57,9 +54,6 @@ public class VerifyCardDataTest {
private static final String FULL_ABILITIES_CHECK_SET_CODE = "THB"; // check all abilities and output cards with wrong abilities texts;
private static final boolean AUTO_FIX_SAMPLE_DECKS = false; // debug only: fix sample decks if it contains errors like wrong card numbers
// right now this is very noisy, and not useful enough to make any assertions on
private static final boolean CHECK_SOURCE_TOKENS = false;
private static final HashMap<String, Set<String>> skipCheckLists = new HashMap<>();
private static final Set<String> subtypesToIgnore = new HashSet<>();
private static final String SKIP_LIST_PT = "PT";
@ -73,8 +67,8 @@ public class VerifyCardDataTest {
private static final String SKIP_LIST_DOUBLE_RARE = "DOUBLE_RARE";
private static final String SKIP_LIST_UNSUPPORTED_SETS = "UNSUPPORTED_SETS";
private static final String SKIP_LIST_SCRYFALL_DOWNLOAD_SETS = "SCRYFALL_DOWNLOAD_SETS";
private static final String SKIP_LIST_WRONG_CARD_NUMBERS = "WRONG_CARD_NUMBERS";
private static final String SKIP_LIST_SAMPLE_DECKS = "SAMPLE_DECKS";
private static final Pattern SHORT_JAVA_STRING = Pattern.compile("(?<=\")[A-Z][a-z]+(?=\")");
static {
// skip lists for checks (example: unstable cards with same name may have different stats)
@ -190,9 +184,20 @@ public class VerifyCardDataTest {
skipListAddName(SKIP_LIST_UNSUPPORTED_SETS, "AMH1"); // Modern Horizons Art Series
skipListAddName(SKIP_LIST_UNSUPPORTED_SETS, "PTG"); // Ponies: The Galloping
// wrond card numbers skip list
skipListCreate(SKIP_LIST_WRONG_CARD_NUMBERS);
skipListAddName(SKIP_LIST_WRONG_CARD_NUMBERS, "SWS"); // Star Wars
skipListAddName(SKIP_LIST_WRONG_CARD_NUMBERS, "POR"); // Portal, TODO: remove after bug fixed https://github.com/mtgjson/mtgjson/issues/660
skipListAddName(SKIP_LIST_WRONG_CARD_NUMBERS, "UND"); // un-sets don't have full implementation of card variations
skipListAddName(SKIP_LIST_WRONG_CARD_NUMBERS, "UST"); // un-sets don't have full implementation of card variations
skipListAddName(SKIP_LIST_WRONG_CARD_NUMBERS, "SOI", "Tamiyo's Journal"); // not all variations implemented
// scryfall download sets (missing from scryfall website)
skipListCreate(SKIP_LIST_SCRYFALL_DOWNLOAD_SETS);
skipListAddName(SKIP_LIST_SCRYFALL_DOWNLOAD_SETS, "SWS"); // Star Wars
//skipListAddName(SKIP_LIST_SCRYFALL_DOWNLOAD_SETS, "8EB"); // Eighth Edition Box - inner xmage set, split from 8ED
//skipListAddName(SKIP_LIST_SCRYFALL_DOWNLOAD_SETS, "9EB"); // Ninth Edition Box - inner xmage set, split from 9ED
// sample decks checking - some decks can contains unimplemented cards, so ignore it
// file name must be related to sample-decks folder
@ -246,12 +251,11 @@ public class VerifyCardDataTest {
int cardIndex = 0;
for (Card card : CardScanner.getAllCards()) {
cardIndex++;
Set<String> tokens = findSourceTokens(card.getClass());
if (card.isSplitCard()) {
check(((SplitCard) card).getLeftHalfCard(), null, cardIndex);
check(((SplitCard) card).getRightHalfCard(), null, cardIndex);
check(((SplitCard) card).getLeftHalfCard(), cardIndex);
check(((SplitCard) card).getRightHalfCard(), cardIndex);
} else {
check(card, tokens, cardIndex);
check(card, cardIndex);
}
}
@ -544,13 +548,87 @@ public class VerifyCardDataTest {
}
@Test
public void test_checkMissingScryfallSettings() {
@Ignore // TODO: enable after all missing cards and settings fixes
public void test_checkWrongCardsDataInSets() {
Collection<String> errorsList = new ArrayList<>();
Collection<String> warningsList = new ArrayList<>();
Collection<ExpansionSet> xmageSets = Sets.getInstance().values();
Set<String> foundedJsonCards = new HashSet<>();
// CHECK: wrong card numbers
for (ExpansionSet set : xmageSets) {
if (skipListHaveName(SKIP_LIST_WRONG_CARD_NUMBERS, set.getCode())) {
continue;
}
for (ExpansionSet.SetCardInfo card : set.getSetCardInfo()) {
MtgJsonCard jsonCard = MtgJsonService.cardFromSet(set.getCode(), card.getName(), card.getCardNumber());
if (jsonCard == null) {
// see convertMtgJsonToXmageCardNumber for card number convert notation
errorsList.add("Error: unknown card number, use standard number notations: "
+ set.getCode() + " - " + set.getName() + " - " + card.getName() + " - " + card.getCardNumber());
continue;
}
// index for missing cards
String code = MtgJsonService.xMageToMtgJsonCodes.getOrDefault(set.getCode(), set.getCode()) + " - " + jsonCard.name + " - " + jsonCard.number;
foundedJsonCards.add(code);
// CHECK: must use full art setting
if (jsonCard.isFullArt && !card.isFullArt()) {
errorsList.add("Error: card must use full art setting: "
+ set.getCode() + " - " + set.getName() + " - " + card.getName() + " - " + card.getCardNumber());
}
// CHECK: must not use full art setting
if (!jsonCard.isFullArt && card.isFullArt()) {
errorsList.add("Error: card must NOT use full art setting: "
+ set.getCode() + " - " + set.getName() + " - " + card.getName() + " - " + card.getCardNumber());
}
}
}
// CHECK: missing cards from set
for (MtgJsonSet jsonSet : MtgJsonService.sets().values()) {
if (skipListHaveName(SKIP_LIST_UNSUPPORTED_SETS, jsonSet.code)
|| skipListHaveName(SKIP_LIST_WRONG_CARD_NUMBERS, jsonSet.code)) {
continue;
}
ExpansionSet xmageSet = Sets.findSet(jsonSet.code);
if (xmageSet == null) {
warningsList.add("Warning: found un-implemented set from mtgjson database: " + jsonSet.code + " - " + jsonSet.name + " - " + jsonSet.releaseDate);
continue;
}
for (MtgJsonCard jsonCard : jsonSet.cards) {
String code = jsonSet.code + " - " + jsonCard.name + " - " + jsonCard.number;
if (!foundedJsonCards.contains(code)) {
if (CardRepository.instance.findCard(jsonCard.name) == null) {
// ignore non-implemented cards
continue;
}
errorsList.add("Error: missing card from xmage's set: " + jsonSet.code + " - " + jsonCard.name + " - " + jsonCard.number);
}
}
}
printMessages(warningsList);
printMessages(errorsList);
if (errorsList.size() > 0) {
Assert.fail("Found wrong cards data in sets, errors: " + errorsList.size());
}
}
@Test
@Ignore // TODO: enable after all missing cards and settings fixes
public void test_checkMissingScryfallSettingsAndCardNumbers() {
Collection<String> errorsList = new ArrayList<>();
Collection<ExpansionSet> xmageSets = Sets.getInstance().values();
Set<String> scryfallSets = ScryfallImageSupportCards.getSupportedSets();
// missing
// CHECK: missing sets in supported list
for (ExpansionSet set : xmageSets) {
if (skipListHaveName(SKIP_LIST_SCRYFALL_DOWNLOAD_SETS, set.getCode())) {
continue;
@ -561,13 +639,64 @@ public class VerifyCardDataTest {
}
}
// unknown
// CHECK: unknown sets in supported list
for (String scryfallCode : scryfallSets) {
if (xmageSets.stream().noneMatch(e -> e.getCode().equals(scryfallCode))) {
errorsList.add("Error: scryfall download unknown setting: " + scryfallCode);
}
}
// card numbers
// all cards with non-ascii numbers must be downloaded by direct links (api)
Set<String> foundedDirectDownloadKeys = new HashSet<>();
for (ExpansionSet set : xmageSets) {
if (skipListHaveName(SKIP_LIST_SCRYFALL_DOWNLOAD_SETS, set.getCode())
|| skipListHaveName(SKIP_LIST_WRONG_CARD_NUMBERS, set.getCode())) {
continue;
}
for (ExpansionSet.SetCardInfo card : set.getSetCardInfo()) {
MtgJsonCard jsonCard = MtgJsonService.cardFromSet(set.getCode(), card.getName(), card.getCardNumber());
if (jsonCard == null) {
// see convertMtgJsonToXmageCardNumber for card number convert notation
errorsList.add("Error: scryfall download can't find card from mtgjson " + set.getCode() + " - " + set.getName() + " - " + card.getName() + " - " + card.getCardNumber());
continue;
}
// CHECK: non-ascii numbers and direct download list
if (!CharMatcher.ascii().matchesAllOf(jsonCard.number)) {
// non-ascii numbers
// xmage card numbers can't have non-ascii numbers (it checked by test_checkMissingCardData)
String key = ScryfallImageSupportCards.findDirectDownloadKey(set.getCode(), card.getName(), card.getCardNumber());
if (key != null) {
foundedDirectDownloadKeys.add(key);
} else {
errorsList.add("Error: scryfall download can't find non-ascii card link in direct download list " + set.getCode() + " - " + set.getName() + " - " + card.getName() + " - " + jsonCard.number);
}
}
}
}
// CHECK: unknown direct download links
for (Map.Entry<String, String> direct : ScryfallImageSupportCards.getDirectDownloadLinks().entrySet()) {
// skip custom sets
String setCode = ScryfallImageSupportCards.extractSetCodeFromDirectKey(direct.getKey());
String cardName = ScryfallImageSupportCards.extractCardNameFromDirectKey(direct.getKey());
if (skipListHaveName(SKIP_LIST_SCRYFALL_DOWNLOAD_SETS, setCode)
|| skipListHaveName(SKIP_LIST_WRONG_CARD_NUMBERS, setCode)) {
continue;
}
// skip non implemented cards list
if (CardRepository.instance.findCard(cardName) == null) {
continue;
}
if (!foundedDirectDownloadKeys.contains(direct.getKey())) {
errorsList.add("Error: scryfall download found unknown direct download link " + direct.getKey() + " - " + direct.getValue());
}
}
printMessages(errorsList);
if (errorsList.size() > 0) {
Assert.fail("Found scryfall download errors: " + errorsList.size());
@ -646,12 +775,7 @@ public class VerifyCardDataTest {
}
// CHECK: wrong basic lands settings (it's for lands search, not booster construct)
Map<String, Boolean> skipLandCheck = new HashMap<>();
for (ExpansionSet set : sets) {
if (skipLandCheck.containsKey(set.getName())) {
continue;
}
Boolean needLand = set.hasBasicLands();
Boolean foundedLand = false;
Map<String, Integer> foundLandsList = new HashMap<>();
@ -983,53 +1107,13 @@ public class VerifyCardDataTest {
}
}
private Set<String> findSourceTokens(Class c) throws IOException {
if (!CHECK_SOURCE_TOKENS || BasicLand.class.isAssignableFrom(c)) {
return null;
}
String path = "../Mage.Sets/src/" + c.getName().replace(".", "/") + ".java";
try {
String source = new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8);
Matcher matcher = SHORT_JAVA_STRING.matcher(source);
Set<String> tokens = new HashSet<>();
while (matcher.find()) {
tokens.add(matcher.group());
}
return tokens;
} catch (NoSuchFileException e) {
System.out.println("failed to read " + path);
return null;
}
}
private void check(Card card, Set<String> tokens, int cardIndex) {
MtgJsonCard ref = MtgJsonService.card(card.getName());
private void check(Card card, int cardIndex) {
MtgJsonCard ref = MtgJsonService.cardFromSet(card.getExpansionSetCode(), card.getName(), card.getCardNumber());
if (ref == null) {
warn(card, "Missing card reference");
return;
}
checkAll(card, ref, cardIndex);
if (tokens != null) {
MtgJsonCard ref2 = null;
if (card.isFlipCard()) {
ref2 = MtgJsonService.card(card.getFlipCardName());
}
for (String token : tokens) {
if (!(token.equals(card.getName())
|| containsInTypesOrText(ref, token)
|| containsInTypesOrText(ref, token.toLowerCase(Locale.ENGLISH))
|| (ref2 != null && (containsInTypesOrText(ref2, token) || containsInTypesOrText(ref2, token.toLowerCase(Locale.ENGLISH)))))) {
System.out.println("unexpected token " + token + " in " + card);
}
}
}
}
private boolean containsInTypesOrText(MtgJsonCard ref, String token) {
return contains(ref.types, token)
|| contains(ref.subtypes, token)
|| contains(ref.supertypes, token)
|| ref.text.contains(token);
}
private boolean contains(Collection<String> options, String value) {
@ -1043,7 +1127,6 @@ public class VerifyCardDataTest {
checkSupertypes(card, ref);
checkTypes(card, ref);
checkColors(card, ref);
//checkNumbers(card, ref); // TODO: load data from AllPrintings.json and check it (allcards.json do not have card numbers)
checkBasicLands(card, ref);
checkMissingAbilities(card, ref);
checkWrongSymbolsInRules(card);
@ -1085,7 +1168,7 @@ public class VerifyCardDataTest {
// fix names (e.g. Urzas to Urza's)
if (expected != null && expected.contains("Urzas")) {
expected = new ArrayList<>(expected);
for (ListIterator<String> it = ((List<String>) expected).listIterator(); it.hasNext();) {
for (ListIterator<String> it = ((List<String>) expected).listIterator(); it.hasNext(); ) {
if (it.next().equals("Urzas")) {
it.set("Urza's");
}
@ -1144,9 +1227,9 @@ public class VerifyCardDataTest {
// ability/effect must have description or not
boolean mustCheck = card.getAbilities().containsClass(objectClass)
|| card.getAbilities().stream()
.map(Ability::getAllEffects)
.flatMap(Collection::stream)
.anyMatch(effect -> effect.getClass().isAssignableFrom(objectClass));
.map(Ability::getAllEffects)
.flatMap(Collection::stream)
.anyMatch(effect -> effect.getClass().isAssignableFrom(objectClass));
mustCheck = false; // TODO: enable and fix all problems with effect and ability hints
if (mustCheck) {
boolean needHint = ref.text.contains(objectHint);
@ -1333,9 +1416,11 @@ public class VerifyCardDataTest {
boolean isFine = true;
for (int i = 0; i <= cardRules.length - 1; i++) {
boolean isAbilityFounded = false;
for (String refRule : refRules) {
for (int j = 0; j <= refRules.length - 1; j++) {
String refRule = refRules[j];
if (cardRules[i].equals(refRule)) {
cardRules[i] = "+ " + cardRules[i];
refRules[j] = "+ " + refRules[j];
isAbilityFounded = true;
break;
}
@ -1348,6 +1433,14 @@ public class VerifyCardDataTest {
}
}
// mark ref rules as unknown
for (int j = 0; j <= refRules.length - 1; j++) {
String refRule = refRules[j];
if (!refRule.startsWith("+ ")) {
refRules[j] = "- " + refRules[j];
}
}
// extra message for easy checks
if (!isFine) {
System.out.println();
@ -1361,7 +1454,7 @@ public class VerifyCardDataTest {
System.out.println("ref:");
Arrays.sort(refRules);
for (String s : refRules) {
System.out.println(" " + s);
System.out.println(s);
}
System.out.println();