tests: improved load tests (improved progress bar, added effects stats)

This commit is contained in:
Oleg Agafonov 2025-02-04 11:41:42 +04:00
parent f5b901beb4
commit 3405b51aaf
7 changed files with 55 additions and 24 deletions

View file

@ -62,7 +62,11 @@ public class GameView implements Serializable {
private final int turn; private final int turn;
private boolean special = false; private boolean special = false;
private final boolean rollbackTurnsAllowed; private final boolean rollbackTurnsAllowed;
// for debug only
// TODO: implement and support in admin tools
private int totalErrorsCount; private int totalErrorsCount;
private int totalEffectsCount;
public GameView(GameState state, Game game, UUID createdForPlayerId, UUID watcherUserId) { public GameView(GameState state, Game game, UUID createdForPlayerId, UUID watcherUserId) {
Player createdForPlayer = null; Player createdForPlayer = null;
@ -209,6 +213,7 @@ public class GameView implements Serializable {
} }
this.rollbackTurnsAllowed = game.getOptions().rollbackTurnsAllowed; this.rollbackTurnsAllowed = game.getOptions().rollbackTurnsAllowed;
this.totalErrorsCount = game.getTotalErrorsCount(); this.totalErrorsCount = game.getTotalErrorsCount();
this.totalEffectsCount = game.getTotalEffectsCount();
} }
private void checkPaid(UUID uuid, StackAbility stackAbility) { private void checkPaid(UUID uuid, StackAbility stackAbility) {
@ -349,4 +354,8 @@ public class GameView implements Serializable {
public int getTotalErrorsCount() { public int getTotalErrorsCount() {
return this.totalErrorsCount; return this.totalErrorsCount;
} }
public int getTotalEffectsCount() {
return this.totalEffectsCount;
}
} }

View file

@ -1001,10 +1001,10 @@ public class MageServerImpl implements MageServer {
} }
public void handleException(Exception ex) throws MageException { public void handleException(Exception ex) throws MageException {
if (!ex.getMessage().equals("No message")) { if (ex.getMessage() != null && !ex.getMessage().equals("No message")) {
logger.fatal("", ex);
throw new MageException("Server error: " + ex.getMessage()); throw new MageException("Server error: " + ex.getMessage());
} }
logger.error("unknown error", ex); // TODO: on logs spamming (e.g. connection problems) move it inside condition block above
} }
@Override @Override

View file

