From 4427ebbdd6e06561b6e4ca0bdfbd45429617dd3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Havl=C3=AD=C4=8Dek?= Date: Sat, 25 Jul 2026 14:02:23 +0200 Subject: [PATCH 1/2] fix: deliver interpreter output of tests to RunTests RunTests.redirectInterpreterOutput never received anything, so whatever a test printed went to System.err instead of the stream RunTests writes to. As a consequence -compactOutput could not suppress that output either. Three independent reasons, all fixed here: - ProgramStateIO shadowed ProgramState.outStream with its own field and overrode getOutStream/setOutStream without forwarding to the native providers. Removed, the base class already implements both correctly. - ReflectionNativeProvider.setOutStream was an empty method, so OutputProvider, which implements println/BJDebugMsg/DisplayTextToPlayer and friends, kept writing to its default System.err. - The PrintStream wrapping the redirect had no autoflush, so its buffer was never emptied. It now autoflushes and uses an explicit UTF-8 charset on both the encoding and the decoding side. --- .../interpreter/ProgramStateIO.java | 10 --- .../ReflectionNativeProvider.java | 11 ++- .../providers/OutputProvider.java | 4 + .../languageserver/requests/RunTests.java | 7 +- .../tests/RunTestsOutputRedirectTests.java | 79 +++++++++++++++++++ 5 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RunTestsOutputRedirectTests.java diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java index 566d97fb1..7d3cc78ec 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java @@ -40,7 +40,6 @@ public class ProgramStateIO extends ProgramState { private final Map> createdObjectDefinitionIds = Maps.newLinkedHashMap(); private int id = 0; private final Map objDefinitions = Maps.newLinkedHashMap(); - private PrintStream outStream = System.err; private @Nullable WTS trigStrings = null; private final Optional mapFile; @@ -946,13 +945,4 @@ private Optional getObjectEditingOutputFolder() { return folder; } - @Override - public PrintStream getOutStream() { - return outStream; - } - - @Override - public void setOutStream(PrintStream os) { - outStream = os; - } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/ReflectionNativeProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/ReflectionNativeProvider.java index e25e93af5..a4cf28654 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/ReflectionNativeProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/ReflectionNativeProvider.java @@ -10,10 +10,14 @@ import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; public class ReflectionNativeProvider implements NativesProvider { private final HashMap methodMap = new HashMap<>(); + /** Providers that print to the interpreter output, so that redirecting it reaches them. */ + private final List outputProviders = new ArrayList<>(); public ReflectionNativeProvider(AbstractInterpreter interpreter) { addProvider(new AbilityProvider(interpreter)); @@ -51,6 +55,9 @@ public NativeJassFunction getFunctionPair(String funcName) { } private void addProvider(Provider provider) { + if (provider instanceof OutputProvider) { + outputProviders.add((OutputProvider) provider); + } for (Method method : provider.getClass().getMethods()) { Implements annotation = method.getAnnotation(Implements.class); if (annotation != null) { @@ -103,6 +110,8 @@ public ILconst invoke(String funcname, ILconst[] args) throws NoSuchNativeExcept @Override public void setOutStream(PrintStream outStream) { - + for (OutputProvider provider : outputProviders) { + provider.setOutStream(outStream); + } } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/OutputProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/OutputProvider.java index 4bccfaf72..083953019 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/OutputProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/OutputProvider.java @@ -18,6 +18,10 @@ public OutputProvider(AbstractInterpreter interpreter) { super(interpreter); } + public void setOutStream(PrintStream outStream) { + this.outStream = outStream; + } + public void DisplayTextToForce(IlConstHandle force, ILconstString msg) { outStream.println(msg.getVal()); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunTests.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunTests.java index 61a62d37f..14adae103 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunTests.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunTests.java @@ -29,6 +29,7 @@ import org.eclipse.lsp4j.MessageType; import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; import java.util.concurrent.*; @@ -369,13 +370,15 @@ public void write(int b) throws IOException { @Override public void write(byte[] b, int off, int len) throws IOException { if (!compactOutput) { - println(new String(b, off, len)); + println(new String(b, off, len, StandardCharsets.UTF_8)); } } }; - globalState.setOutStream(new PrintStream(os)); + // autoflush, so that the output of a test is delivered while that test is running + // instead of being left in the buffer of the PrintStream + globalState.setOutStream(new PrintStream(os, true, StandardCharsets.UTF_8)); } private String qualifiedTestName(ImFunction f) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RunTestsOutputRedirectTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RunTestsOutputRedirectTests.java new file mode 100644 index 000000000..55995800a --- /dev/null +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RunTestsOutputRedirectTests.java @@ -0,0 +1,79 @@ +package tests.wurstscript.tests; + +import de.peeeq.wurstio.WurstCompilerJassImpl; +import de.peeeq.wurstio.languageserver.requests.RunTests; +import de.peeeq.wurstscript.RunArgs; +import de.peeeq.wurstscript.ast.WurstModel; +import de.peeeq.wurstscript.gui.WurstGui; +import de.peeeq.wurstscript.gui.WurstGuiCliImpl; +import de.peeeq.wurstscript.jassIm.ImProg; +import de.peeeq.wurstscript.utils.Utils; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.Optional; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * Tests that RunTests.redirectInterpreterOutput actually captures what the tests print, + * instead of it going to System.err unnoticed. + */ +public class RunTestsOutputRedirectTests extends WurstScriptTest { + + private static final String[] PROGRAM = { + "package test", + "native testFail(string msg)", + "native println(string msg)", + "@test function passingTest()", + " println(\"output of the passing test\")", + "@test function failingTest()", + " println(\"output of the failing test\")", + " testFail(\"assertion message\")", + }; + + @Test + public void interpreterOutputIsRedirected() { + String output = runTests(false); + assertTrue(output.contains("output of the passing test"), output); + assertTrue(output.contains("output of the failing test"), output); + } + + @Test + public void interpreterOutputIsSuppressedWithCompactOutput() { + String output = runTests(true); + assertFalse(output.contains("output of the passing test"), output); + assertFalse(output.contains("output of the failing test"), output); + } + + @Test + public void testResultsAreStillReported() { + String output = runTests(false); + assertTrue(output.contains("Tests succeeded: 1/2"), output); + assertTrue(output.contains("assertion message"), output); + } + + private String runTests(boolean compactOutput) { + RunArgs runArgs = new RunArgs(); + WurstGui gui = new WurstGuiCliImpl(); + WurstCompilerJassImpl compiler = new WurstCompilerJassImpl(null, gui, null, runArgs); + WurstModel model = parseFiles(null, + Collections.singletonList(new CU("test", Utils.join(PROGRAM, "\n") + "\n")), false, compiler); + compiler.checkProg(model); + if (!gui.getErrorList().isEmpty()) { + throw gui.getErrorList().get(0); + } + ImProg imProg = compiler.translateProgToIm(model); + + StringBuilder output = new StringBuilder(); + RunTests runTests = new RunTests(Optional.empty(), 0, 0, Optional.empty(), 20, Optional.empty(), compactOutput) { + @Override + protected void print(String message) { + output.append(message); + } + }; + runTests.runTests(compiler.getImTranslator(), imProg, Optional.empty(), Optional.empty()); + return output.toString(); + } +} From 8800e20ffeaacd404fdd5b1b69f5814566a7f0f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Havl=C3=AD=C4=8Dek?= Date: Sat, 1 Aug 2026 15:33:57 +0200 Subject: [PATCH 2/2] fix: preserve interpreter output line separators --- .../de/peeeq/wurstio/languageserver/requests/RunTests.java | 2 +- .../tests/wurstscript/tests/RunTestsOutputRedirectTests.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunTests.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunTests.java index 14adae103..ce893b437 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunTests.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunTests.java @@ -370,7 +370,7 @@ public void write(int b) throws IOException { @Override public void write(byte[] b, int off, int len) throws IOException { if (!compactOutput) { - println(new String(b, off, len, StandardCharsets.UTF_8)); + print(new String(b, off, len, StandardCharsets.UTF_8)); } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RunTestsOutputRedirectTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RunTestsOutputRedirectTests.java index 55995800a..4593eb5a4 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RunTestsOutputRedirectTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RunTestsOutputRedirectTests.java @@ -36,8 +36,9 @@ public class RunTestsOutputRedirectTests extends WurstScriptTest { @Test public void interpreterOutputIsRedirected() { String output = runTests(false); - assertTrue(output.contains("output of the passing test"), output); - assertTrue(output.contains("output of the failing test"), output); + String lineSeparator = System.lineSeparator(); + assertTrue(output.contains("output of the passing test" + lineSeparator + "\tOK!"), output); + assertTrue(output.contains("output of the failing test" + lineSeparator + "\tFAILED assertion:"), output); } @Test