Skip to content

Commit 6bb6989

Browse files
authored
Merge pull request #397 from recca0120/refactor/append-output
refactor: pass location and testId to TestRun.appendOutput
2 parents 53d42db + 4817bb2 commit 6bb6989

11 files changed

Lines changed: 335 additions & 46 deletions

File tree

packages/extension/__mocks__/vscode.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,16 @@ class FakeEventEmitter<T = void> {
462462
}
463463
const EventEmitter = FakeEventEmitter;
464464

465-
class FakeTestMessageStackFrame {}
465+
class FakeTestMessageStackFrame {
466+
label: string;
467+
uri?: any;
468+
position?: any;
469+
constructor(label: string, uri?: any, position?: any) {
470+
this.label = label;
471+
this.uri = uri;
472+
this.position = position;
473+
}
474+
}
466475
const TestMessageStackFrame = FakeTestMessageStackFrame;
467476

468477
const extensions = {

packages/extension/src/Observers/ObserverFactory.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { PHPUnitXML, type TestDefinition } from '@vscode-phpunit/phpunit';
2-
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { beforeEach, describe, expect, it } from 'vitest';
33
import type { OutputChannel, TestItem, TestRun } from 'vscode';
44
import type { Configuration } from '../Configuration';
55
import type { TestCollection } from '../TestCollection/TestCollection';
@@ -14,23 +14,23 @@ describe('ObserverFactory', () => {
1414

1515
beforeEach(() => {
1616
const testCollection = {
17-
getTestDefinition: vi.fn(),
18-
resolveDatasetChild: vi.fn(),
17+
getTestDefinition: () => undefined,
18+
resolveDatasetChild: () => undefined,
1919
} as unknown as TestCollection;
2020
outputChannel = {
21-
append: vi.fn(),
22-
appendLine: vi.fn(),
23-
clear: vi.fn(),
24-
show: vi.fn(),
21+
append: () => {},
22+
appendLine: () => {},
23+
clear: () => {},
24+
show: () => {},
2525
} as unknown as OutputChannel;
26-
const configuration = { get: vi.fn() } as unknown as Configuration;
26+
const configuration = { get: () => undefined } as unknown as Configuration;
2727
const phpUnitXML = new PHPUnitXML();
2828
factory = new ObserverFactory(testCollection, outputChannel, configuration, phpUnitXML);
2929
});
3030

3131
it('should create observers including DatasetObserver, TestResultObserver, and PrinterObserver', () => {
3232
const queue = new Map<TestDefinition, TestItem>();
33-
const testRun = { enqueued: vi.fn() } as unknown as TestRun;
33+
const testRun = { enqueued: () => {} } as unknown as TestRun;
3434
const observers = factory.create(queue, testRun);
3535

3636
expect(observers.length).toBe(5);
@@ -41,7 +41,7 @@ describe('ObserverFactory', () => {
4141

4242
it('should create new instances for each call', () => {
4343
const queue = new Map<TestDefinition, TestItem>();
44-
const testRun = { enqueued: vi.fn() } as unknown as TestRun;
44+
const testRun = { enqueued: () => {} } as unknown as TestRun;
4545
const observers1 = factory.create(queue, testRun);
4646
const observers2 = factory.create(queue, testRun);
4747

packages/extension/src/Observers/ObserverFactory.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ export class ObserverFactory {
4040
new DatasetObserver(this.testCollection, testItemById),
4141
new TestResultObserver(testItemById, queue, testRun),
4242
new RawOutputObserver(this.outputChannel, this.configuration),
43-
new PrinterObserver(new TestRunWriter(testRun), new Printer(this.phpUnitXML, format)),
43+
new PrinterObserver(
44+
new TestRunWriter(testRun, testItemById),
45+
new Printer(this.phpUnitXML, format),
46+
),
4447
new ErrorDialogObserver(this.configuration),
4548
];
4649
}

packages/extension/src/Observers/PrinterObserver.test.ts

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import {
22
Configuration,
3+
type OutputWriter,
34
type Path,
45
PathReplacer,
56
PHPUnitXML,
67
PRESET_PROGRESS,
78
Printer,
89
ProcessBuilder,
910
semverLt,
11+
TeamcityEvent,
1012
TestRunner,
1113
} from '@vscode-phpunit/phpunit';
1214
import { detectPhpUnitStubs, phpUnitProject } from '@vscode-phpunit/phpunit/testing';
@@ -20,16 +22,22 @@ const writers = [
2022
writerName: 'OutputChannelWriter',
2123
createWriter: () => {
2224
const spy = vi.fn();
23-
const outputChannel = { append: spy, appendLine: vi.fn() };
25+
const outputChannel = { append: spy, appendLine: () => {} };
2426
return { writer: new OutputChannelWriter(outputChannel), spy };
2527
},
2628
},
2729
{
2830
writerName: 'TestRunWriter',
2931
createWriter: () => {
32+
const testRun = { appendOutput: vi.fn() } as unknown as TestRun;
33+
const writer = new TestRunWriter(testRun, new Map());
3034
const spy = vi.fn();
31-
const testRun = { appendOutput: spy } as unknown as TestRun;
32-
return { writer: new TestRunWriter(testRun), spy };
35+
const originalAppend = writer.append.bind(writer);
36+
writer.append = (text: string, ...args: unknown[]) => {
37+
spy(text);
38+
originalAppend(text, ...(args as []));
39+
};
40+
return { writer, spy };
3341
},
3442
},
3543
];
@@ -187,3 +195,96 @@ describe.each(writers)('PrinterObserver with $writerName', ({ createWriter }) =>
187195
});
188196
});
189197
});
198+
199+
describe('PrinterObserver passes location and testId to writer', () => {
200+
let appendSpy: ReturnType<typeof vi.fn>;
201+
let observer: PrinterObserver;
202+
203+
beforeEach(() => {
204+
appendSpy = vi.fn();
205+
const writer: OutputWriter = {
206+
append: appendSpy as OutputWriter['append'],
207+
appendLine: () => {},
208+
};
209+
observer = new PrinterObserver(writer, new Printer(new PHPUnitXML(), PRESET_PROGRESS));
210+
});
211+
212+
function startTest(name: string, id: string) {
213+
observer.testStarted({
214+
event: TeamcityEvent.testStarted,
215+
name,
216+
locationHint: '',
217+
flowId: 1,
218+
id,
219+
file: '/app/tests/MyTest.php',
220+
});
221+
appendSpy.mockClear();
222+
}
223+
224+
it('testFinished passes file line 1 as location', () => {
225+
startTest('test_passed', 'App\\Tests\\MyTest::test_passed');
226+
227+
observer.testFinished({
228+
event: TeamcityEvent.testFinished,
229+
name: 'test_passed',
230+
locationHint: '',
231+
flowId: 1,
232+
id: 'App\\Tests\\MyTest::test_passed',
233+
file: '/app/tests/MyTest.php',
234+
duration: 5,
235+
});
236+
237+
expect(appendSpy).toHaveBeenCalledWith(
238+
expect.any(String),
239+
{ file: '/app/tests/MyTest.php', line: 1 },
240+
'App\\Tests\\MyTest::test_passed',
241+
);
242+
});
243+
244+
it('testFailed passes detail location when available', () => {
245+
startTest('test_fail', 'App\\Tests\\MyTest::test_fail');
246+
247+
observer.testFailed({
248+
event: TeamcityEvent.testFailed,
249+
name: 'test_fail',
250+
locationHint: '',
251+
flowId: 1,
252+
id: 'App\\Tests\\MyTest::test_fail',
253+
file: '/app/tests/MyTest.php',
254+
message: 'Failed asserting that false is true.',
255+
details: [
256+
{ file: 'vendor/phpunit/phpunit/src/Framework/Assert.php', line: 198 },
257+
{ file: '/app/tests/MyTest.php', line: 27 },
258+
],
259+
duration: 0,
260+
});
261+
262+
expect(appendSpy).toHaveBeenCalledWith(
263+
expect.any(String),
264+
{ file: '/app/tests/MyTest.php', line: 27 },
265+
'App\\Tests\\MyTest::test_fail',
266+
);
267+
});
268+
269+
it('testIgnored passes file line 1 as location', () => {
270+
startTest('test_skipped', 'App\\Tests\\MyTest::test_skipped');
271+
272+
observer.testIgnored({
273+
event: TeamcityEvent.testIgnored,
274+
name: 'test_skipped',
275+
locationHint: '',
276+
flowId: 1,
277+
id: 'App\\Tests\\MyTest::test_skipped',
278+
file: '/app/tests/MyTest.php',
279+
message: 'The MySQLi extension is not available.',
280+
details: [],
281+
duration: 0,
282+
});
283+
284+
expect(appendSpy).toHaveBeenCalledWith(
285+
expect.any(String),
286+
{ file: '/app/tests/MyTest.php', line: 1 },
287+
'App\\Tests\\MyTest::test_skipped',
288+
);
289+
});
290+
});

packages/extension/src/Observers/PrinterObserver.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
OutputLocation,
23
OutputWriter,
34
Printer,
45
TestConfiguration,
@@ -8,6 +9,7 @@ import type {
89
TestIgnored,
910
TestProcesses,
1011
TestResult,
12+
TestResultIdentify,
1113
TestResultSummary,
1214
TestRunnerObserver,
1315
TestRuntime,
@@ -52,11 +54,11 @@ export class PrinterObserver implements TestRunnerObserver {
5254
}
5355

5456
testStarted(result: TestStarted): void {
55-
this.append(this.printer.testStarted(result));
57+
this.append(this.printer.testStarted(result), this.toLocation(result), result.id);
5658
}
5759

5860
testFinished(result: TestFinished): void {
59-
this.append(this.printer.testFinished(result));
61+
this.append(this.printer.testFinished(result), this.toLocation(result), result.id);
6062
this.flushOutput(result);
6163
}
6264

@@ -65,7 +67,7 @@ export class PrinterObserver implements TestRunnerObserver {
6567
}
6668

6769
testIgnored(result: TestIgnored): void {
68-
this.append(this.printer.testIgnored(result));
70+
this.append(this.printer.testIgnored(result), this.toLocation(result), result.id);
6971
this.flushOutput(result);
7072
}
7173

@@ -88,14 +90,39 @@ export class PrinterObserver implements TestRunnerObserver {
8890

8991
private flushOutput(result?: TestResult): void {
9092
const output = this.printer.flushOutput(result);
91-
if (output) {
93+
if (!output) {
94+
return;
95+
}
96+
97+
if (!result || !this.isIdentifiable(result)) {
9298
this.append(output);
99+
return;
100+
}
101+
102+
this.append(output, this.toLocation(result), result.id);
103+
}
104+
105+
private isIdentifiable(
106+
result: TestResult,
107+
): result is TestResult & TestResultIdentify & { details?: TestFailed['details'] } {
108+
return 'id' in result && 'file' in result;
109+
}
110+
111+
private toLocation(
112+
result: TestResultIdentify & { details?: TestFailed['details'] },
113+
): OutputLocation | undefined {
114+
if (!result.file) {
115+
return undefined;
93116
}
117+
118+
const detail = result.details?.find(({ file }) => file === result.file);
119+
120+
return { file: result.file, line: detail?.line ?? 1 };
94121
}
95122

96-
private append(text: string | undefined) {
123+
private append(text: string | undefined, location?: OutputLocation, testId?: string) {
97124
if (text !== undefined) {
98-
this.writer.append(text);
125+
this.writer.append(text, location, testId);
99126
}
100127
}
101128
}

packages/extension/src/Observers/TestResultObserver.test.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import type { TeamcityEvent, TestDefinition, TestFailed } from '@vscode-phpunit/phpunit';
1+
import type {
2+
TeamcityEvent,
3+
TestDefinition,
4+
TestFailed,
5+
TestFinished,
6+
} from '@vscode-phpunit/phpunit';
27
import { beforeEach, describe, expect, it, vi } from 'vitest';
38
import {
49
type TestController,
@@ -122,6 +127,68 @@ describe('TestResultObserver', () => {
122127
expect(testRun.started).toHaveBeenCalledWith(datasetItem);
123128
});
124129

130+
it('should pass duration to testRun.passed', () => {
131+
observer.testFinished({
132+
event: 'testFinished' as unknown as TeamcityEvent,
133+
id: 'Tests\\ExampleTest::test_example',
134+
flowId: 1,
135+
name: 'test_example',
136+
file: '/project/tests/ExampleTest.php',
137+
locationHint: '',
138+
duration: 42,
139+
} as TestFinished);
140+
141+
expect(testRun.passed).toHaveBeenCalledWith(testItem, 42);
142+
});
143+
144+
it('should use 0-based line in message location', () => {
145+
observer.testFailed(
146+
createTestFailed({
147+
details: [
148+
{ file: '/project/tests/ExampleTest.php', line: 10 },
149+
{ file: '/project/tests/ExampleTest.php', line: 20 },
150+
],
151+
}),
152+
);
153+
154+
const failedCall = (testRun.failed as ReturnType<typeof vi.fn>).mock.calls[0];
155+
const message = failedCall[1];
156+
157+
expect(message.location.range.start.line).toBe(9);
158+
});
159+
160+
it('should use 0-based line in stackTrace position', () => {
161+
observer.testFailed(
162+
createTestFailed({
163+
details: [
164+
{ file: '/project/tests/ExampleTest.php', line: 10 },
165+
{ file: '/project/tests/ExampleTest.php', line: 20 },
166+
],
167+
}),
168+
);
169+
170+
const failedCall = (testRun.failed as ReturnType<typeof vi.fn>).mock.calls[0];
171+
const message = failedCall[1];
172+
173+
expect(message.stackTrace).toHaveLength(1);
174+
expect(message.stackTrace[0].position.line).toBe(19);
175+
});
176+
177+
it('should not set location when result.file is undefined', () => {
178+
observer.testFailed(
179+
createTestFailed({
180+
file: undefined as unknown as string,
181+
details: [{ file: '/project/tests/ExampleTest.php', line: 10 }],
182+
}),
183+
);
184+
185+
const failedCall = (testRun.failed as ReturnType<typeof vi.fn>).mock.calls[0];
186+
const message = failedCall[1];
187+
188+
expect(message.location).toBeUndefined();
189+
expect(message.stackTrace).toBeUndefined();
190+
});
191+
125192
it('should not use TestMessage.diff when expected/actual are missing', () => {
126193
const diffSpy = vi.spyOn(TestMessage, 'diff');
127194

0 commit comments

Comments
 (0)