Skip to content

Commit 6ae4fa8

Browse files
authored
Cleanup: Refactor swift-testing output parser (#1688)
Break up the giant switch statement for handling events coming from the swift testing even stream into smaller, well define functions.
1 parent 95582d7 commit 6ae4fa8

File tree

1 file changed

+190
-129
lines changed

1 file changed

+190
-129
lines changed

src/TestExplorer/TestParsers/SwiftTestingOutputParser.ts

Lines changed: 190 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ interface TestFunction extends TestBase {
8181
isParameterized: boolean;
8282
}
8383

84+
type ParameterizedTestRecord = TestRecord & {
85+
payload: {
86+
kind: "function";
87+
isParameterized: true;
88+
_testCases: TestCase[];
89+
};
90+
};
91+
8492
export interface TestCase {
8593
id: string;
8694
displayName: string;
@@ -249,6 +257,188 @@ export class SwiftTestingOutputParser {
249257
: new UnixNamedPipeReader(path);
250258
}
251259

260+
private parse(item: SwiftTestEvent, runState: ITestRunState) {
261+
switch (item.kind) {
262+
case "test":
263+
this.handleTestEvent(item, runState);
264+
break;
265+
case "event":
266+
this.handleEventRecord(item.payload, runState);
267+
break;
268+
}
269+
}
270+
271+
private handleTestEvent(item: TestRecord, runState: ITestRunState) {
272+
if (this.isParameterizedFunction(item)) {
273+
this.handleParameterizedFunction(item, runState);
274+
}
275+
}
276+
277+
private handleEventRecord(payload: EventRecordPayload, runState: ITestRunState) {
278+
switch (payload.kind) {
279+
case "runStarted":
280+
this.handleRunStarted();
281+
break;
282+
case "testStarted":
283+
this.handleTestStarted(payload, runState);
284+
break;
285+
case "testCaseStarted":
286+
this.handleTestCaseStarted(payload, runState);
287+
break;
288+
case "testSkipped":
289+
this.handleTestSkipped(payload, runState);
290+
break;
291+
case "issueRecorded":
292+
this.handleIssueRecorded(payload, runState);
293+
break;
294+
case "testEnded":
295+
this.handleTestEnded(payload, runState);
296+
break;
297+
case "testCaseEnded":
298+
this.handleTestCaseEnded(payload, runState);
299+
break;
300+
case "_valueAttached":
301+
this.handleValueAttached(payload, runState);
302+
break;
303+
}
304+
}
305+
306+
private isParameterizedFunction(item: TestRecord): item is ParameterizedTestRecord {
307+
return (
308+
item.kind === "test" &&
309+
item.payload.kind === "function" &&
310+
item.payload.isParameterized &&
311+
!!item.payload._testCases
312+
);
313+
}
314+
315+
private handleParameterizedFunction(item: ParameterizedTestRecord, runState: ITestRunState) {
316+
// Store a map of [Test ID, [Test Case ID, TestCase]] so we can quickly
317+
// map an event.payload.testID back to a test case.
318+
this.buildTestCaseMapForParameterizedTest(item);
319+
320+
const testIndex = this.testItemIndexFromTestID(item.payload.id, runState);
321+
// If a test has test cases it is paramterized and we need to notify
322+
// the caller that the TestClass should be added to the vscode.TestRun
323+
// before it starts.
324+
item.payload._testCases
325+
.map((testCase, index) =>
326+
this.parameterizedFunctionTestCaseToTestClass(
327+
item.payload.id,
328+
testCase,
329+
sourceLocationToVSCodeLocation(
330+
item.payload.sourceLocation._filePath,
331+
item.payload.sourceLocation.line,
332+
item.payload.sourceLocation.column
333+
),
334+
index
335+
)
336+
)
337+
.flatMap(testClass => (testClass ? [testClass] : []))
338+
.forEach(testClass => this.addParameterizedTestCase(testClass, testIndex));
339+
}
340+
341+
private handleRunStarted() {
342+
// Notify the runner that we've received all the test cases and
343+
// are going to start running tests now.
344+
this.testRunStarted();
345+
}
346+
347+
private handleTestStarted(payload: TestStarted, runState: ITestRunState) {
348+
const testIndex = this.testItemIndexFromTestID(payload.testID, runState);
349+
runState.started(testIndex, payload.instant.absolute);
350+
}
351+
352+
private handleTestCaseStarted(payload: TestCaseStarted, runState: ITestRunState) {
353+
const testID = this.idFromOptionalTestCase(payload.testID, payload._testCase);
354+
const testIndex = this.getTestCaseIndex(runState, testID);
355+
runState.started(testIndex, payload.instant.absolute);
356+
}
357+
358+
private handleTestSkipped(payload: TestSkipped, runState: ITestRunState) {
359+
const testIndex = this.testItemIndexFromTestID(payload.testID, runState);
360+
runState.skipped(testIndex);
361+
}
362+
363+
private handleIssueRecorded(payload: IssueRecorded, runState: ITestRunState) {
364+
const testID = this.idFromOptionalTestCase(payload.testID, payload._testCase);
365+
const testIndex = this.getTestCaseIndex(runState, testID);
366+
const { isKnown, sourceLocation } = payload.issue;
367+
const location = sourceLocationToVSCodeLocation(
368+
sourceLocation._filePath,
369+
sourceLocation.line,
370+
sourceLocation.column
371+
);
372+
373+
const messages = this.transformIssueMessageSymbols(payload.messages);
374+
const { issues, details } = this.partitionIssueMessages(messages);
375+
376+
// Order the details after the issue text.
377+
const additionalDetails = details
378+
.map(message => MessageRenderer.render(message))
379+
.join("\n");
380+
381+
issues.forEach(message => {
382+
runState.recordIssue(
383+
testIndex,
384+
additionalDetails.length > 0
385+
? `${MessageRenderer.render(message)}\n${additionalDetails}`
386+
: MessageRenderer.render(message),
387+
isKnown,
388+
location
389+
);
390+
});
391+
392+
if (payload._testCase && testID !== payload.testID) {
393+
const testIndex = this.getTestCaseIndex(runState, payload.testID);
394+
messages.forEach(message => {
395+
runState.recordIssue(testIndex, message.text, isKnown, location);
396+
});
397+
}
398+
}
399+
400+
private handleTestEnded(payload: TestEnded, runState: ITestRunState) {
401+
const testIndex = this.testItemIndexFromTestID(payload.testID, runState);
402+
403+
// When running a single test the testEnded and testCaseEnded events
404+
// have the same ID, and so we'd end the same test twice.
405+
if (this.checkTestCompleted(testIndex)) {
406+
return;
407+
}
408+
runState.completed(testIndex, { timestamp: payload.instant.absolute });
409+
}
410+
411+
private handleTestCaseEnded(payload: TestCaseEnded, runState: ITestRunState) {
412+
const testID = this.idFromOptionalTestCase(payload.testID, payload._testCase);
413+
const testIndex = this.getTestCaseIndex(runState, testID);
414+
415+
// When running a single test the testEnded and testCaseEnded events
416+
// have the same ID, and so we'd end the same test twice.
417+
if (this.checkTestCompleted(testIndex)) {
418+
return;
419+
}
420+
runState.completed(testIndex, { timestamp: payload.instant.absolute });
421+
}
422+
423+
private handleValueAttached(payload: ValueAttached, runState: ITestRunState) {
424+
if (!payload._attachment.path) {
425+
return;
426+
}
427+
const testID = this.idFromOptionalTestCase(payload.testID);
428+
const testIndex = this.getTestCaseIndex(runState, testID);
429+
430+
this.onAttachment(testIndex, payload._attachment.path);
431+
}
432+
433+
private checkTestCompleted(testIndex: number): boolean {
434+
// If the test has already been completed, we don't need to do anything.
435+
if (this.completionMap.get(testIndex)) {
436+
return true;
437+
}
438+
this.completionMap.set(testIndex, true);
439+
return false;
440+
}
441+
252442
private testName(id: string): string {
253443
const nameMatcher = /^(.*\(.*\))\/(.*)\.swift:\d+:\d+$/;
254444
const matches = id.match(nameMatcher);
@@ -355,135 +545,6 @@ export class SwiftTestingOutputParser {
355545
}
356546
return id;
357547
}
358-
359-
private parse(item: SwiftTestEvent, runState: ITestRunState) {
360-
if (
361-
item.kind === "test" &&
362-
item.payload.kind === "function" &&
363-
item.payload.isParameterized &&
364-
item.payload._testCases
365-
) {
366-
// Store a map of [Test ID, [Test Case ID, TestCase]] so we can quickly
367-
// map an event.payload.testID back to a test case.
368-
this.buildTestCaseMapForParameterizedTest(item);
369-
370-
const testIndex = this.testItemIndexFromTestID(item.payload.id, runState);
371-
// If a test has test cases it is paramterized and we need to notify
372-
// the caller that the TestClass should be added to the vscode.TestRun
373-
// before it starts.
374-
item.payload._testCases
375-
.map((testCase, index) =>
376-
this.parameterizedFunctionTestCaseToTestClass(
377-
item.payload.id,
378-
testCase,
379-
sourceLocationToVSCodeLocation(
380-
item.payload.sourceLocation._filePath,
381-
item.payload.sourceLocation.line,
382-
item.payload.sourceLocation.column
383-
),
384-
index
385-
)
386-
)
387-
.flatMap(testClass => (testClass ? [testClass] : []))
388-
.forEach(testClass => this.addParameterizedTestCase(testClass, testIndex));
389-
} else if (item.kind === "event") {
390-
if (item.payload.kind === "runStarted") {
391-
// Notify the runner that we've recieved all the test cases and
392-
// are going to start running tests now.
393-
this.testRunStarted();
394-
return;
395-
} else if (item.payload.kind === "testStarted") {
396-
const testIndex = this.testItemIndexFromTestID(item.payload.testID, runState);
397-
runState.started(testIndex, item.payload.instant.absolute);
398-
return;
399-
} else if (item.payload.kind === "testCaseStarted") {
400-
const testID = this.idFromOptionalTestCase(
401-
item.payload.testID,
402-
item.payload._testCase
403-
);
404-
const testIndex = this.getTestCaseIndex(runState, testID);
405-
runState.started(testIndex, item.payload.instant.absolute);
406-
return;
407-
} else if (item.payload.kind === "testSkipped") {
408-
const testIndex = this.testItemIndexFromTestID(item.payload.testID, runState);
409-
runState.skipped(testIndex);
410-
return;
411-
} else if (item.payload.kind === "issueRecorded") {
412-
const testID = this.idFromOptionalTestCase(
413-
item.payload.testID,
414-
item.payload._testCase
415-
);
416-
const testIndex = this.getTestCaseIndex(runState, testID);
417-
418-
const isKnown = item.payload.issue.isKnown;
419-
const sourceLocation = item.payload.issue.sourceLocation;
420-
const location = sourceLocationToVSCodeLocation(
421-
sourceLocation._filePath,
422-
sourceLocation.line,
423-
sourceLocation.column
424-
);
425-
426-
const messages = this.transformIssueMessageSymbols(item.payload.messages);
427-
const { issues, details } = this.partitionIssueMessages(messages);
428-
429-
// Order the details after the issue text.
430-
const additionalDetails = details
431-
.map(message => MessageRenderer.render(message))
432-
.join("\n");
433-
434-
issues.forEach(message => {
435-
runState.recordIssue(
436-
testIndex,
437-
additionalDetails.length > 0
438-
? `${MessageRenderer.render(message)}\n${additionalDetails}`
439-
: MessageRenderer.render(message),
440-
isKnown,
441-
location
442-
);
443-
});
444-
445-
if (item.payload._testCase && testID !== item.payload.testID) {
446-
const testIndex = this.getTestCaseIndex(runState, item.payload.testID);
447-
messages.forEach(message => {
448-
runState.recordIssue(testIndex, message.text, isKnown, location);
449-
});
450-
}
451-
return;
452-
} else if (item.payload.kind === "testEnded") {
453-
const testIndex = this.testItemIndexFromTestID(item.payload.testID, runState);
454-
455-
// When running a single test the testEnded and testCaseEnded events
456-
// have the same ID, and so we'd end the same test twice.
457-
if (this.completionMap.get(testIndex)) {
458-
return;
459-
}
460-
this.completionMap.set(testIndex, true);
461-
runState.completed(testIndex, { timestamp: item.payload.instant.absolute });
462-
return;
463-
} else if (item.payload.kind === "testCaseEnded") {
464-
const testID = this.idFromOptionalTestCase(
465-
item.payload.testID,
466-
item.payload._testCase
467-
);
468-
const testIndex = this.getTestCaseIndex(runState, testID);
469-
470-
// When running a single test the testEnded and testCaseEnded events
471-
// have the same ID, and so we'd end the same test twice.
472-
if (this.completionMap.get(testIndex)) {
473-
return;
474-
}
475-
this.completionMap.set(testIndex, true);
476-
runState.completed(testIndex, { timestamp: item.payload.instant.absolute });
477-
return;
478-
} else if (item.payload.kind === "_valueAttached" && item.payload._attachment.path) {
479-
const testID = this.idFromOptionalTestCase(item.payload.testID);
480-
const testIndex = this.getTestCaseIndex(runState, testID);
481-
482-
this.onAttachment(testIndex, item.payload._attachment.path);
483-
return;
484-
}
485-
}
486-
}
487548
}
488549

489550
export class MessageRenderer {

0 commit comments

Comments
 (0)