Skip to content

Commit 211ed2f

Browse files
committed
feat: route real test program output to Test Results
The debuggee's stdout/stderr are delivered by java-debug as standard DAP `output` events, which by default only surface in the Debug Console. This splits program output (Debug Console) from test results (Test Results view) across two separate surfaces, and the runner previously echoed the raw JUnit/TestNG control protocol frames to Test Results as noise. - Attach a DebugAdapterTracker to the test's own debug session and forward its DAP `output` events into the Test Results view (BaseRunner). - Stop echoing the socket control protocol to Test Results: JUnit no longer appends every raw line, and TestNG no longer echoes its JSON frames. - Attribute forwarded output to the running test when exactly one test is executing; fall back to run-level output when idle or when several tests run in parallel (attribution would only be a guess).
1 parent 56feff7 commit 211ed2f

4 files changed

Lines changed: 73 additions & 13 deletions

File tree

src/runners/baseRunner/BaseRunner.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import * as iconv from 'iconv-lite';
55
import { AddressInfo, createServer, Server, Socket } from 'net';
66
import * as os from 'os';
7-
import { CancellationToken, debug, DebugConfiguration, DebugSession, Disposable } from 'vscode';
7+
import { CancellationToken, debug, DebugAdapterTracker, DebugConfiguration, DebugSession, Disposable, ProviderResult } from 'vscode';
88
import { sendError } from 'vscode-extension-telemetry-wrapper';
99
import { Configurations } from '../../constants';
1010
import { IProgressReporter } from '../../debugger.api';
@@ -56,6 +56,34 @@ export abstract class BaseRunner implements ITestRunnerInternal {
5656
// So we force to use internal console here to make sure the session is still under debugger's control.
5757
launchConfiguration.console = 'internalConsole';
5858

59+
// The debuggee's stdout/stderr are delivered by java-debug as standard DAP `output` events,
60+
// which by default only surface in the Debug Console. Attach a tracker to the test's own debug
61+
// session and forward those output events into the Test Results view, so the program output shows
62+
// up next to the test results instead of being split across two separate surfaces.
63+
this.disposables.push(debug.registerDebugAdapterTrackerFactory('java', {
64+
createDebugAdapterTracker: (session: DebugSession): ProviderResult<DebugAdapterTracker> => {
65+
if (session.name !== launchConfiguration.name) {
66+
return undefined;
67+
}
68+
return {
69+
onDidSendMessage: (message: any): void => {
70+
if (message?.type === 'event' && message.event === 'output') {
71+
const category: string | undefined = message.body?.category;
72+
// `telemetry` output events are not meant for the user.
73+
if (category === 'telemetry') {
74+
return;
75+
}
76+
const output: string | undefined = message.body?.output;
77+
if (output) {
78+
// Let the analyzer attribute the output to the running test when possible.
79+
this.runnerResultAnalyzer.appendProgramOutput(output);
80+
}
81+
}
82+
},
83+
};
84+
},
85+
}));
86+
5987
let debugSession: DebugSession | undefined;
6088
this.disposables.push(debug.onDidStartDebugSession((session: DebugSession) => {
6189
if (session.name === launchConfiguration.name) {

src/runners/baseRunner/RunnerResultAnalyzer.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,45 @@ export abstract class RunnerResultAnalyzer {
1010
// Track parent test item states to update them when all children complete
1111
protected parentStates: Map<TestItem, ParentItemState> = new Map();
1212

13+
// Test items that are currently executing. Used to attribute program output
14+
// (captured from the debug session) to the running test when it is unambiguous.
15+
private runningItems: Set<TestItem> = new Set();
16+
1317
constructor(protected testContext: IRunTestContext) { }
1418

1519
public abstract analyzeData(data: string): void;
1620
public abstract processData(data: string): void;
1721
protected testMessageLocation: Location | undefined;
1822

23+
/**
24+
* Record that a test item has started executing.
25+
*/
26+
protected markItemStarted(item: TestItem): void {
27+
this.runningItems.add(item);
28+
}
29+
30+
/**
31+
* Record that a test item has finished executing.
32+
*/
33+
protected markItemFinished(item: TestItem): void {
34+
this.runningItems.delete(item);
35+
}
36+
37+
/**
38+
* Forward program output (captured from the test's debug session as DAP `output`
39+
* events) into the Test Results view. When exactly one test is currently running,
40+
* the output is attributed to that test item so it shows up under the test in the
41+
* explorer; otherwise (idle, or several tests running in parallel where attribution
42+
* would only be a guess) it is appended to the run as a whole.
43+
*/
44+
public appendProgramOutput(output: string): void {
45+
const normalized: string = output.replace(/\r?\n/g, '\r\n');
46+
const item: TestItem | undefined = this.runningItems.size === 1
47+
? this.runningItems.values().next().value
48+
: undefined;
49+
this.testContext.testRun.appendOutput(normalized, undefined, item);
50+
}
51+
1952
/**
2053
* Return a string array which contains the stacktraces that need to be filtered.
2154
* All the stacktraces which include the element in the return array will be removed.

src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,14 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
5252
public analyzeData(data: string): void {
5353
const lines: string[] = data.split(/\r?\n/);
5454
for (const line of lines) {
55+
// The socket stream carries only the JUnit runner's control protocol
56+
// (`%`-prefixed frames plus stack-trace / expected-actual payloads).
57+
// The control frames are noise, and the failure payloads are already
58+
// surfaced structurally as TestMessages on the failed items, so nothing
59+
// here is echoed to the Test Results output. The user-facing program
60+
// output is instead forwarded from the debug session's DAP `output`
61+
// events (see BaseRunner).
5562
this.processData(line);
56-
this.testContext.testRun.appendOutput(line + '\r\n');
5763
}
5864
}
5965

@@ -70,6 +76,7 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
7076
this.setDurationAtStart(this.getCurrentState(item));
7177
setTestState(this.testContext.testRun, item, this.getCurrentState(item).resultState);
7278
this.updateParentOnChildStart(item);
79+
this.markItemStarted(item);
7380
} else if (data.startsWith(MessageId.TestEnd)) {
7481
const item: TestItem | undefined = this.getTestItem(data.substr(MessageId.TestEnd.length));
7582
if (!item) {
@@ -79,6 +86,7 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer {
7986
this.calcDurationAtEnd(currentState);
8087
this.determineResultStateAtEnd(data, currentState);
8188
setTestState(this.testContext.testRun, item, currentState.resultState, undefined, currentState.duration);
89+
this.markItemFinished(item);
8290
const itemData: ITestItemData | undefined = dataCache.get(item);
8391
if (itemData?.testLevel === TestLevel.Method) {
8492
this.updateParentOnChildComplete(item, currentState.resultState);

src/runners/testngRunner/TestNGRunnerResultAnalyzer.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ export class TestNGRunnerResultAnalyzer extends RunnerResultAnalyzer {
5555
public processData(data: string): void {
5656
const outputData: ITestNGOutputData = JSON.parse(data) as ITestNGOutputData;
5757

58-
this.testContext.testRun.appendOutput(this.unescape(data).replace(/\r?\n/g, '\r\n'));
59-
6058
const id: string = `${this.projectName}@${outputData.attributes.name}`;
6159
if (outputData.name === TEST_START) {
6260
this.initializeCache();
@@ -68,6 +66,7 @@ export class TestNGRunnerResultAnalyzer extends RunnerResultAnalyzer {
6866
this.currentTestState = TestResultState.Running;
6967
this.testContext.testRun.started(item);
7068
this.updateParentOnChildStart(item);
69+
this.markItemStarted(item);
7170
} else if (outputData.name === TEST_FAIL) {
7271
const item: TestItem | undefined = this.getTestItem(id);
7372
if (!item) {
@@ -105,6 +104,7 @@ export class TestNGRunnerResultAnalyzer extends RunnerResultAnalyzer {
105104
}
106105
const duration: number = Number.parseInt(outputData.attributes.duration, 10);
107106
setTestState(this.testContext.testRun, item, this.currentTestState, undefined, duration);
107+
this.markItemFinished(item);
108108
const itemData: ITestItemData | undefined = dataCache.get(item);
109109
if (itemData?.testLevel === TestLevel.Method) {
110110
this.updateParentOnChildComplete(item, this.currentTestState);
@@ -121,15 +121,6 @@ export class TestNGRunnerResultAnalyzer extends RunnerResultAnalyzer {
121121
return this.currentItem;
122122
}
123123

124-
protected unescape(content: string): string {
125-
return content.replace(/\\r/gm, '\r')
126-
.replace(/\\f/gm, '\f')
127-
.replace(/\\n/gm, '\n')
128-
.replace(/\\t/gm, '\t')
129-
.replace(/\\b/gm, '\b')
130-
.replace(/\\"/gm, '"');
131-
}
132-
133124
protected initializeCache(): void {
134125
this.currentTestState = TestResultState.Queued;
135126
this.currentItem = undefined;

0 commit comments

Comments
 (0)