Skip to content

Commit 6e5c04a

Browse files
authored
Fix improper parenting of tests w/ identical names in explorer (#1664)
* Fix improper parenting of tests w/ identical names in explorer Two tests in swift-testing can share the same identifier, for instance if you have two identically named tests with the same name but that take arguments of different types, i.e: ``` @test(arguments: [1]) func a(_ x: Int) {} @test(arguments: ["a"]) func a(_ x: String) {} ``` In this case sourcekit-lsp provides a disambiguated id with the filename/line number at the end. The code to synthesize parents in the test explorer when provided with test items that had none didn't take in to account this form of disambiguated id, which lead to tests being incorrectly synthesized and parented to the wrong test.
1 parent da07bbd commit 6e5c04a

File tree

5 files changed

+71
-13
lines changed

5 files changed

+71
-13
lines changed

src/TestExplorer/TestDiscovery.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ export async function updateTestsFromClasses(
7373
updateTests(testController, results);
7474
}
7575

76+
function isFileDisambiguated(id: string): boolean {
77+
// a regex to check if the id ends with a string like "filename.swift:line:column"
78+
const regex = /^(.*\/)?([^/]+\.swift):(\d+):(\d+)$/;
79+
return regex.test(id);
80+
}
81+
7682
export function updateTestsForTarget(
7783
testController: vscode.TestController,
7884
testTarget: { id: string; label: string },
@@ -91,13 +97,23 @@ export function updateTestsForTarget(
9197
return testItem;
9298
}
9399

100+
const fileDisambiguated = isFileDisambiguated(testItem.id);
94101
const item = { ...testItem };
95102
// To determine if any root level test items are missing a parent we check how many
96103
// components there are in the ID. If there are more than one (the test target) then
97104
// we synthesize all the intermediary test items.
98105
const idComponents = testItem.id.split(/\.|\//);
99-
idComponents.pop(); // Remove the last component to get the parent ID components
100-
if (idComponents.length > 1) {
106+
107+
// Remove the last component to get the parent ID components
108+
idComponents.pop();
109+
110+
// If this is a file disambiguated id (ends in <file>.swift:<line>:<column>),
111+
// remove both the filename and line info.
112+
if (fileDisambiguated) {
113+
idComponents.pop();
114+
}
115+
116+
if (idComponents.length > (fileDisambiguated ? 2 : 1)) {
101117
let newId = idComponents.slice(0, 2).join(".");
102118
const remainingIdComponents = idComponents.slice(2);
103119
if (remainingIdComponents.length) {

src/TestExplorer/TestParsers/SwiftTestingOutputParser.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,15 @@ export class SwiftTestingOutputParser {
347347
}));
348348
}
349349

350+
private testItemIndexFromTestID(testID: string, runState: ITestRunState): number {
351+
const testName = this.testName(testID);
352+
const id = runState.getTestItemIndex(testName, undefined);
353+
if (id === -1) {
354+
return runState.getTestItemIndex(testID, undefined);
355+
}
356+
return id;
357+
}
358+
350359
private parse(item: SwiftTestEvent, runState: ITestRunState) {
351360
if (
352361
item.kind === "test" &&
@@ -358,8 +367,7 @@ export class SwiftTestingOutputParser {
358367
// map an event.payload.testID back to a test case.
359368
this.buildTestCaseMapForParameterizedTest(item);
360369

361-
const testName = this.testName(item.payload.id);
362-
const testIndex = runState.getTestItemIndex(testName, undefined);
370+
const testIndex = this.testItemIndexFromTestID(item.payload.id, runState);
363371
// If a test has test cases it is paramterized and we need to notify
364372
// the caller that the TestClass should be added to the vscode.TestRun
365373
// before it starts.
@@ -385,8 +393,7 @@ export class SwiftTestingOutputParser {
385393
this.testRunStarted();
386394
return;
387395
} else if (item.payload.kind === "testStarted") {
388-
const testName = this.testName(item.payload.testID);
389-
const testIndex = runState.getTestItemIndex(testName, undefined);
396+
const testIndex = this.testItemIndexFromTestID(item.payload.testID, runState);
390397
runState.started(testIndex, item.payload.instant.absolute);
391398
return;
392399
} else if (item.payload.kind === "testCaseStarted") {
@@ -398,8 +405,7 @@ export class SwiftTestingOutputParser {
398405
runState.started(testIndex, item.payload.instant.absolute);
399406
return;
400407
} else if (item.payload.kind === "testSkipped") {
401-
const testName = this.testName(item.payload.testID);
402-
const testIndex = runState.getTestItemIndex(testName, undefined);
408+
const testIndex = this.testItemIndexFromTestID(item.payload.testID, runState);
403409
runState.skipped(testIndex);
404410
return;
405411
} else if (item.payload.kind === "issueRecorded") {
@@ -444,8 +450,7 @@ export class SwiftTestingOutputParser {
444450
}
445451
return;
446452
} else if (item.payload.kind === "testEnded") {
447-
const testName = this.testName(item.payload.testID);
448-
const testIndex = runState.getTestItemIndex(testName, undefined);
453+
const testIndex = this.testItemIndexFromTestID(item.payload.testID, runState);
449454

450455
// When running a single test the testEnded and testCaseEnded events
451456
// have the same ID, and so we'd end the same test twice.

src/TestExplorer/TestRunArguments.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import * as vscode from "vscode";
1616
import { reduceTestItemChildren } from "./TestUtils";
17+
import { TestRunProxy } from "./TestRunner";
1718

1819
type ProcessResult = {
1920
testItems: vscode.TestItem[];
@@ -93,7 +94,7 @@ export class TestRunArguments {
9394
const terminator = hasChildren ? "/" : "$";
9495
// Debugging XCTests requires exact matches, so we don't need a trailing terminator.
9596
return isDebug ? arg.id : `${arg.id}${terminator}`;
96-
} else if (hasChildren) {
97+
} else if (hasChildren && !this.hasParameterizedTestChildren(arg)) {
9798
// Append a trailing slash to match a suite name exactly.
9899
// This prevents TestTarget.MySuite matching TestTarget.MySuite2.
99100
return `${arg.id}/`;
@@ -102,6 +103,12 @@ export class TestRunArguments {
102103
});
103104
}
104105

106+
private hasParameterizedTestChildren(testItem: vscode.TestItem): boolean {
107+
return Array.from(testItem.children).some(arr =>
108+
arr[1].tags.some(tag => tag.id === TestRunProxy.Tags.PARAMETERIZED_TEST_RESULT)
109+
);
110+
}
111+
105112
private createTestItemReducer(
106113
include: readonly vscode.TestItem[],
107114
exclude: readonly vscode.TestItem[],

src/TestExplorer/TestRunner.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export class TestRunProxy {
164164
// https://github.com/swiftlang/swift-testing/issues/671 is resolved.
165165
testClass.tags = compactMap(parent.tags, t =>
166166
t.id === runnableTag.id ? null : new vscode.TestTag(t.id)
167-
);
167+
).concat(new vscode.TestTag(TestRunProxy.Tags.PARAMETERIZED_TEST_RESULT));
168168

169169
const added = upsertTestItem(this.controller, testClass, parent);
170170

@@ -406,9 +406,10 @@ export class TestRunProxy {
406406
await this.coverage.computeCoverage(this.testRun);
407407
}
408408

409-
private static Tags = {
409+
static Tags = {
410410
SKIPPED: "skipped",
411411
HAS_ATTACHMENT: "hasAttachment",
412+
PARAMETERIZED_TEST_RESULT: "parameterizedTestResult",
412413
};
413414

414415
// Remove any tags that were added due to test results

test/integration-tests/testexplorer/TestDiscovery.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,35 @@ suite("TestDiscovery Suite", () => {
160160
assert.deepStrictEqual(testController.items.get("foo")?.label, "New Label");
161161
});
162162

163+
test("handles adding tests that are disambiguated by a file/location", () => {
164+
const child1 = testItem("AppTarget.example(_:)/AppTarget.swift:4:2", "swift-testing");
165+
const child2 = testItem("AppTarget.example(_:)/AppTarget.swift:16:2", "swift-testing");
166+
167+
updateTestsForTarget(testController, { id: "AppTarget", label: "AppTarget" }, [
168+
child1,
169+
child2,
170+
]);
171+
172+
assert.deepStrictEqual(testControllerChildren(testController.items), [
173+
{
174+
id: "AppTarget",
175+
tags: [{ id: "test-target" }, { id: "runnable" }],
176+
children: [
177+
{
178+
id: "AppTarget.example(_:)/AppTarget.swift:4:2",
179+
tags: [{ id: "swift-testing" }, { id: "runnable" }],
180+
children: [],
181+
},
182+
{
183+
id: "AppTarget.example(_:)/AppTarget.swift:16:2",
184+
tags: [{ id: "swift-testing" }, { id: "runnable" }],
185+
children: [],
186+
},
187+
],
188+
},
189+
]);
190+
});
191+
163192
test("handles adding a test to an existing parent when updating with a partial tree", () => {
164193
const child = testItem("AppTarget.AppTests/ChildTests/SubChildTests", "swift-testing");
165194

0 commit comments

Comments
 (0)