diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts index 06f5719c..e26362f2 100644 --- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts +++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts @@ -13,6 +13,24 @@ import { IRunTestContext, TestKind, TestLevel, TestResultState } from '../../jav export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { + 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(); private projectName: string; @@ -25,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); @@ -51,12 +69,34 @@ 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); - this.testContext.testRun.appendOutput(line + '\r\n'); + // 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'); + } } } + /** + * Whether the given line is an Eclipse RemoteTestRunner control message. + * 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_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 cae956be..abb09d68 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\n`; const runnerContext: IRunTestContext = { isDebug: false, kind: TestKind.JUnit, @@ -89,6 +89,75 @@ 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 +%OK +%ABC +%STATUS 200 +%FAILED 1,shouldFail(junit4.TestAnnotation) +%EXPECTS +expected +%EXPECTE +%ACTUALS +actual +%ACTUALE +%TRACES +java.lang.AssertionError +at junit4.TestAnnotation.shouldFail(TestAnnotation.java:15) +%TRACEE +%TESTE 1,shouldFail(junit4.TestAnnotation) +%RUNTIME20\n`; + 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); + 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 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.notStrictEqual(echoedLines[echoedLines.length - 1], '', 'Trailing newline produced a blank output line'); + }); + test("test stacktrace should be simplified", () => { const testItem = generateTestItem(testController, 'junit@App#name', TestKind.JUnit); const testRunRequest = new TestRunRequest([testItem], []);