tests: improved testable dialogs (added source code ref in result table for better IDE navigation, part of #13643, #13638);

This commit is contained in:
Oleg Agafonov 2025-06-15 14:08:43 +04:00
parent 4f8eb30e4c
commit d893d52190
21 changed files with 127 additions and 49 deletions

View file

@ -11,8 +11,8 @@ public class AmountTestableResult extends BaseTestableResult {
int amount = 0;
public void onFinish(boolean status, List<String> info, int amount) {
this.onFinish(status, info);
public void onFinish(String resDebugSource, boolean status, List<String> info, int amount) {
this.onFinish(resDebugSource, status, info);
this.amount = amount;
}

View file

@ -3,6 +3,7 @@ package mage.utils.testers;
import mage.abilities.Ability;
import mage.game.Game;
import mage.players.Player;
import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@ -37,12 +38,12 @@ class AnnounceXTestableDialog extends BaseTestableDialog {
public void showDialog(Player player, Ability source, Game game, Player opponent) {
Player choosingPlayer = this.isYou ? player : opponent;
String message = "<font color=green>message</font> with html";
int chooseRes;
chooseRes = choosingPlayer.announceX(this.min, this.max, message, game, source, this.isMana);
String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
int chooseRes = choosingPlayer.announceX(this.min, this.max, message, game, source, this.isMana);
List<String> res = new ArrayList<>();
res.add(getGroup() + " - " + this.getName() + " selected " + chooseRes);
((AmountTestableResult) this.getResult()).onFinish(true, res, chooseRes);
((AmountTestableResult) this.getResult()).onFinish(chooseDebugSource, true, res, chooseRes);
}
static public void register(TestableDialogsRunner runner) {

View file

@ -11,9 +11,15 @@ import java.util.List;
public class BaseTestableResult implements TestableResult {
boolean isFinished = false;
String resDebugSource = ""; // source code line to find starting place to debug
boolean resStatus = false;
List<String> resInfo = new ArrayList<>();
@Override
public String getResDebugSource() {
return this.resDebugSource;
}
@Override
public boolean getResStatus() {
return this.resStatus;
@ -30,8 +36,9 @@ public class BaseTestableResult implements TestableResult {
}
@Override
public void onFinish(boolean resStatus, List<String> resDetails) {
public void onFinish(String resDebugSource, boolean resStatus, List<String> resDetails) {
this.isFinished = true;
this.resDebugSource = resDebugSource;
this.resStatus = resStatus;
this.resInfo = resDetails;
}

View file

@ -11,8 +11,8 @@ public class ChoiceTestableResult extends BaseTestableResult {
String choice = null;
public void onFinish(boolean status, List<String> info, String choice) {
this.onFinish(status, info);
public void onFinish(String resDebugSource, boolean status, List<String> info, String choice) {
this.onFinish(resDebugSource, status, info);
this.choice = choice;
}

View file

@ -7,6 +7,7 @@ import mage.players.Player;
import mage.target.TargetAmount;
import mage.target.Targets;
import mage.target.common.TargetAnyTargetAmount;
import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@ -46,6 +47,7 @@ class ChooseAmountTestableDialog extends BaseTestableDialog {
Player choosingPlayer = this.isYou ? player : opponent;
// TODO: add "damage" word in ability text, so chooseTargetAmount an show diff dialog (due inner logic - distribute damage or 1/1)
String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
boolean chooseRes = choosingPlayer.chooseTargetAmount(Outcome.Benefit, choosingTarget, source, game);
List<String> res = new ArrayList<>();
if (chooseRes) {
@ -54,7 +56,7 @@ class ChooseAmountTestableDialog extends BaseTestableDialog {
Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "FALSE", new Targets(choosingTarget), source, game, res);
}
((TargetTestableResult) this.getResult()).onFinish(chooseRes, res, choosingTarget);
((TargetTestableResult) this.getResult()).onFinish(chooseDebugSource, chooseRes, res, choosingTarget);
}
static public void register(TestableDialogsRunner runner) {

View file

@ -13,6 +13,7 @@ import mage.players.Player;
import mage.target.TargetCard;
import mage.target.Targets;
import mage.target.common.TargetCardInHand;
import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@ -57,9 +58,12 @@ class ChooseCardsTestableDialog extends BaseTestableDialog {
Cards choosingCards = new CardsImpl(all.stream().limit(100).collect(Collectors.toList()));
boolean chooseRes;
String chooseDebugSource;
if (this.isTargetChoice) {
chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingPlayer.chooseTarget(Outcome.Benefit, choosingCards, choosingTarget, source, game);
} else {
chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingPlayer.choose(Outcome.Benefit, choosingCards, choosingTarget, source, game);
}
@ -70,7 +74,7 @@ class ChooseCardsTestableDialog extends BaseTestableDialog {
Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "FALSE", new Targets(choosingTarget), source, game, res);
}
((TargetTestableResult) this.getResult()).onFinish(chooseRes, res, choosingTarget);
((TargetTestableResult) this.getResult()).onFinish(chooseDebugSource, chooseRes, res, choosingTarget);
}
static public void register(TestableDialogsRunner runner) {

View file

@ -5,6 +5,7 @@ import mage.choices.*;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@ -37,6 +38,7 @@ class ChooseChoiceTestableDialog extends BaseTestableDialog {
public void showDialog(Player player, Ability source, Game game, Player opponent) {
Player choosingPlayer = this.isYou ? player : opponent;
Choice dialog = this.choice.copy();
String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
boolean chooseRes = choosingPlayer.choose(Outcome.Benefit, dialog, game);
List<String> res = new ArrayList<>();
@ -52,7 +54,7 @@ class ChooseChoiceTestableDialog extends BaseTestableDialog {
res.add(String.format("* selected value: %s", choice));
}
((ChoiceTestableResult) this.getResult()).onFinish(chooseRes, res, choice);
((ChoiceTestableResult) this.getResult()).onFinish(chooseDebugSource, chooseRes, res, choice);
}
static public void register(TestableDialogsRunner runner) {

View file

@ -6,6 +6,7 @@ import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@ -51,12 +52,13 @@ class ChoosePileTestableDialog extends BaseTestableDialog {
List<Card> pile2 = all.stream().limit(this.pileSize2).collect(Collectors.toList());
Player choosingPlayer = this.isYou ? player : opponent;
String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
boolean chooseRes = choosingPlayer.choosePile(Outcome.Benefit, mainMessage, pile1, pile2, game);
List<String> res = new ArrayList<>();
res.add(getGroup() + " - " + this.getName() + " - " + (chooseRes ? "TRUE" : "FALSE"));
res.add(" * selected pile: " + (chooseRes ? "pile 1" : "pile 2"));
this.getResult().onFinish(chooseRes, res);
this.getResult().onFinish(chooseDebugSource, chooseRes, res);
}
static public void register(TestableDialogsRunner runner) {

View file

@ -6,6 +6,7 @@ import mage.game.Game;
import mage.players.Player;
import mage.target.Target;
import mage.target.Targets;
import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@ -51,18 +52,23 @@ class ChooseTargetTestableDialog extends BaseTestableDialog {
Player choosingPlayer = this.isYou ? player : opponent;
boolean chooseRes;
String chooseDebugSource;
if (this.isPlayerChoice) {
// player.chooseXXX
if (this.isTargetChoice) {
chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingPlayer.chooseTarget(Outcome.Benefit, choosingTarget, source, game);
} else {
chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingPlayer.choose(Outcome.Benefit, choosingTarget, source, game);
}
} else {
// target.chooseXXX
if (this.isTargetChoice) {
chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingTarget.chooseTarget(Outcome.Benefit, choosingPlayer.getId(), source, game);
} else {
chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
chooseRes = choosingTarget.choose(Outcome.Benefit, choosingPlayer.getId(), source, game);
}
}
@ -74,7 +80,7 @@ class ChooseTargetTestableDialog extends BaseTestableDialog {
Targets.printDebugTargets(getGroup() + " - " + this.getName() + " - " + "FALSE", new Targets(choosingTarget), source, game, res);
}
((TargetTestableResult) this.getResult()).onFinish(chooseRes, res, choosingTarget);
((TargetTestableResult) this.getResult()).onFinish(chooseDebugSource, chooseRes, res, choosingTarget);
}
private ChooseTargetTestableDialog aiMustChoose(boolean resStatus, int targetsCount) {

View file

@ -5,6 +5,7 @@ import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@ -48,6 +49,7 @@ class ChooseUseTestableDialog extends BaseTestableDialog {
@Override
public void showDialog(Player player, Ability source, Game game, Player opponent) {
Player choosingPlayer = this.isYou ? player : opponent;
String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
boolean chooseRes = choosingPlayer.chooseUse(
Outcome.Benefit,
messageMain,
@ -60,7 +62,7 @@ class ChooseUseTestableDialog extends BaseTestableDialog {
List<String> res = new ArrayList<>();
res.add(chooseRes ? "TRUE" : "FALSE");
this.getResult().onFinish(chooseRes, res);
this.getResult().onFinish(chooseDebugSource, chooseRes, res);
}
static public void register(TestableDialogsRunner runner) {

View file

@ -3,6 +3,7 @@ package mage.utils.testers;
import mage.abilities.Ability;
import mage.game.Game;
import mage.players.Player;
import mage.util.DebugUtil;
import java.util.ArrayList;
import java.util.Arrays;
@ -39,12 +40,12 @@ class GetAmountTestableDialog extends BaseTestableDialog {
public void showDialog(Player player, Ability source, Game game, Player opponent) {
Player choosingPlayer = this.isYou ? player : opponent;
String message = "<font color=green>message</font> with html";
int chooseRes;
chooseRes = choosingPlayer.getAmount(this.min, this.max, message, source, game);
String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
int chooseRes = choosingPlayer.getAmount(this.min, this.max, message, source, game);
List<String> res = new ArrayList<>();
res.add(getGroup() + " - " + this.getName() + " selected " + chooseRes);
((AmountTestableResult) this.getResult()).onFinish(true, res, chooseRes);
((AmountTestableResult) this.getResult()).onFinish(chooseDebugSource, true, res, chooseRes);
}
static public void register(TestableDialogsRunner runner) {

View file

@ -5,6 +5,7 @@ import mage.constants.MultiAmountType;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import mage.util.DebugUtil;
import mage.util.MultiAmountMessage;
import java.util.ArrayList;
@ -54,9 +55,9 @@ class GetMultiAmountTestableDialog extends BaseTestableDialog {
public void showDialog(Player player, Ability source, Game game, Player opponent) {
Player choosingPlayer = this.isYou ? player : opponent;
//String message = "<font color=green>message</font> with html";
List<Integer> chooseRes;
List<MultiAmountMessage> options = this.amountOptions.stream().map(MultiAmountMessage::copy).collect(Collectors.toList());
chooseRes = choosingPlayer.getMultiAmountWithIndividualConstraints(
String chooseDebugSource = DebugUtil.getMethodNameWithSource(0, "class");
List<Integer> chooseRes = choosingPlayer.getMultiAmountWithIndividualConstraints(
Outcome.Benefit,
options,
this.totalMin,
@ -82,7 +83,7 @@ class GetMultiAmountTestableDialog extends BaseTestableDialog {
}
res.add("total selected: " + selectedTotal);
((MultiAmountTestableResult) this.getResult()).onFinish(true, res, chooseRes);
((MultiAmountTestableResult) this.getResult()).onFinish(chooseDebugSource, true, res, chooseRes);
}
static public void register(TestableDialogsRunner runner) {

View file

@ -12,8 +12,8 @@ public class MultiAmountTestableResult extends BaseTestableResult {
List<Integer> values = new ArrayList<>();
public void onFinish(boolean status, List<String> info, List<Integer> values) {
this.onFinish(status, info);
public void onFinish(String resDebugSource, boolean status, List<String> info, List<Integer> values) {
this.onFinish(resDebugSource, status, info);
this.values = values;
}

View file

@ -17,8 +17,8 @@ public class TargetTestableResult extends BaseTestableResult {
boolean aiAssertResStatus = false;
int aiAssertTargetsCount = 0;
public void onFinish(boolean status, List<String> info, Target target) {
this.onFinish(status, info);
public void onFinish(String resDebugSource, boolean status, List<String> info, Target target) {
this.onFinish(resDebugSource, status, info);
this.target = target;
}

View file

@ -9,6 +9,11 @@ import java.util.List;
*/
public interface TestableResult {
/**
* Get source code line with called dialog, use it as starting debug point
*/
String getResDebugSource();
/**
* Dialog's result
*/
@ -22,7 +27,7 @@ public interface TestableResult {
/**
* Save new result after show dialog
*/
void onFinish(boolean resStatus, List<String> resDetails);
void onFinish(String chooseDebugSource, boolean resStatus, List<String> resDetails);
boolean isFinished();

View file

@ -148,6 +148,12 @@ public class ComputerPlayer extends PlayerImpl {
log.debug("choose: " + outcome.toString() + ':' + target.toString());
}
// choose itself for starting player all the time
if (target.getMessage(game).equals("Select a starting player")) {
target.add(this.getId(), game);
return true;
}
boolean isAddedSomething = false; // must return true on any changes in targets, so game can ask next choose dialog until finish
// controller hints:

View file

@ -297,7 +297,7 @@ public class HumanPlayer extends PlayerImpl {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Setting game priority for " + getId() + " [" + DebugUtil.getMethodNameWithSource(1) + ']');
logger.debug("Setting game priority for " + getId() + " [" + DebugUtil.getMethodNameWithSource(1, "method") + ']');
}
game.getState().setPriorityPlayerId(getId());
}
@ -328,7 +328,7 @@ public class HumanPlayer extends PlayerImpl {
while (loop) {
// start waiting for next answer
response.clear();
response.setActiveAction(game, DebugUtil.getMethodNameWithSource(1));
response.setActiveAction(game, DebugUtil.getMethodNameWithSource(1, "method"));
game.resumeTimer(getTurnControlledBy());
responseOpenedForAnswer = true;

View file

@ -78,7 +78,7 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
@Test
@Ignore // debug only - run single dialog by reg number
public void test_RunSingle_Debugging() {
int needRedNumber = 95;
int needRegNumber = 7;
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
addCard(Zone.HAND, playerA, "Forest", 6);
@ -86,7 +86,7 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
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);
TestableDialog dialog = findDialog(runner, needRegNumber);
dialog.prepare();
dialog.showDialog(playerA, fakeAbility, game, playerB);
});
@ -148,20 +148,31 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
}
private void assertAndPrintRunnerResults(boolean showFullList, boolean failOnBadResults) {
// print text table with full dialogs list and results
// print table with full dialogs list and assert results
// found table sizes
// auto-size for columns
int maxNumberLength = "9999".length();
int maxGroupLength = "Group".length();
int maxNameLength = "Name".length();
int maxResultLength = "Result".length();
int needTotalsSize = 0;
for (TestableDialog dialog : runner.getDialogs()) {
if (!showFullList && !dialog.getResult().isFinished()) {
continue;
}
maxGroupLength = Math.max(maxGroupLength, dialog.getGroup().length());
maxNameLength = Math.max(maxNameLength, dialog.getName().length());
// resize group to keep space for bigger total message like assert res
String resAssert = dialog.getResult().getResAssert();
resAssert = resAssert == null ? "" : resAssert;
needTotalsSize = Math.max(needTotalsSize, dialog.getResult().getResDebugSource().length());
needTotalsSize = Math.max(needTotalsSize, resAssert.length());
int currentTotalsSize = maxNumberLength + maxGroupLength + maxNameLength + maxResultLength + 9;
if (currentTotalsSize < needTotalsSize) {
maxGroupLength = maxGroupLength + needTotalsSize - currentTotalsSize;
}
}
int maxResultLength = "Result".length();
String rowFormat = "| %-" + maxNumberLength + "s | %-" + maxGroupLength + "s | %-" + maxNameLength + "s | %-" + maxResultLength + "s |%n";
String horizontalBorder = "+-" +
@ -172,12 +183,12 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
String totalsLeftFormat = "| %-" + (maxNumberLength + maxGroupLength + maxNameLength + maxResultLength + 9) + "s |%n";
String totalsRightFormat = "| %" + (maxNumberLength + maxGroupLength + maxNameLength + maxResultLength + 9) + "s |%n";
// header
// print header row
System.out.println(horizontalBorder);
System.out.printf(rowFormat, "N", "Group", "Name", "Result");
System.out.println(horizontalBorder);
// data
// print data rows
String prevGroup = "";
int totalDialogs = 0;
int totalGood = 0;
@ -203,6 +214,7 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
String status;
coloredTexts.clear();
String resAssert = dialog.getResult().getResAssert();
String resDebugSource = dialog.getResult().getResDebugSource();
String assertError = "";
if (resAssert == null) {
totalUnknown++;
@ -231,7 +243,9 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
if (!assertError.isEmpty()) {
coloredTexts.clear();
coloredTexts.put(resAssert, asRed(resAssert));
System.out.print(getColoredRow(totalsRightFormat, coloredTexts, String.format("%s", resAssert)));
coloredTexts.put(resDebugSource, asRed(resDebugSource));
System.out.print(getColoredRow(totalsRightFormat, coloredTexts, resAssert));
System.out.print(getColoredRow(totalsRightFormat, coloredTexts, resDebugSource));
System.out.println(horizontalBorder);
usedHorizontalBorder = true;
}
@ -241,10 +255,9 @@ public class TestableDialogsTest extends CardTestPlayerBaseWithAIHelps {
usedHorizontalBorder = true;
}
// totals dialogs
// print totals
System.out.printf(totalsLeftFormat, "Total dialogs: " + totalDialogs);
usedHorizontalBorder = false;
// totals results
String goodStats = String.format("%d good", totalGood);
String badStats = String.format("%d bad", totalBad);
String unknownStats = String.format("%d unknown", totalUnknown);

View file

@ -2267,6 +2267,13 @@ public class TestPlayer implements Player {
@Override
public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map<String, Serializable> options) {
// choose itself for starting player all the time
if (target.getMessage(game).equals("Select a starting player")) {
target.add(this.getId(), game);
return true;
}
UUID abilityControllerId = this.getId();
if (target.getTargetController() != null && target.getAbilityController() != null) {
abilityControllerId = target.getAbilityController();
@ -2276,11 +2283,6 @@ public class TestPlayer implements Player {
// most use cases - discard and other cost with choice like that method
// must migrate all choices.remove(xxx) to choices.remove(0), takeMaxTargetsPerChoose can help to find it
// ignore player select
if (target.getMessage(game).equals("Select a starting player")) {
return computerPlayer.choose(outcome, target, source, game, options);
}
boolean isAddedSomething = false; // must return true on any changes in targets, so game can ask next choose dialog until finish
assertAliasSupportInChoices(true);

View file

@ -15,9 +15,9 @@ public class DebugUtilTest extends CardTestPlayerBase {
}
private void secondMethod() {
String resCurrent = DebugUtil.getMethodNameWithSource(0);
String resPrev = DebugUtil.getMethodNameWithSource(1);
String resPrevPrev = DebugUtil.getMethodNameWithSource(2);
String resCurrent = DebugUtil.getMethodNameWithSource(0, "method");
String resPrev = DebugUtil.getMethodNameWithSource(1, "method");
String resPrevPrev = DebugUtil.getMethodNameWithSource(2, "method");
Assert.assertTrue("must find secondMethod, but get " + resCurrent, resCurrent.startsWith("secondMethod"));
Assert.assertTrue("must find firstMethod, but get " + resPrev, resPrev.startsWith("firstMethod"));
Assert.assertTrue("must find test_StackTraceWithSourceName, but get " + resPrevPrev, resPrevPrev.startsWith("test_StackTraceWithSourceName"));

View file

@ -68,16 +68,24 @@ public class DebugUtil {
public static String NETWORK_PROFILE_REQUESTS_DUMP_FILE_NAME = "httpRequests.log";
/**
* Return method and source line number like "secondMethod - DebugUtilTest.java:21"
* Return source line number for better debugging like "secondMethod - DebugUtilTest.java:21"
*
* @param skipMethodsAmount use 0 to return current method info, use 1 for prev method, use 2 for prev-prev method
* @param skipMethodsAmount use 0 to return current method info, use 1 for prev method, use 2 for prev-prev method, etc
* @param infoType use "class" for full class name and "method" for short method name
*/
public static String getMethodNameWithSource(final int skipMethodsAmount) {
public static String getMethodNameWithSource(int skipMethodsAmount, String infoType) {
// 3 is default methods amount to skip:
// - getMethodNameWithSource
// - TraceHelper.getMethodNameWithSource
// - Thread.currentThread().getStackTrace()
return TraceHelper.getMethodNameWithSource(3 + skipMethodsAmount);
switch (infoType) {
case "class":
return TraceHelper.getClassNameWithSource(3 + skipMethodsAmount);
case "method":
return TraceHelper.getMethodNameWithSource(3 + skipMethodsAmount);
default:
throw new IllegalArgumentException("Unknown info type: " + infoType);
}
}
}
@ -88,6 +96,9 @@ public class DebugUtil {
*/
class TraceHelper {
/**
* Example: showDialog - ChooseTargetTestableDialog.java:72
*/
public static String getMethodNameWithSource(final int depth) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
if (stackTrace.length == 0) {
@ -96,4 +107,17 @@ class TraceHelper {
return String.format("%s - %s:%d", stackTrace[depth].getMethodName(), stackTrace[depth].getFileName(), stackTrace[depth].getLineNumber());
}
}
/**
* Compatible with IntelliJ IDEA's logs navigation (will be clickable)
* Example: mage.utils.testers.ChooseTargetTestableDialog(ChooseTargetTestableDialog.java:62)
*/
public static String getClassNameWithSource(final int depth) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
if (stackTrace.length == 0) {
return "[no access to stack]";
} else {
return String.format("%s(%s:%d)", stackTrace[depth].getClassName(), stackTrace[depth].getFileName(), stackTrace[depth].getLineNumber());
}
}
}