tests: improved testable dialogs (added assert messages in result table, added single dialog debugging, part of #13643, #13638);

This commit is contained in:
Oleg Agafonov 2025-06-15 11:02:32 +04:00
parent 7a44ee2a97
commit 4732fdf527
10 changed files with 166 additions and 53 deletions

View file

@ -17,7 +17,7 @@ public class AmountTestableResult extends BaseTestableResult {
} }
@Override @Override
public Boolean getResAssert() { public String getResAssert() {
return null; // TODO: implement return null; // TODO: implement
} }

View file

@ -15,6 +15,7 @@ import mage.target.common.TargetPermanentOrPlayer;
*/ */
abstract class BaseTestableDialog implements TestableDialog { abstract class BaseTestableDialog implements TestableDialog {
private Integer regNumber; // dialog number in runner (use it to find results and debugging)
private final String group; private final String group;
private final String name; private final String name;
private final String description; private final String description;
@ -27,6 +28,16 @@ abstract class BaseTestableDialog implements TestableDialog {
this.result = result; this.result = result;
} }
@Override
public void setRegNumber(Integer regNumber) {
this.regNumber = regNumber;
}
@Override
public Integer getRegNumber() {
return this.regNumber;
}
@Override @Override
final public String getGroup() { final public String getGroup() {
return this.group; return this.group;

View file

@ -25,7 +25,7 @@ public class BaseTestableResult implements TestableResult {
} }
@Override @Override
public Boolean getResAssert() { public String getResAssert() {
return null; // TODO: implement return null; // TODO: implement
} }

View file

@ -17,7 +17,7 @@ public class ChoiceTestableResult extends BaseTestableResult {
} }
@Override @Override
public Boolean getResAssert() { public String getResAssert() {
return null; // TODO: implement return null; // TODO: implement
} }

View file

@ -18,7 +18,7 @@ public class MultiAmountTestableResult extends BaseTestableResult {
} }
@Override @Override
public Boolean getResAssert() { public String getResAssert() {
return null; // TODO: implement return null; // TODO: implement
} }

View file

