From ff9ce5a116a6ecd591c9a123cb1fcff44ef2327f Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Tue, 7 Jul 2026 15:33:16 +0800 Subject: [PATCH 01/12] fix: do not echo RemoteTestRunner control frames to test output JUnitRunnerResultAnalyzer.analyzeData() forwarded every line received from the Eclipse RemoteTestRunner socket to the Test Results output channel, including the machine-readable control frames (%TSTTREE, %TESTS, %TESTE, %FAILED, %TRACES, %TESTC, %RUNTIME, ...). These frames are already consumed by processData() to build the test tree and failure messages, so echoing them verbatim produced noisy, non CLI-parity output. Only forward lines that are not Eclipse control frames (identified by a leading '%' followed by an upper-case message id). Genuine program output and stack-trace content are still forwarded. Also covers control markers that are emitted by the runner but not modelled in the MessageId enum. --- .../junitRunner/JUnitRunnerResultAnalyzer.ts | 17 ++++++++- test/suite/JUnitAnalyzer.test.ts | 38 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts index 06f5719c..3fc03062 100644 --- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts +++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts @@ -53,10 +53,25 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { const lines: string[] = data.split(/\r?\n/); for (const line of lines) { this.processData(line); - this.testContext.testRun.appendOutput(line + '\r\n'); + // Only forward genuine program output to the Test Results output channel. + // Lines that start with an Eclipse RemoteTestRunner control marker (e.g. %TSTTREE, + // %TESTS, %TRACES, %TESTC, %RUNTIME) are protocol frames already consumed by + // processData(); echoing them verbatim shows up as noise in the output. + if (!this.isControlMessage(line)) { + this.testContext.testRun.appendOutput(line + '\r\n'); + } } } + /** + * Whether the given line is an Eclipse RemoteTestRunner control message. + * All control messages start with '%' followed by an upper-case message id + * (e.g. %TSTTREE, %TESTS, %TESTE, %FAILED, %TRACES, %TESTC, %RUNTIME). + */ + private isControlMessage(line: string): boolean { + return /^%[A-Z]/.test(line); + } + public processData(data: string): void { if (data.startsWith(MessageId.TestTree)) { this.enlistToTestMapping(data.substring(MessageId.TestTree.length).trim()); diff --git a/test/suite/JUnitAnalyzer.test.ts b/test/suite/JUnitAnalyzer.test.ts index cae956be..1b189d4e 100644 --- a/test/suite/JUnitAnalyzer.test.ts +++ b/test/suite/JUnitAnalyzer.test.ts @@ -89,6 +89,44 @@ at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) assert.ok((testMessage.message as MarkdownString).value.includes('junit4.TestAnnotation.shouldFail([TestAnnotation.java:15](command:_java.test.openStackTrace?%5B%22at%20junit4.TestAnnotation.shouldFail(TestAnnotation.java%3A15)%22%2C%22junit%22%5D))')); }); + test("control protocol frames should not be echoed to the output", () => { + const testItem = generateTestItem(testController, 'junit@junit4.TestAnnotation#shouldFail', TestKind.JUnit); + const testRunRequest = new TestRunRequest([testItem], []); + const testRun = testController.createTestRun(testRunRequest); + const appendOutputSpy = sinon.spy(testRun, 'appendOutput'); + const testRunnerOutput = `%TESTC 1 v2 +%TSTTREE1,shouldFail(junit4.TestAnnotation),false,1,false,-1,shouldFail(junit4.TestAnnotation),, +%TESTS 1,shouldFail(junit4.TestAnnotation) +Hello from System.out +%FAILED 1,shouldFail(junit4.TestAnnotation) +%TRACES +java.lang.AssertionError +at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) +%TRACEE +%TESTE 1,shouldFail(junit4.TestAnnotation) +%RUNTIME20;`; + const runnerContext: IRunTestContext = { + isDebug: false, + kind: TestKind.JUnit, + projectName: 'junit', + testItems: [testItem], + testRun: testRun, + workspaceFolder: workspace.workspaceFolders?.[0]!, + }; + + const analyzer = new JUnitRunnerResultAnalyzer(runnerContext); + analyzer.analyzeData(testRunnerOutput); + + const echoed: string[] = appendOutputSpy.getCalls().map((call) => call.args[0] as string); + // No Eclipse RemoteTestRunner control frame should reach the output channel. + for (const output of echoed) { + assert.ok(!/^%[A-Z]/.test(output), `Control frame leaked to output: ${output}`); + } + // Genuine program output and stack trace content should still be forwarded. + assert.ok(echoed.some((output) => output.includes('Hello from System.out')), 'Program output was dropped'); + assert.ok(echoed.some((output) => output.includes('java.lang.AssertionError')), 'Stack trace content was dropped'); + }); + test("test stacktrace should be simplified", () => { const testItem = generateTestItem(testController, 'junit@App#name', TestKind.JUnit); const testRunRequest = new TestRunRequest([testItem], []); From 910983bacbedd2c8c37d811b42854900e26e9f20 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 8 Jul 2026 11:14:05 +0800 Subject: [PATCH 02/12] fix: tighten RemoteTestRunner frame filtering --- .../junitRunner/JUnitRunnerResultAnalyzer.ts | 13 ++++++------- test/suite/JUnitAnalyzer.test.ts | 6 +++++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts index 3fc03062..e244cfc3 100644 --- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts +++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts @@ -53,10 +53,8 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { const lines: string[] = data.split(/\r?\n/); for (const line of lines) { this.processData(line); - // Only forward genuine program output to the Test Results output channel. - // Lines that start with an Eclipse RemoteTestRunner control marker (e.g. %TSTTREE, - // %TESTS, %TRACES, %TESTC, %RUNTIME) are protocol frames already consumed by - // processData(); echoing them verbatim shows up as noise in the output. + // Hide Eclipse RemoteTestRunner protocol frames already consumed by processData(). + // Forward everything else, including stdout/stderr and trace payload lines. if (!this.isControlMessage(line)) { this.testContext.testRun.appendOutput(line + '\r\n'); } @@ -65,11 +63,12 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { /** * Whether the given line is an Eclipse RemoteTestRunner control message. - * All control messages start with '%' followed by an upper-case message id - * (e.g. %TSTTREE, %TESTS, %TESTE, %FAILED, %TRACES, %TESTC, %RUNTIME). + * Control messages start with '%' followed by an upper-case message id, + * an optional index, and a protocol separator (whitespace, comma, or semicolon). + * This avoids dropping legitimate output such as "%OK". */ private isControlMessage(line: string): boolean { - return /^%[A-Z]/.test(line); + return /^%[A-Z]+(?:\d+)?(?:\s|,|;)/.test(line); } public processData(data: string): void { diff --git a/test/suite/JUnitAnalyzer.test.ts b/test/suite/JUnitAnalyzer.test.ts index 1b189d4e..537a396c 100644 --- a/test/suite/JUnitAnalyzer.test.ts +++ b/test/suite/JUnitAnalyzer.test.ts @@ -98,6 +98,8 @@ at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) %TSTTREE1,shouldFail(junit4.TestAnnotation),false,1,false,-1,shouldFail(junit4.TestAnnotation),, %TESTS 1,shouldFail(junit4.TestAnnotation) Hello from System.out +%OK +%ABC %FAILED 1,shouldFail(junit4.TestAnnotation) %TRACES java.lang.AssertionError @@ -120,10 +122,12 @@ at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) const echoed: string[] = appendOutputSpy.getCalls().map((call) => call.args[0] as string); // No Eclipse RemoteTestRunner control frame should reach the output channel. for (const output of echoed) { - assert.ok(!/^%[A-Z]/.test(output), `Control frame leaked to output: ${output}`); + assert.ok(!/^%[A-Z]+(?:\d+)?(?:\s|,|;)/.test(output), `Control frame leaked to output: ${output}`); } // Genuine program output and stack trace content should still be forwarded. assert.ok(echoed.some((output) => output.includes('Hello from System.out')), 'Program output was dropped'); + assert.ok(echoed.some((output) => output.includes('%OK')), 'Percent-prefixed program output was dropped'); + assert.ok(echoed.some((output) => output.includes('%ABC')), 'Percent-prefixed program output was dropped'); assert.ok(echoed.some((output) => output.includes('java.lang.AssertionError')), 'Stack trace content was dropped'); }); From a7894cc35506e6da83a0facc37772a1b64fb7840 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 8 Jul 2026 11:25:24 +0800 Subject: [PATCH 03/12] test: cover numeric RemoteTestRunner frames --- src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts | 5 +++-- test/suite/JUnitAnalyzer.test.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts index e244cfc3..08aa22dc 100644 --- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts +++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts @@ -64,11 +64,12 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { /** * Whether the given line is an Eclipse RemoteTestRunner control message. * Control messages start with '%' followed by an upper-case message id, - * an optional index, and a protocol separator (whitespace, comma, or semicolon). + * an optional index, and either a protocol separator (whitespace, comma, or semicolon) + * or the end of the line for numeric-suffixed frames such as "%RUNTIME15". * This avoids dropping legitimate output such as "%OK". */ private isControlMessage(line: string): boolean { - return /^%[A-Z]+(?:\d+)?(?:\s|,|;)/.test(line); + return /^%[A-Z]+(?:\d+(?:$|\s|,|;)|(?:\s|,|;))/.test(line); } public processData(data: string): void { diff --git a/test/suite/JUnitAnalyzer.test.ts b/test/suite/JUnitAnalyzer.test.ts index 537a396c..32b6498f 100644 --- a/test/suite/JUnitAnalyzer.test.ts +++ b/test/suite/JUnitAnalyzer.test.ts @@ -70,7 +70,7 @@ at org.junit.Assert.assertTrue(Assert.java:53) at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) %TRACEE %TESTE 1,shouldFail(junit4.TestAnnotation) -%RUNTIME20;`; +%RUNTIME20`; const runnerContext: IRunTestContext = { isDebug: false, kind: TestKind.JUnit, @@ -122,7 +122,8 @@ at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) const echoed: string[] = appendOutputSpy.getCalls().map((call) => call.args[0] as string); // No Eclipse RemoteTestRunner control frame should reach the output channel. for (const output of echoed) { - assert.ok(!/^%[A-Z]+(?:\d+)?(?:\s|,|;)/.test(output), `Control frame leaked to output: ${output}`); + const outputLine = output.replace(/\r?\n$/, ''); + assert.ok(!/^%[A-Z]+(?:\d+(?:$|\s|,|;)|(?:\s|,|;))/.test(outputLine), `Control frame leaked to output: ${output}`); } // Genuine program output and stack trace content should still be forwarded. assert.ok(echoed.some((output) => output.includes('Hello from System.out')), 'Program output was dropped'); From 66b08b8c17e0ae6b1e7bb7e47fe4de2fa39cba4e Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 8 Jul 2026 11:33:28 +0800 Subject: [PATCH 04/12] test: use numeric runtime frame fixture --- test/suite/JUnitAnalyzer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/suite/JUnitAnalyzer.test.ts b/test/suite/JUnitAnalyzer.test.ts index 32b6498f..f6bb3dee 100644 --- a/test/suite/JUnitAnalyzer.test.ts +++ b/test/suite/JUnitAnalyzer.test.ts @@ -106,7 +106,7 @@ java.lang.AssertionError at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) %TRACEE %TESTE 1,shouldFail(junit4.TestAnnotation) -%RUNTIME20;`; +%RUNTIME20`; const runnerContext: IRunTestContext = { isDebug: false, kind: TestKind.JUnit, From 02a03717846c0c9ec99a9470a6a62265091a1366 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 8 Jul 2026 12:15:13 +0800 Subject: [PATCH 05/12] fix: filter bare RemoteTestRunner frames --- .../junitRunner/JUnitRunnerResultAnalyzer.ts | 13 ++++++++++++- test/suite/JUnitAnalyzer.test.ts | 13 ++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts index 08aa22dc..8172edfa 100644 --- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts +++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts @@ -66,10 +66,21 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { * Control messages start with '%' followed by an upper-case message id, * an optional index, and either a protocol separator (whitespace, comma, or semicolon) * or the end of the line for numeric-suffixed frames such as "%RUNTIME15". + * Some payload delimiters, such as "%TRACES", are bare control frames with no suffix. * This avoids dropping legitimate output such as "%OK". */ private isControlMessage(line: string): boolean { - return /^%[A-Z]+(?:\d+(?:$|\s|,|;)|(?:\s|,|;))/.test(line); + return /^%[A-Z]+(?:\d+(?:$|\s|,|;)|(?:\s|,|;))/.test(line) + || this.isBareControlMessage(line); + } + + private isBareControlMessage(line: string): boolean { + return line === MessageId.ExpectStart + || line === MessageId.ExpectEnd + || line === MessageId.ActualStart + || line === MessageId.ActualEnd + || line === MessageId.TraceStart + || line === MessageId.TraceEnd; } public processData(data: string): void { diff --git a/test/suite/JUnitAnalyzer.test.ts b/test/suite/JUnitAnalyzer.test.ts index f6bb3dee..58f24b9a 100644 --- a/test/suite/JUnitAnalyzer.test.ts +++ b/test/suite/JUnitAnalyzer.test.ts @@ -101,10 +101,16 @@ Hello from System.out %OK %ABC %FAILED 1,shouldFail(junit4.TestAnnotation) -%TRACES +%EXPECTS +expected +%EXPECTE +%ACTUALS +actual +%ACTUALE +%TRACES java.lang.AssertionError at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) -%TRACEE +%TRACEE %TESTE 1,shouldFail(junit4.TestAnnotation) %RUNTIME20`; const runnerContext: IRunTestContext = { @@ -121,9 +127,10 @@ at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) const echoed: string[] = appendOutputSpy.getCalls().map((call) => call.args[0] as string); // No Eclipse RemoteTestRunner control frame should reach the output channel. + const controlFrameRegExp = /^%[A-Z]+(?:\d+(?:$|\s|,|;)|(?:\s|,|;))$|^%(?:TRACES|TRACEE|EXPECTS|EXPECTE|ACTUALS|ACTUALE)$/; for (const output of echoed) { const outputLine = output.replace(/\r?\n$/, ''); - assert.ok(!/^%[A-Z]+(?:\d+(?:$|\s|,|;)|(?:\s|,|;))/.test(outputLine), `Control frame leaked to output: ${output}`); + assert.ok(!controlFrameRegExp.test(outputLine), `Control frame leaked to output: ${output}`); } // Genuine program output and stack trace content should still be forwarded. assert.ok(echoed.some((output) => output.includes('Hello from System.out')), 'Program output was dropped'); From 959ce79c02e837105c77210d4bf0b9eb9eab255d Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 8 Jul 2026 12:47:42 +0800 Subject: [PATCH 06/12] refactor: clarify RemoteTestRunner frame checks --- .../junitRunner/JUnitRunnerResultAnalyzer.ts | 6 +++- test/suite/JUnitAnalyzer.test.ts | 31 +++++++++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts index 8172edfa..1e782c76 100644 --- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts +++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts @@ -13,6 +13,9 @@ import { IRunTestContext, TestKind, TestLevel, TestResultState } from '../../jav export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { + private static readonly CONTROL_FRAME_PREFIX: RegExp = /^%[A-Z]+(?:\d+)?(?:\s|,|;)/; + private static readonly NUMERIC_CONTROL_FRAME: RegExp = /^%[A-Z]+\d+$/; + private testOutputMapping: Map = new Map(); private triggeredTestsMapping: Map = new Map(); private projectName: string; @@ -70,7 +73,8 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { * This avoids dropping legitimate output such as "%OK". */ private isControlMessage(line: string): boolean { - return /^%[A-Z]+(?:\d+(?:$|\s|,|;)|(?:\s|,|;))/.test(line) + return JUnitRunnerResultAnalyzer.CONTROL_FRAME_PREFIX.test(line) + || JUnitRunnerResultAnalyzer.NUMERIC_CONTROL_FRAME.test(line) || this.isBareControlMessage(line); } diff --git a/test/suite/JUnitAnalyzer.test.ts b/test/suite/JUnitAnalyzer.test.ts index 58f24b9a..715554d6 100644 --- a/test/suite/JUnitAnalyzer.test.ts +++ b/test/suite/JUnitAnalyzer.test.ts @@ -126,17 +126,30 @@ at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) analyzer.analyzeData(testRunnerOutput); const echoed: string[] = appendOutputSpy.getCalls().map((call) => call.args[0] as string); - // No Eclipse RemoteTestRunner control frame should reach the output channel. - const controlFrameRegExp = /^%[A-Z]+(?:\d+(?:$|\s|,|;)|(?:\s|,|;))$|^%(?:TRACES|TRACEE|EXPECTS|EXPECTE|ACTUALS|ACTUALE)$/; - for (const output of echoed) { - const outputLine = output.replace(/\r?\n$/, ''); - assert.ok(!controlFrameRegExp.test(outputLine), `Control frame leaked to output: ${output}`); + const echoedLines: string[] = echoed.map((output) => output.replace(/\r?\n$/, '')); + + const controlFrames: string[] = [ + '%TESTC 1 v2', + '%TSTTREE1,shouldFail(junit4.TestAnnotation),false,1,false,-1,shouldFail(junit4.TestAnnotation),,', + '%TESTS 1,shouldFail(junit4.TestAnnotation)', + '%FAILED 1,shouldFail(junit4.TestAnnotation)', + '%EXPECTS', + '%EXPECTE', + '%ACTUALS', + '%ACTUALE', + '%TRACES', + '%TRACEE', + '%TESTE 1,shouldFail(junit4.TestAnnotation)', + '%RUNTIME20', + ]; + for (const frame of controlFrames) { + assert.ok(!echoedLines.includes(frame), `Control frame leaked to output: ${frame}`); } // Genuine program output and stack trace content should still be forwarded. - assert.ok(echoed.some((output) => output.includes('Hello from System.out')), 'Program output was dropped'); - assert.ok(echoed.some((output) => output.includes('%OK')), 'Percent-prefixed program output was dropped'); - assert.ok(echoed.some((output) => output.includes('%ABC')), 'Percent-prefixed program output was dropped'); - assert.ok(echoed.some((output) => output.includes('java.lang.AssertionError')), 'Stack trace content was dropped'); + assert.ok(echoedLines.includes('Hello from System.out'), 'Program output was dropped'); + assert.ok(echoedLines.includes('%OK'), 'Percent-prefixed program output was dropped'); + assert.ok(echoedLines.includes('%ABC'), 'Percent-prefixed program output was dropped'); + assert.ok(echoedLines.includes('java.lang.AssertionError'), 'Stack trace content was dropped'); }); test("test stacktrace should be simplified", () => { From 03259d95046f388f7e02b84b0b9ea4bec75cef67 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 8 Jul 2026 13:08:28 +0800 Subject: [PATCH 07/12] fix: skip trailing empty runner output --- src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts | 7 ++++++- test/suite/JUnitAnalyzer.test.ts | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts index 1e782c76..b7661f5f 100644 --- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts +++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts @@ -54,7 +54,12 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { public analyzeData(data: string): void { const lines: string[] = data.split(/\r?\n/); - for (const line of lines) { + const endsWithNewLine: boolean = /\r?\n$/.test(data); + for (let i: number = 0; i < lines.length; i++) { + const line: string = lines[i]; + if (i === lines.length - 1 && endsWithNewLine && line === '') { + continue; + } this.processData(line); // Hide Eclipse RemoteTestRunner protocol frames already consumed by processData(). // Forward everything else, including stdout/stderr and trace payload lines. diff --git a/test/suite/JUnitAnalyzer.test.ts b/test/suite/JUnitAnalyzer.test.ts index 715554d6..0ab9297a 100644 --- a/test/suite/JUnitAnalyzer.test.ts +++ b/test/suite/JUnitAnalyzer.test.ts @@ -70,7 +70,8 @@ at org.junit.Assert.assertTrue(Assert.java:53) at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) %TRACEE %TESTE 1,shouldFail(junit4.TestAnnotation) -%RUNTIME20`; +%RUNTIME20 +`; const runnerContext: IRunTestContext = { isDebug: false, kind: TestKind.JUnit, @@ -150,6 +151,7 @@ at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) assert.ok(echoedLines.includes('%OK'), 'Percent-prefixed program output was dropped'); assert.ok(echoedLines.includes('%ABC'), 'Percent-prefixed program output was dropped'); assert.ok(echoedLines.includes('java.lang.AssertionError'), 'Stack trace content was dropped'); + assert.ok(!echoedLines.includes(''), 'Trailing newline produced a blank output line'); }); test("test stacktrace should be simplified", () => { From 669262eb0aedbb6a78528f6f891981da73297c0f Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 8 Jul 2026 13:17:31 +0800 Subject: [PATCH 08/12] fix: use known RemoteTestRunner frame prefixes --- .../junitRunner/JUnitRunnerResultAnalyzer.ts | 39 ++++++++++--------- test/suite/JUnitAnalyzer.test.ts | 2 + 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts index b7661f5f..4aa1f62a 100644 --- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts +++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts @@ -13,8 +13,23 @@ import { IRunTestContext, TestKind, TestLevel, TestResultState } from '../../jav export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { - private static readonly CONTROL_FRAME_PREFIX: RegExp = /^%[A-Z]+(?:\d+)?(?:\s|,|;)/; - private static readonly NUMERIC_CONTROL_FRAME: RegExp = /^%[A-Z]+\d+$/; + private static readonly CONTROL_MESSAGE_PREFIXES: string[] = [ + '%TSTTREE', + '%TESTS', + '%TESTE', + '%FAILED', + '%ERROR', + '%EXPECTS', + '%EXPECTE', + '%ACTUALS', + '%ACTUALE', + '%TRACES', + '%TRACEE', + '%TESTC', + '%RUNTIME', + '%MENTER', + '%MEXIT', + ]; private testOutputMapping: Map = new Map(); private triggeredTestsMapping: Map = new Map(); @@ -71,25 +86,11 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { /** * Whether the given line is an Eclipse RemoteTestRunner control message. - * Control messages start with '%' followed by an upper-case message id, - * an optional index, and either a protocol separator (whitespace, comma, or semicolon) - * or the end of the line for numeric-suffixed frames such as "%RUNTIME15". - * Some payload delimiters, such as "%TRACES", are bare control frames with no suffix. - * This avoids dropping legitimate output such as "%OK". + * Only known protocol frame prefixes are filtered so user output that merely starts + * with '%' (for example, "%OK" or "%STATUS 200") remains visible. */ private isControlMessage(line: string): boolean { - return JUnitRunnerResultAnalyzer.CONTROL_FRAME_PREFIX.test(line) - || JUnitRunnerResultAnalyzer.NUMERIC_CONTROL_FRAME.test(line) - || this.isBareControlMessage(line); - } - - private isBareControlMessage(line: string): boolean { - return line === MessageId.ExpectStart - || line === MessageId.ExpectEnd - || line === MessageId.ActualStart - || line === MessageId.ActualEnd - || line === MessageId.TraceStart - || line === MessageId.TraceEnd; + return JUnitRunnerResultAnalyzer.CONTROL_MESSAGE_PREFIXES.some((prefix: string) => line.startsWith(prefix)); } public processData(data: string): void { diff --git a/test/suite/JUnitAnalyzer.test.ts b/test/suite/JUnitAnalyzer.test.ts index 0ab9297a..58e3b28d 100644 --- a/test/suite/JUnitAnalyzer.test.ts +++ b/test/suite/JUnitAnalyzer.test.ts @@ -101,6 +101,7 @@ at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) Hello from System.out %OK %ABC +%STATUS 200 %FAILED 1,shouldFail(junit4.TestAnnotation) %EXPECTS expected @@ -150,6 +151,7 @@ at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) assert.ok(echoedLines.includes('Hello from System.out'), 'Program output was dropped'); assert.ok(echoedLines.includes('%OK'), 'Percent-prefixed program output was dropped'); assert.ok(echoedLines.includes('%ABC'), 'Percent-prefixed program output was dropped'); + assert.ok(echoedLines.includes('%STATUS 200'), 'Percent-prefixed program output was dropped'); assert.ok(echoedLines.includes('java.lang.AssertionError'), 'Stack trace content was dropped'); assert.ok(!echoedLines.includes(''), 'Trailing newline produced a blank output line'); }); From 8e1063c0f324ce07852004b271222717c0bc053e Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 8 Jul 2026 14:17:32 +0800 Subject: [PATCH 09/12] test: make trailing newline fixtures explicit --- test/suite/JUnitAnalyzer.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/suite/JUnitAnalyzer.test.ts b/test/suite/JUnitAnalyzer.test.ts index 58e3b28d..71214f64 100644 --- a/test/suite/JUnitAnalyzer.test.ts +++ b/test/suite/JUnitAnalyzer.test.ts @@ -70,8 +70,7 @@ at org.junit.Assert.assertTrue(Assert.java:53) at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) %TRACEE %TESTE 1,shouldFail(junit4.TestAnnotation) -%RUNTIME20 -`; +%RUNTIME20\n`; const runnerContext: IRunTestContext = { isDebug: false, kind: TestKind.JUnit, @@ -114,7 +113,7 @@ java.lang.AssertionError at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) %TRACEE %TESTE 1,shouldFail(junit4.TestAnnotation) -%RUNTIME20`; +%RUNTIME20\n`; const runnerContext: IRunTestContext = { isDebug: false, kind: TestKind.JUnit, @@ -153,7 +152,7 @@ at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) assert.ok(echoedLines.includes('%ABC'), 'Percent-prefixed program output was dropped'); assert.ok(echoedLines.includes('%STATUS 200'), 'Percent-prefixed program output was dropped'); assert.ok(echoedLines.includes('java.lang.AssertionError'), 'Stack trace content was dropped'); - assert.ok(!echoedLines.includes(''), 'Trailing newline produced a blank output line'); + assert.notStrictEqual(echoedLines[echoedLines.length - 1], '', 'Trailing newline produced a blank output line'); }); test("test stacktrace should be simplified", () => { From adf5b70c5f31edf5cc7882de4e67dde4ad6257b7 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 8 Jul 2026 15:11:04 +0800 Subject: [PATCH 10/12] fix: filter RemoteTestRunner payload blocks --- .../junitRunner/JUnitRunnerResultAnalyzer.ts | 16 ++++++++++------ test/suite/JUnitAnalyzer.test.ts | 7 +++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts index 4aa1f62a..ba30757b 100644 --- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts +++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts @@ -43,9 +43,9 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { private tracingItem: TestItem | undefined; private traces: MarkdownString; private assertionFailure: TestMessage | undefined; - private recordingType: RecordingType; - private expectString: string; - private actualString: string; + private recordingType: RecordingType = RecordingType.None; + private expectString: string = ''; + private actualString: string = ''; constructor(protected testContext: IRunTestContext) { super(testContext); @@ -76,9 +76,9 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { continue; } this.processData(line); - // Hide Eclipse RemoteTestRunner protocol frames already consumed by processData(). - // Forward everything else, including stdout/stderr and trace payload lines. - if (!this.isControlMessage(line)) { + // Hide Eclipse RemoteTestRunner protocol frames/payload already consumed by processData(). + // Forward everything else, including stdout/stderr. + if (!this.isControlMessage(line) && !this.isRecordingProtocolPayload()) { this.testContext.testRun.appendOutput(line + '\r\n'); } } @@ -93,6 +93,10 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { return JUnitRunnerResultAnalyzer.CONTROL_MESSAGE_PREFIXES.some((prefix: string) => line.startsWith(prefix)); } + private isRecordingProtocolPayload(): boolean { + return this.recordingType !== RecordingType.None; + } + public processData(data: string): void { if (data.startsWith(MessageId.TestTree)) { this.enlistToTestMapping(data.substring(MessageId.TestTree.length).trim()); diff --git a/test/suite/JUnitAnalyzer.test.ts b/test/suite/JUnitAnalyzer.test.ts index 71214f64..abb09d68 100644 --- a/test/suite/JUnitAnalyzer.test.ts +++ b/test/suite/JUnitAnalyzer.test.ts @@ -146,12 +146,15 @@ at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) for (const frame of controlFrames) { assert.ok(!echoedLines.includes(frame), `Control frame leaked to output: ${frame}`); } - // Genuine program output and stack trace content should still be forwarded. + // Genuine program output should still be forwarded. assert.ok(echoedLines.includes('Hello from System.out'), 'Program output was dropped'); assert.ok(echoedLines.includes('%OK'), 'Percent-prefixed program output was dropped'); assert.ok(echoedLines.includes('%ABC'), 'Percent-prefixed program output was dropped'); assert.ok(echoedLines.includes('%STATUS 200'), 'Percent-prefixed program output was dropped'); - assert.ok(echoedLines.includes('java.lang.AssertionError'), 'Stack trace content was dropped'); + assert.ok(!echoedLines.includes('expected'), 'Expected-value protocol payload leaked to output'); + assert.ok(!echoedLines.includes('actual'), 'Actual-value protocol payload leaked to output'); + assert.ok(!echoedLines.includes('java.lang.AssertionError'), 'Trace protocol payload leaked to output'); + assert.ok(!echoedLines.includes('at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15)'), 'Trace protocol payload leaked to output'); assert.notStrictEqual(echoedLines[echoedLines.length - 1], '', 'Trailing newline produced a blank output line'); }); From 0eeae8ae09f27c1363112a6420e042ee570a7c5a Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 8 Jul 2026 15:28:41 +0800 Subject: [PATCH 11/12] fix: keep stack trace output visible --- src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts | 11 ++++++----- test/suite/JUnitAnalyzer.test.ts | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts index ba30757b..e667c788 100644 --- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts +++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts @@ -76,9 +76,9 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { continue; } this.processData(line); - // Hide Eclipse RemoteTestRunner protocol frames/payload already consumed by processData(). - // Forward everything else, including stdout/stderr. - if (!this.isControlMessage(line) && !this.isRecordingProtocolPayload()) { + // Hide Eclipse RemoteTestRunner protocol frames and comparison payload already consumed by processData(). + // Forward everything else, including stdout/stderr and stack trace payload lines. + if (!this.isControlMessage(line) && !this.isRecordingComparisonPayload()) { this.testContext.testRun.appendOutput(line + '\r\n'); } } @@ -93,8 +93,9 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { return JUnitRunnerResultAnalyzer.CONTROL_MESSAGE_PREFIXES.some((prefix: string) => line.startsWith(prefix)); } - private isRecordingProtocolPayload(): boolean { - return this.recordingType !== RecordingType.None; + private isRecordingComparisonPayload(): boolean { + return this.recordingType === RecordingType.ExpectMessage + || this.recordingType === RecordingType.ActualMessage; } public processData(data: string): void { diff --git a/test/suite/JUnitAnalyzer.test.ts b/test/suite/JUnitAnalyzer.test.ts index abb09d68..ace0a311 100644 --- a/test/suite/JUnitAnalyzer.test.ts +++ b/test/suite/JUnitAnalyzer.test.ts @@ -146,15 +146,15 @@ at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) for (const frame of controlFrames) { assert.ok(!echoedLines.includes(frame), `Control frame leaked to output: ${frame}`); } - // Genuine program output should still be forwarded. + // Genuine program output and stack trace content should still be forwarded. assert.ok(echoedLines.includes('Hello from System.out'), 'Program output was dropped'); assert.ok(echoedLines.includes('%OK'), 'Percent-prefixed program output was dropped'); assert.ok(echoedLines.includes('%ABC'), 'Percent-prefixed program output was dropped'); assert.ok(echoedLines.includes('%STATUS 200'), 'Percent-prefixed program output was dropped'); assert.ok(!echoedLines.includes('expected'), 'Expected-value protocol payload leaked to output'); assert.ok(!echoedLines.includes('actual'), 'Actual-value protocol payload leaked to output'); - assert.ok(!echoedLines.includes('java.lang.AssertionError'), 'Trace protocol payload leaked to output'); - assert.ok(!echoedLines.includes('at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15)'), 'Trace protocol payload leaked to output'); + assert.ok(echoedLines.includes('java.lang.AssertionError'), 'Stack trace content was dropped'); + assert.ok(echoedLines.includes('at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15)'), 'Stack trace content was dropped'); assert.notStrictEqual(echoedLines[echoedLines.length - 1], '', 'Trailing newline produced a blank output line'); }); From 7e72179505480b448261381f329247578570591d Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 8 Jul 2026 15:47:58 +0800 Subject: [PATCH 12/12] fix: filter RemoteTestRunner protocol payload --- src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts | 11 +++++------ test/suite/JUnitAnalyzer.test.ts | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts index e667c788..e26362f2 100644 --- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts +++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts @@ -76,9 +76,9 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { continue; } this.processData(line); - // Hide Eclipse RemoteTestRunner protocol frames and comparison payload already consumed by processData(). - // Forward everything else, including stdout/stderr and stack trace payload lines. - if (!this.isControlMessage(line) && !this.isRecordingComparisonPayload()) { + // Hide Eclipse RemoteTestRunner protocol frames/payload already consumed by processData(). + // Forward everything else, including stdout/stderr if present in the runner stream. + if (!this.isControlMessage(line) && !this.isRecordingProtocolPayload()) { this.testContext.testRun.appendOutput(line + '\r\n'); } } @@ -93,9 +93,8 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { return JUnitRunnerResultAnalyzer.CONTROL_MESSAGE_PREFIXES.some((prefix: string) => line.startsWith(prefix)); } - private isRecordingComparisonPayload(): boolean { - return this.recordingType === RecordingType.ExpectMessage - || this.recordingType === RecordingType.ActualMessage; + private isRecordingProtocolPayload(): boolean { + return this.recordingType !== RecordingType.None; } public processData(data: string): void { diff --git a/test/suite/JUnitAnalyzer.test.ts b/test/suite/JUnitAnalyzer.test.ts index ace0a311..abb09d68 100644 --- a/test/suite/JUnitAnalyzer.test.ts +++ b/test/suite/JUnitAnalyzer.test.ts @@ -146,15 +146,15 @@ at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) for (const frame of controlFrames) { assert.ok(!echoedLines.includes(frame), `Control frame leaked to output: ${frame}`); } - // Genuine program output and stack trace content should still be forwarded. + // Genuine program output should still be forwarded. assert.ok(echoedLines.includes('Hello from System.out'), 'Program output was dropped'); assert.ok(echoedLines.includes('%OK'), 'Percent-prefixed program output was dropped'); assert.ok(echoedLines.includes('%ABC'), 'Percent-prefixed program output was dropped'); assert.ok(echoedLines.includes('%STATUS 200'), 'Percent-prefixed program output was dropped'); assert.ok(!echoedLines.includes('expected'), 'Expected-value protocol payload leaked to output'); assert.ok(!echoedLines.includes('actual'), 'Actual-value protocol payload leaked to output'); - assert.ok(echoedLines.includes('java.lang.AssertionError'), 'Stack trace content was dropped'); - assert.ok(echoedLines.includes('at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15)'), 'Stack trace content was dropped'); + assert.ok(!echoedLines.includes('java.lang.AssertionError'), 'Trace protocol payload leaked to output'); + assert.ok(!echoedLines.includes('at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15)'), 'Trace protocol payload leaked to output'); assert.notStrictEqual(echoedLines[echoedLines.length - 1], '', 'Trailing newline produced a blank output line'); });