tests: added fuzzy testing (disabled by default, added random phased out permanents, part of #13748);

This commit is contained in:
Oleg Agafonov 2025-06-13 23:05:32 +04:00
parent 58b5bb76f9
commit 849aea5946
5 changed files with 147 additions and 8 deletions

View file

@ -1,8 +1,10 @@
package org.mage.test.cards.abilities.keywords; package org.mage.test.cards.abilities.keywords;
import mage.cards.Card;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.util.CardUtil;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
@ -59,6 +61,41 @@ public class DayNightTest extends CardTestPlayerBase {
assertRuffianSmasher(true); assertRuffianSmasher(true);
} }
@Test
public void testCopy() {
// possible bug: stack overflow on copy
addCard(Zone.HAND, playerA, ruffian);
runCode("copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
Card card = currentGame.getCards().stream().filter(c -> c.getName().equals(ruffian)).findFirst().orElse(null);
Assert.assertNotNull(card);
Assert.assertNotNull(card.getSecondCardFace());
// original
Assert.assertNotNull(card.getSecondCardFace());
// copy
Card copy = card.copy();
Assert.assertNotNull(copy.getSecondCardFace());
// deep copy
copy = CardUtil.deepCopyObject(card);
Assert.assertNotNull(copy.getSecondCardFace());
// copied
Card copied = game.copyCard(card, null, playerA.getId());
Assert.assertNotNull(copied.getSecondCardFace());
// copy
copy = copied.copy();
Assert.assertNotNull(copy.getSecondCardFace());
// deep copy
copy = CardUtil.deepCopyObject(copied);
Assert.assertNotNull(copy.getSecondCardFace());
});
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
}
@Test @Test
public void testNightbound() { public void testNightbound() {
currentGame.setDaytime(false); currentGame.setDaytime(false);

View file

@ -62,10 +62,14 @@ public abstract class ModalDoubleFacedCard extends CardImpl implements CardWithH
public ModalDoubleFacedCard(ModalDoubleFacedCard card) { public ModalDoubleFacedCard(ModalDoubleFacedCard card) {
super(card); super(card);
this.leftHalfCard = card.getLeftHalfCard().copy(); if (card.leftHalfCard != null) {
((ModalDoubleFacedCardHalf) leftHalfCard).setParentCard(this); this.leftHalfCard = card.leftHalfCard;
this.rightHalfCard = card.rightHalfCard.copy(); ((ModalDoubleFacedCardHalf) this.leftHalfCard).setParentCard(this);
((ModalDoubleFacedCardHalf) rightHalfCard).setParentCard(this); }
if (card.rightHalfCard != null) {
this.rightHalfCard = card.rightHalfCard;
((ModalDoubleFacedCardHalf) this.rightHalfCard).setParentCard(this);
}
} }
public ModalDoubleFacedCardHalf getLeftHalfCard() { public ModalDoubleFacedCardHalf getLeftHalfCard() {

View file

@ -38,10 +38,14 @@ public abstract class SplitCard extends CardImpl implements CardWithHalves {
protected SplitCard(SplitCard card) { protected SplitCard(SplitCard card) {
super(card); super(card);
this.leftHalfCard = card.getLeftHalfCard().copy(); if (card.leftHalfCard != null) {
((SplitCardHalf) leftHalfCard).setParentCard(this); this.leftHalfCard = card.leftHalfCard;
this.rightHalfCard = card.rightHalfCard.copy(); ((SplitCardHalf) this.leftHalfCard).setParentCard(this);
((SplitCardHalf) rightHalfCard).setParentCard(this); }
if (card.rightHalfCard != null) {
this.rightHalfCard = card.rightHalfCard;
((SplitCardHalf) this.rightHalfCard).setParentCard(this);
}
} }
public void setParts(SplitCardHalf leftHalfCard, SplitCardHalf rightHalfCard) { public void setParts(SplitCardHalf leftHalfCard, SplitCardHalf rightHalfCard) {

View file

@ -16,6 +16,7 @@ import mage.game.permanent.PermanentToken;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.players.Player; import mage.players.Player;
import mage.target.TargetCard; import mage.target.TargetCard;
import mage.util.FuzzyTestsUtil;
import java.util.*; import java.util.*;
@ -416,6 +417,9 @@ public final class ZonesHandler {
&& card.removeFromZone(game, fromZone, source)) { && card.removeFromZone(game, fromZone, source)) {
success = true; success = true;
event.setTarget(permanent); event.setTarget(permanent);
// tests only: inject fuzzy data with random phased out permanents
FuzzyTestsUtil.addRandomPhasedOutPermanent(permanent, source, game);
} else { } else {
// revert controller to owner if permanent does not enter // revert controller to owner if permanent does not enter
game.getContinuousEffects().setController(permanent.getId(), permanent.getOwnerId()); game.getContinuousEffects().setController(permanent.getId(), permanent.getOwnerId());

View file

@ -0,0 +1,90 @@
package mage.util;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.effects.EntersBattlefieldEffect;
import mage.abilities.keyword.PhasingAbility;
import mage.cards.Card;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import java.util.Map;
import java.util.UUID;
/**
* Helper class for fuzzy testing
* <p>
* Support:
* - [x] enable by command line;
* - [x] phased out permanents must be hidden
* - [ ] TODO: out of range players and permanents must be hidden
* - [ ] TODO: leave/disconnected players must be hidden
* <p>
* How-to use:
* - enable here or by command line
* - run unit tests and research the fails
*
* @author JayDi85
*/
public class FuzzyTestsUtil {
/**
* Phased out permanents must be hidden
* Make sure other cards and effects do not see phased out permanents and ignore it
* <p>
* Use case:
* - each permanent on battlefield will have copied phased out version with all abilities and effects
* <p>
* How-to use:
* - set true or run with -Dxmage.tests.addRandomPhasedOutPermanents=true
*/
public static boolean ADD_RANDOM_PHASED_OUT_PERMANENTS = false;
static {
String val = System.getProperty("xmage.tests.addRandomPhasedOutPermanents");
if (val != null) {
ADD_RANDOM_PHASED_OUT_PERMANENTS = Boolean.parseBoolean(val);
}
}
/**
* Create duplicated phased out permanent
*/
public static void addRandomPhasedOutPermanent(Permanent originalPermanent, Ability source, Game game) {
if (!ADD_RANDOM_PHASED_OUT_PERMANENTS) {
return;
}
Player samplePlayer = game.getPlayers().values().stream().findFirst().orElse(null);
if (samplePlayer == null || !samplePlayer.isTestsMode()) {
return;
}
// copy permanent and put it to battlefield as phased out (diff sides also supported here)
// TODO: add phased out tests support (must not fail on it)
Card originalCardSide = game.getCard(originalPermanent.getId());
Card originalCardMain = originalCardSide.getMainCard();
if (!originalCardMain.hasAbility(PhasingAbility.getInstance(), game)) {
boolean canCreate = true;
Card doppelgangerCardMain = game.copyCard(originalCardMain, source, originalPermanent.getControllerId());
Map<UUID, MageObject> mapOldToNew = CardUtil.getOriginalToCopiedPartsMap(originalCardMain, doppelgangerCardMain);
Card doppelgangerCardSide = (Card) mapOldToNew.getOrDefault(originalCardSide.getId(), null);
doppelgangerCardSide.addAbility(PhasingAbility.getInstance());
// compatibility workaround: remove all etb abilities to skip any etb choices (e.g. copy effect)
doppelgangerCardSide.getAbilities().removeIf(a -> a.getEffects().stream().anyMatch(e -> e instanceof EntersBattlefieldEffect));
// compatibility workaround: ignore aura to skip any etb choices (e.g. select new target)
if (doppelgangerCardSide.hasSubtype(SubType.AURA, game)) {
canCreate = false;
}
if (canCreate) {
doppelgangerCardSide.putOntoBattlefield(game, Zone.BATTLEFIELD, source, originalPermanent.getControllerId());
Permanent doppelgangerPerm = CardUtil.getPermanentFromCardPutToBattlefield(doppelgangerCardSide, game);
doppelgangerPerm.phaseOut(game, true); // use indirect, so no phase in on untap
}
}
}
}