@ -23,7 +23,7 @@ public class TargetTestableResult extends BaseTestableResult {
} }
@Override @Override
public Boolean getResAssert() { public String getResAssert() {
if (!this.aiAssertEnabled) { if (!this.aiAssertEnabled) {
return null; return null;
} }
@ -35,16 +35,22 @@ public class TargetTestableResult extends BaseTestableResult {
// wrong choose // wrong choose
if (this.getResStatus() != this.aiAssertResStatus) { if (this.getResStatus() != this.aiAssertResStatus) {
return false; return String.format("Wrong status: need %s, but get %s",
this.aiAssertResStatus,
this.getResStatus()
);
} }
// wrong targets // wrong targets
if (this.target.getTargets().size() != this.aiAssertTargetsCount) { if (this.target.getTargets().size() != this.aiAssertTargetsCount) {
return false; return String.format("Wrong targets count: need %d, but get %d",
this.aiAssertTargetsCount,
this.target.getTargets().size()
);
} }
// all fine // all fine
return true; return "";
} }
@Override @Override

View file

@ -17,6 +17,10 @@ import mage.players.Player;
*/ */
public interface TestableDialog { public interface TestableDialog {
void setRegNumber(Integer regNumber);
Integer getRegNumber();
String getGroup(); String getGroup();
String getName(); String getName();

View file

@ -8,8 +8,10 @@ import mage.constants.Outcome;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import java.util.ArrayList; import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -55,7 +57,7 @@ import java.util.stream.Collectors;
*/ */
public class TestableDialogsRunner { public class TestableDialogsRunner {
private final List<TestableDialog> dialogs = new ArrayList<>(); private final Map<Integer, TestableDialog> dialogs = new LinkedHashMap<>();
static final int LAST_SELECTED_GROUP_ID = 997; static final int LAST_SELECTED_GROUP_ID = 997;
static final int LAST_SELECTED_DIALOG_ID = 998; static final int LAST_SELECTED_DIALOG_ID = 998;
@ -79,12 +81,14 @@ public class TestableDialogsRunner {
} }
void registerDialog(TestableDialog dialog) { void registerDialog(TestableDialog dialog) {
this.dialogs.add(dialog); Integer regNumber = this.dialogs.size() + 1;
dialog.setRegNumber(regNumber);
this.dialogs.put(regNumber, dialog);
} }
public void selectAndShowTestableDialog(Player player, Ability source, Game game, Player opponent) { public void selectAndShowTestableDialog(Player player, Ability source, Game game, Player opponent) {
// select group or fast links // select group or fast links
List<String> groups = this.dialogs.stream() List<String> groups = this.dialogs.values().stream()
.map(TestableDialog::getGroup) .map(TestableDialog::getGroup)
.distinct() .distinct()
.sorted() .sorted()
@ -201,8 +205,8 @@ public class TestableDialogsRunner {
return choice; return choice;
} }
public List<TestableDialog> getDialogs() { public Collection<TestableDialog> getDialogs() {
return this.dialogs; return this.dialogs.values();
} }
} }

View file

@ -21,7 +21,6 @@ public interface TestableResult {
/** /**
* Save new result after show dialog * Save new result after show dialog
*
*/ */
void onFinish(boolean resStatus, List<String> resDetails); void onFinish(boolean resStatus, List<String> resDetails);
@ -29,5 +28,11 @@ public interface TestableResult {
void onClear(); void onClear();
Boolean getResAssert(); /**
* Assert dialog result
* - null - not ready (dev must setup wanted result)
* - empty - good
* - not empty - fail (return error message)
*/
String getResAssert();
} }

View file

@ -14,8 +14,9 @@ import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -36,8 +37,7 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
addCard(Zone.HAND, playerA, "Forest", 6); addCard(Zone.HAND, playerA, "Forest", 6);
runCode("run single", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { runCode("run single", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
TestableDialog dialog = findDialogs(runner, "target.choose(you, target)", "any 0-3").get(0); TestableDialog dialog = findDialog(runner, "target.choose(you, target)", "any 0-3");
Assert.assertNotNull(dialog);
dialog.prepare(); dialog.prepare();
dialog.showDialog(playerA, fakeAbility, game, playerB); dialog.showDialog(playerA, fakeAbility, game, playerB);
}); });
@ -51,7 +51,8 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);
execute(); execute();
printRunnerResults(false, false); // it's ok to have wrong targets message cause manual testing selected x2, not AI's x3
assertAndPrintRunnerResults(false, false);
} }
@Test @Test
@ -62,8 +63,7 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA); aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA);
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB); aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB);
runCode("run single", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> { runCode("run single", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> {
TestableDialog dialog = findDialogs(runner, "target.choose(you, target)", "any 0-3").get(0); TestableDialog dialog = findDialog(runner, "target.choose(you, target)", "any 0-3");
Assert.assertNotNull(dialog);
dialog.prepare(); dialog.prepare();
dialog.showDialog(playerA, fakeAbility, game, playerB); dialog.showDialog(playerA, fakeAbility, game, playerB);
}); });
@ -72,7 +72,30 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);
execute(); execute();
printRunnerResults(false, true); assertAndPrintRunnerResults(false, true);
}
@Test
@Ignore // debug only - run single dialog by reg number
public void test_RunSingle_Debugging() {
int needRedNumber = 95;
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
addCard(Zone.HAND, playerA, "Forest", 6);
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA);
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB);
runCode("run by number", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> {
TestableDialog dialog = findDialog(runner, needRedNumber);
dialog.prepare();
dialog.showDialog(playerA, fakeAbility, game, playerB);
});
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAndPrintRunnerResults(false, true);
} }
@Test @Test
@ -86,17 +109,16 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA); aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA);
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB); aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerB);
AtomicInteger dialogNumber = new AtomicInteger();
runCode("run all", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> { runCode("run all", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> {
runner.getDialogs().forEach(dialog -> { runner.getDialogs().forEach(dialog -> {
dialogNumber.incrementAndGet();
System.out.println(String.format("run testable dialog %d of %d (%s, %s - %s)", System.out.println(String.format("run testable dialog %d of %d (%s, %s - %s)",
dialogNumber.get(), dialog.getRegNumber(),
runner.getDialogs().size(), runner.getDialogs().size(),
dialog.getClass().getSimpleName(), dialog.getClass().getSimpleName(),
dialog.getGroup(), dialog.getGroup(),
dialog.getName() dialog.getName()
)); ));
dialog.prepare();
dialog.showDialog(playerA, fakeAbility, game, playerB); dialog.showDialog(playerA, fakeAbility, game, playerB);
}); });
}); });
@ -105,20 +127,31 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);
execute(); execute();
printRunnerResults(true, true); assertAndPrintRunnerResults(true, true);
} }
private List<TestableDialog> findDialogs(TestableDialogsRunner runner, String byGroup, String byName) { private TestableDialog findDialog(TestableDialogsRunner runner, String byGroup, String byName) {
return runner.getDialogs().stream() List<TestableDialog> res = runner.getDialogs().stream()
.filter(d -> d.getGroup().equals(byGroup)) .filter(dialog -> dialog.getGroup().equals(byGroup))
.filter(d -> d.getName().equals(byName)) .filter(dialog -> dialog.getName().equals(byName))
.collect(Collectors.toList()); .collect(Collectors.toList());
Assert.assertEquals("must found only 1 dialog", 1, res.size());
return res.get(0);
} }
private void printRunnerResults(boolean showFullList, boolean assertGoodResults) { private TestableDialog findDialog(TestableDialogsRunner runner, Integer byRegNumber) {
List<TestableDialog> res = runner.getDialogs().stream()
.filter(dialog -> dialog.getRegNumber().equals(byRegNumber))
.collect(Collectors.toList());
Assert.assertEquals("must found only 1 dialog", 1, res.size());
return res.get(0);
}
private void assertAndPrintRunnerResults(boolean showFullList, boolean failOnBadResults) {
// print text table with full dialogs list and results // print text table with full dialogs list and results
// found table sizes // found table sizes
int maxNumberLength = "9999".length();
int maxGroupLength = "Group".length(); int maxGroupLength = "Group".length();
int maxNameLength = "Name".length(); int maxNameLength = "Name".length();
for (TestableDialog dialog : runner.getDialogs()) { for (TestableDialog dialog : runner.getDialogs()) {
@ -130,15 +163,18 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
} }
int maxResultLength = "Result".length(); int maxResultLength = "Result".length();
String rowFormat = "| %-" + maxGroupLength + "s | %-" + maxNameLength + "s | %-" + maxResultLength + "s |%n"; String rowFormat = "| %-" + maxNumberLength + "s | %-" + maxGroupLength + "s | %-" + maxNameLength + "s | %-" + maxResultLength + "s |%n";
String horizontalBorder = "+-" + String horizontalBorder = "+-" +
String.join("", Collections.nCopies(maxNumberLength, "-")) + "-+-" +
String.join("", Collections.nCopies(maxGroupLength, "-")) + "-+-" + String.join("", Collections.nCopies(maxGroupLength, "-")) + "-+-" +
String.join("", Collections.nCopies(maxNameLength, "-")) + "-+-" + String.join("", Collections.nCopies(maxNameLength, "-")) + "-+-" +
String.join("", Collections.nCopies(maxResultLength, "-")) + "-+"; String.join("", Collections.nCopies(maxResultLength, "-")) + "-+";
String totalsLeftFormat = "| %-" + (maxNumberLength + maxGroupLength + maxNameLength + maxResultLength + 9) + "s |%n";
String totalsRightFormat = "| %" + (maxNumberLength + maxGroupLength + maxNameLength + maxResultLength + 9) + "s |%n";
// header // header
System.out.println(horizontalBorder); System.out.println(horizontalBorder);
System.out.printf(rowFormat, "Group", "Name", "Result"); System.out.printf(rowFormat, "N", "Group", "Name", "Result");
System.out.println(horizontalBorder); System.out.println(horizontalBorder);
// data // data
@ -147,47 +183,94 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
int totalGood = 0; int totalGood = 0;
int totalBad = 0; int totalBad = 0;
int totalUnknown = 0; int totalUnknown = 0;
boolean usedHorizontalBorder = true; // mark that last print used horizontal border (fix duplicates)
Map<String, String> coloredTexts = new HashMap<>(); // must colorize after string format to keep pretty table
for (TestableDialog dialog : runner.getDialogs()) { for (TestableDialog dialog : runner.getDialogs()) {
if (!showFullList && !dialog.getResult().isFinished()) { if (!showFullList && !dialog.getResult().isFinished()) {
// print only required dialogs
continue; continue;
} }
totalDialogs++; totalDialogs++;
if (!prevGroup.isEmpty() && !prevGroup.equals(dialog.getGroup())) { if (!prevGroup.isEmpty() && !prevGroup.equals(dialog.getGroup())) {
System.out.println(horizontalBorder); if (!usedHorizontalBorder) {
System.out.println(horizontalBorder);
usedHorizontalBorder = true;
}
} }
prevGroup = dialog.getGroup(); prevGroup = dialog.getGroup();
// print dialog stats
String status; String status;
if (dialog.getResult().getResAssert() == null) { coloredTexts.clear();
status = asYellow("?"); String resAssert = dialog.getResult().getResAssert();
String assertError = "";
if (resAssert == null) {
totalUnknown++; totalUnknown++;
} else if (dialog.getResult().getResAssert()) { status = "?";
coloredTexts.put("?", asYellow("?"));
} else if (resAssert.isEmpty()) {
totalGood++; totalGood++;
status = asGreen("OK"); status = "OK";
coloredTexts.put("OK", asGreen("OK"));
} else { } else {
totalBad++; totalBad++;
status = asRed("FAIL"); status = "FAIL";
coloredTexts.put("FAIL", asRed("FAIL"));
assertError = resAssert;
}
if (!assertError.isEmpty()) {
if (!usedHorizontalBorder) {
System.out.println(horizontalBorder);
usedHorizontalBorder = true;
}
}
System.out.print(getColoredRow(rowFormat, coloredTexts, dialog.getRegNumber(), dialog.getGroup(), dialog.getName(), status));
usedHorizontalBorder = false;
// print dialog error
if (!assertError.isEmpty()) {
coloredTexts.clear();
coloredTexts.put(resAssert, asRed(resAssert));
System.out.print(getColoredRow(totalsRightFormat, coloredTexts, String.format("%s", resAssert)));
System.out.println(horizontalBorder);
usedHorizontalBorder = true;
} }
System.out.printf(rowFormat, dialog.getGroup(), dialog.getName(), status);
} }
System.out.println(horizontalBorder); if (!usedHorizontalBorder) {
System.out.println(horizontalBorder);
usedHorizontalBorder = true;
}
// totals // totals dialogs
System.out.printf("| %-" + (maxGroupLength + maxNameLength + maxResultLength + 6) + "s |%n", System.out.printf(totalsLeftFormat, "Total dialogs: " + totalDialogs);
"Total dialogs: " + totalDialogs); usedHorizontalBorder = false;
System.out.printf("| %-" + (maxGroupLength + maxNameLength + maxResultLength + 6) + "s |%n", // totals results
String.format("Total results: %s good, %s bad, %s unknown", String goodStats = String.format("%d good", totalGood);
asGreen(String.valueOf(totalGood)), String badStats = String.format("%d bad", totalBad);
asRed(String.valueOf(totalBad)), String unknownStats = String.format("%d unknown", totalUnknown);
asYellow(String.valueOf(totalUnknown)) coloredTexts.clear();
) coloredTexts.put(goodStats, String.format("%s good", asGreen(String.valueOf(totalGood))));
); coloredTexts.put(badStats, String.format("%s bad", asRed(String.valueOf(totalBad))));
coloredTexts.put(unknownStats, String.format("%s unknown", asYellow(String.valueOf(totalUnknown))));
System.out.print(getColoredRow(totalsLeftFormat, coloredTexts, String.format("Total results: %s, %s, %s",
goodStats, badStats, unknownStats)));
// table end
System.out.println(horizontalBorder); System.out.println(horizontalBorder);
usedHorizontalBorder = true;
if (assertGoodResults && totalBad > 0) { if (failOnBadResults && totalBad > 0) {
Assert.fail(String.format("Testable dialogs has %d bad results, try to fix it", totalBad)); Assert.fail(String.format("Testable dialogs has %d bad results, try to fix it", totalBad));
} }
} }
private String getColoredRow(String rowFormat, Map<String, String> coloredTexts, Object... args) {
String line = String.format(rowFormat, args);
for (String coloredText : coloredTexts.keySet()) {
line = line.replace(coloredText, coloredTexts.get(coloredText));
}
return line;
}
private String asRed(String text) { private String asRed(String text) {
return "\u001B[31m" + text + "\u001B[0m"; return "\u001B[31m" + text + "\u001B[0m";
} }