@ -33,7 +33,7 @@ public class LoadCallbackClient implements CallbackClient {
private final String logsPrefix; private final String logsPrefix;
private final Boolean showLogsAsHtml; // original game logs in HTML, but it can be converted to txt for more readable console private final Boolean showLogsAsHtml; // original game logs in HTML, but it can be converted to txt for more readable console
private String globalProgress = ""; // progress [=20, +21, +17], AI game #9: --- private String globalProgress = ""; // progress 33% [=20, +21, +17], AI game #9: ---
public LoadCallbackClient(boolean joinGameChat, String logsPrefix, Boolean showLogsAsHtml) { public LoadCallbackClient(boolean joinGameChat, String logsPrefix, Boolean showLogsAsHtml) {
this.joinGameChat = joinGameChat; this.joinGameChat = joinGameChat;

View file

@ -321,7 +321,7 @@ public class LoadTest {
long randomSeed = RandomUtil.nextInt(); long randomSeed = RandomUtil.nextInt();
LoadTestGameResult gameResult = gameResults.createGame(0, "test game", randomSeed); LoadTestGameResult gameResult = gameResults.createGame(0, "test game", randomSeed);
TasksProgress tasksProgress = new TasksProgress(); TasksProgress tasksProgress = new TasksProgress();
tasksProgress.update(1, true, 0); tasksProgress.update(1, false, 0);
playTwoAIGame("Single AI game", 1, tasksProgress, randomSeed, "WGUBR", TEST_AI_RANDOM_DECK_SETS, gameResult); playTwoAIGame("Single AI game", 1, tasksProgress, randomSeed, "WGUBR", TEST_AI_RANDOM_DECK_SETS, gameResult);
printGameResults(gameResults); printGameResults(gameResults);
@ -333,12 +333,13 @@ public class LoadTest {
// play multiple AI games with CLIENT side code (catch every GameView changes from the server) // play multiple AI games with CLIENT side code (catch every GameView changes from the server)
int singleGameSID = 0; // set sid for same deck games, set 0 for random decks int singleGameSID = 0; // set sid for same deck games, set 0 for random decks
int gamesAmount = 5; // games per run
boolean isRunParallel = true; // can generate too much logs in test run, so search server logs for possible errors int runTotalGames = 10;
int runMaxParallelGames = 5; // use 1 to run one by one (warning, it's limited by COMPUTER_MAX_THREADS_FOR_SIMULATIONS)
ExecutorService executerService; ExecutorService executerService;
if (isRunParallel) { if (runMaxParallelGames > 1) {
executerService = Executors.newFixedThreadPool(gamesAmount, new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TESTS_AI_VS_AI_GAMES)); executerService = Executors.newFixedThreadPool(runMaxParallelGames, new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TESTS_AI_VS_AI_GAMES));
} else { } else {
executerService = Executors.newSingleThreadExecutor(new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TESTS_AI_VS_AI_GAMES)); executerService = Executors.newSingleThreadExecutor(new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TESTS_AI_VS_AI_GAMES));
} }
@ -346,11 +347,11 @@ public class LoadTest {
// save random seeds for repeated results (in decks generating) // save random seeds for repeated results (in decks generating)
List<Integer> seedsList = new ArrayList<>(); List<Integer> seedsList = new ArrayList<>();
if (singleGameSID != 0) { if (singleGameSID != 0) {
for (int i = 1; i <= gamesAmount; i++) { for (int i = 1; i <= runTotalGames; i++) {
seedsList.add(singleGameSID); seedsList.add(singleGameSID);
} }
} else { } else {
for (int i = 1; i <= gamesAmount; i++) { for (int i = 1; i <= runTotalGames; i++) {
seedsList.add(RandomUtil.nextInt()); seedsList.add(RandomUtil.nextInt());
} }
} }
@ -360,7 +361,7 @@ public class LoadTest {
TasksProgress tasksProgress = new TasksProgress(); TasksProgress tasksProgress = new TasksProgress();
for (int i = 0; i < seedsList.size(); i++) { for (int i = 0; i < seedsList.size(); i++) {
int gameIndex = i; int gameIndex = i;
tasksProgress.update(gameIndex + 1, true, 0); tasksProgress.update(gameIndex + 1, false, 0);
long randomSeed = seedsList.get(i); long randomSeed = seedsList.get(i);
logger.info("Game " + (i + 1) + " of " + seedsList.size() + ", RANDOM seed: " + randomSeed); logger.info("Game " + (i + 1) + " of " + seedsList.size() + ", RANDOM seed: " + randomSeed);
Future gameTask = executerService.submit(() -> { Future gameTask = executerService.submit(() -> {
@ -369,12 +370,12 @@ public class LoadTest {
playTwoAIGame(gameName, gameIndex + 1, tasksProgress, randomSeed, TEST_AI_RANDOM_DECK_COLORS_FOR_AI_GAME, TEST_AI_RANDOM_DECK_SETS, gameResult); playTwoAIGame(gameName, gameIndex + 1, tasksProgress, randomSeed, TEST_AI_RANDOM_DECK_COLORS_FOR_AI_GAME, TEST_AI_RANDOM_DECK_SETS, gameResult);
}); });
if (!isRunParallel) { if (runMaxParallelGames <= 1) {
// run one by one // run one by one
gameTask.get(); gameTask.get();
} }
} }
if (isRunParallel) { if (runMaxParallelGames > 1) {
// run parallel // run parallel
executerService.shutdown(); executerService.shutdown();
Assert.assertTrue(executerService.awaitTermination(1, TimeUnit.HOURS)); Assert.assertTrue(executerService.awaitTermination(1, TimeUnit.HOURS));
@ -601,14 +602,18 @@ public class LoadTest {
} }
private void updateInfo() { private void updateInfo() {
// example: progress [=00, +01, +01, =12, =15, =01, +61] // example: progress 70% [=00, +01, +01, =12, =15, =01, +61]
int completed = this.finishes.values().stream().mapToInt(x -> x ? 1 : 0).sum();
int completedPercent = this.finishes.size() == 0 ? 0 : completed * 100 / this.finishes.size();
String res = this.finishes.keySet().stream() String res = this.finishes.keySet().stream()
.map(taskNumber -> String.format("%s%02d", .map(taskNumber -> String.format("%s%02d",
this.finishes.getOrDefault(taskNumber, false) ? "=" : "+", this.finishes.getOrDefault(taskNumber, false) ? "=" : "+",
this.turns.getOrDefault(taskNumber, 0) this.turns.getOrDefault(taskNumber, 0)
)) ))
.collect(Collectors.joining(", ")); .collect(Collectors.joining(", "));
this.info = String.format("progress [%s]", res); this.info = String.format("progress %d%% [%s]", completedPercent, res);
} }
public String getInfo() { public String getInfo() {
@ -877,12 +882,16 @@ public class LoadTest {
public int getTotalErrorsCount() { public int getTotalErrorsCount() {
return finalGameView == null ? 0 : this.finalGameView.getTotalErrorsCount(); return finalGameView == null ? 0 : this.finalGameView.getTotalErrorsCount();
} }
public int getTotalEffectsCount() {
return finalGameView == null ? 0 : this.finalGameView.getTotalEffectsCount();
}
} }
private static class LoadTestGameResultsList extends HashMap<Integer, LoadTestGameResult> { private static class LoadTestGameResultsList extends HashMap<Integer, LoadTestGameResult> {
private static final String tableFormatHeader = "|%-10s|%-15s|%-20s|%-10s|%-10s|%-10s|%-10s|%-15s|%-15s|%-10s|%n"; private static final String tableFormatHeader = "|%-10s|%-15s|%-20s|%-10s|%-10s|%-10s|%-10s|%-10s|%-15s|%-15s|%-10s|%n";
private static final String tableFormatData = "|%-10s|%15s|%20s|%10s|%10s|%10s|%10s|%15s|%15s|%10s|%n"; private static final String tableFormatData = "|%-10s|%15s|%20s|%10s|%10s|%10s|%10s|%10s|%15s|%15s|%10s|%n";
public LoadTestGameResult createGame(int index, String name, long randomSeed) { public LoadTestGameResult createGame(int index, String name, long randomSeed) {
if (this.containsKey(index)) { if (this.containsKey(index)) {
@ -899,6 +908,7 @@ public class LoadTest {
"name", "name",
"random sid", "random sid",
"errors", "errors",
"effects",
"turn", "turn",
"life p1", "life p1",
"life p2", "life p2",
@ -920,6 +930,7 @@ public class LoadTest {
gameResult.name, //"name", gameResult.name, //"name",
String.valueOf(gameResult.randomSeed), // "random sid", String.valueOf(gameResult.randomSeed), // "random sid",
String.valueOf(gameResult.getTotalErrorsCount()), // "errors", String.valueOf(gameResult.getTotalErrorsCount()), // "errors",
String.valueOf(gameResult.getTotalEffectsCount()), // "effects",
String.valueOf(gameResult.getTurn()), //"turn", String.valueOf(gameResult.getTurn()), //"turn",
String.valueOf(gameResult.getLife1()), //"life p1", String.valueOf(gameResult.getLife1()), //"life p1",
String.valueOf(gameResult.getLife2()), //"life p2", String.valueOf(gameResult.getLife2()), //"life p2",
@ -937,6 +948,7 @@ public class LoadTest {
String.valueOf(this.size()), //"name", String.valueOf(this.size()), //"name",
"total, secs: " + String.format("%.3f", (float) this.getTotalDurationMs() / 1000), // "random sid", "total, secs: " + String.format("%.3f", (float) this.getTotalDurationMs() / 1000), // "random sid",
String.valueOf(this.getTotalErrorsCount()), // errors String.valueOf(this.getTotalErrorsCount()), // errors
String.valueOf(this.getAvgEffectsCount()), // effects
String.valueOf(this.getAvgTurn()), // turn String.valueOf(this.getAvgTurn()), // turn
String.valueOf(this.getAvgLife1()), // life p1 String.valueOf(this.getAvgLife1()), // life p1
String.valueOf(this.getAvgLife2()), // life p2 String.valueOf(this.getAvgLife2()), // life p2
@ -952,6 +964,10 @@ public class LoadTest {
return this.values().stream().mapToInt(LoadTestGameResult::getTotalErrorsCount).sum(); return this.values().stream().mapToInt(LoadTestGameResult::getTotalErrorsCount).sum();
} }
private int getAvgEffectsCount() {
return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getTotalEffectsCount).sum() / this.size();
}
private int getAvgTurn() { private int getAvgTurn() {
return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getTurn).sum() / this.size(); return this.size() == 0 ? 0 : this.values().stream().mapToInt(LoadTestGameResult::getTurn).sum() / this.size();
} }

View file

@ -1487,8 +1487,12 @@ public class ContinuousEffects implements Serializable {
} }
} }
public int getTotalEffectsCount() {
return allEffectsLists.stream().mapToInt(ContinuousEffectsList::size).sum();
}
@Override @Override
public String toString() { public String toString() {
return "Effects: " + allEffectsLists.stream().mapToInt(ContinuousEffectsList::size).sum(); return "Effects: " + getTotalEffectsCount();
} }
} }

View file

@ -314,7 +314,9 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
Player getLosingPlayer(); Player getLosingPlayer();
int getTotalErrorsCount(); int getTotalErrorsCount(); // debug only
int getTotalEffectsCount(); // debug only
//client event methods //client event methods
void addTableEventListener(Listener<TableEvent> listener); void addTableEventListener(Listener<TableEvent> listener);

View file

@ -3547,11 +3547,6 @@ public abstract class GameImpl implements Game {
} }
protected void removeCreaturesFromCombat() {
//20091005 - 511.3
getCombat().endCombat(this);
}
@Override @Override
public ContinuousEffects getContinuousEffects() { public ContinuousEffects getContinuousEffects() {
return state.getContinuousEffects(); return state.getContinuousEffects();
@ -3694,6 +3689,11 @@ public abstract class GameImpl implements Game {
return this.totalErrorsCount.get(); return this.totalErrorsCount.get();
} }
@Override
public int getTotalEffectsCount() {
return this.getContinuousEffects().getTotalEffectsCount();
}
@Override @Override
public void cheat(UUID ownerId, Map<Zone, String> commands) { public void cheat(UUID ownerId, Map<Zone, String> commands) {
if (commands != null) { if (commands != null) {