Skip to content

Commit 5cc4236

Browse files
authored
Test suites in nested extensions have incorrect parent in test explorer (#1275)
1 parent 1cbbb45 commit 5cc4236

File tree

3 files changed

+100
-17
lines changed

3 files changed

+100
-17
lines changed

src/TestExplorer/TestDiscovery.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,61 @@ export function updateTestsFromClasses(
6666
updateTests(testController, targets);
6767
}
6868

69+
export function updateTestsForTarget(
70+
testController: vscode.TestController,
71+
testTarget: { id: string; label: string },
72+
testItems: TestClass[],
73+
filterFile?: vscode.Uri
74+
) {
75+
// Because swift-testing suites can be defined through nested extensions the tests
76+
// provided might not be directly parented to the test target. For instance, the
77+
// target might be `Foo`, and one of the child `testItems` might be `Foo.Bar/Baz`.
78+
// If we simply attach the `testItems` to the root test target then the intermediate
79+
// suite `Bar` will be dropped. To avoid this, we syntheize the intermediate children
80+
// just like we synthesize the test target.
81+
function synthesizeChildren(testItem: TestClass): TestClass {
82+
// Only Swift Testing tests can be nested in a way that requires synthesis.
83+
if (testItem.style === "XCTest") {
84+
return testItem;
85+
}
86+
87+
const item = { ...testItem };
88+
// To determine if any root level test items are missing a parent we check how many
89+
// components there are in the ID. If there are more than one (the test target) then
90+
// we synthesize all the intermediary test items.
91+
const idComponents = testItem.id.split(/\.|\//);
92+
idComponents.pop(); // Remove the last component to get the parent ID components
93+
if (idComponents.length > 1) {
94+
let newId = idComponents.slice(0, 2).join(".");
95+
const remainingIdComponents = idComponents.slice(2);
96+
if (remainingIdComponents.length) {
97+
newId += "/" + remainingIdComponents.join("/");
98+
}
99+
return synthesizeChildren({
100+
id: newId,
101+
label: idComponents[idComponents.length - 1],
102+
children: [item],
103+
location: undefined,
104+
disabled: false,
105+
style: item.style,
106+
tags: item.tags,
107+
});
108+
}
109+
return item;
110+
}
111+
112+
const testTargetClass: TestClass = {
113+
id: testTarget.id,
114+
label: testTarget.label,
115+
children: testItems.map(synthesizeChildren),
116+
location: undefined,
117+
disabled: false,
118+
style: "test-target",
119+
tags: [],
120+
};
121+
updateTests(testController, [testTargetClass], filterFile);
122+
}
123+
69124
/**
70125
* Update Test Controller TestItems based off array of TestTargets
71126
* @param testController Test controller

src/TestExplorer/TestExplorer.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -210,23 +210,17 @@ export class TestExplorer {
210210
if (target && target.type === "test") {
211211
testExplorer.lspTestDiscovery
212212
.getDocumentTests(folder.swiftPackage, uri)
213-
.then(
214-
tests =>
215-
[
216-
{
217-
id: target.c99name,
218-
label: target.name,
219-
children: tests,
220-
location: undefined,
221-
disabled: false,
222-
style: "test-target",
223-
tags: [],
224-
},
225-
] as TestDiscovery.TestClass[]
213+
.then(tests =>
214+
TestDiscovery.updateTestsForTarget(
215+
testExplorer.controller,
216+
{ id: target.c99name, label: target.name },
217+
tests,
218+
uri
219+
)
226220
)
227221
// Fallback to parsing document symbols for XCTests only
228-
.catch(() => parseTestsFromDocumentSymbols(target.name, symbols, uri))
229-
.then(tests => {
222+
.catch(() => {
223+
const tests = parseTestsFromDocumentSymbols(target.name, symbols, uri);
230224
testExplorer.updateTests(testExplorer.controller, tests, uri);
231225
});
232226
}

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ import { beforeEach } from "mocha";
1818
import {
1919
TestClass,
2020
updateTests,
21+
updateTestsForTarget,
2122
updateTestsFromClasses,
2223
} from "../../../src/TestExplorer/TestDiscovery";
2324
import { reduceTestItemChildren } from "../../../src/TestExplorer/TestUtils";
2425
import { SwiftPackage, Target, TargetType } from "../../../src/SwiftPackage";
2526
import { SwiftToolchain } from "../../../src/toolchain/toolchain";
27+
import { TestStyle } from "../../../src/sourcekit-lsp/extensions";
2628

2729
suite("TestDiscovery Suite", () => {
2830
let testController: vscode.TestController;
@@ -49,12 +51,12 @@ suite("TestDiscovery Suite", () => {
4951
);
5052
}
5153

52-
function testItem(id: string): TestClass {
54+
function testItem(id: string, style: TestStyle = "XCTest"): TestClass {
5355
return {
5456
id,
5557
label: id,
5658
disabled: false,
57-
style: "XCTest",
59+
style,
5860
location: undefined,
5961
tags: [],
6062
children: [],
@@ -158,6 +160,38 @@ suite("TestDiscovery Suite", () => {
158160
assert.deepStrictEqual(testController.items.get("foo")?.label, "New Label");
159161
});
160162

163+
test("handles adding a test to an existing parent when updating with a partial tree", () => {
164+
const child = testItem("AppTarget.AppTests/ChildTests/SubChildTests", "swift-testing");
165+
166+
updateTestsForTarget(testController, { id: "AppTarget", label: "AppTarget" }, [child]);
167+
168+
assert.deepStrictEqual(testControllerChildren(testController.items), [
169+
{
170+
id: "AppTarget",
171+
tags: [{ id: "test-target" }, { id: "runnable" }],
172+
children: [
173+
{
174+
id: "AppTarget.AppTests",
175+
tags: [{ id: "swift-testing" }, { id: "runnable" }],
176+
children: [
177+
{
178+
id: "AppTarget.AppTests/ChildTests",
179+
tags: [{ id: "swift-testing" }, { id: "runnable" }],
180+
children: [
181+
{
182+
id: "AppTarget.AppTests/ChildTests/SubChildTests",
183+
tags: [{ id: "swift-testing" }, { id: "runnable" }],
184+
children: [],
185+
},
186+
],
187+
},
188+
],
189+
},
190+
],
191+
},
192+
]);
193+
});
194+
161195
test("updates tests from classes within a swift package", async () => {
162196
const file = vscode.Uri.file("file:///some/file.swift");
163197
const swiftPackage = await SwiftPackage.create(file, await SwiftToolchain.create());

0 commit comments

Comments
 (0)