Skip to content

Commit de3c2f3

Browse files
authored
Fix improper test argument simplification (#1186)
If the user tried to run a test suite that had as many tests as there are test suites in the module we improperly simplified the test arguments to run all suites in the module. This patch fixes this behaviour, and also introduces more robust tests. Issue: #1183
1 parent d28a238 commit de3c2f3

File tree

2 files changed

+341
-179
lines changed

2 files changed

+341
-179
lines changed

src/TestExplorer/TestRunArguments.ts

+80-67
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import { reduceTestItemChildren } from "./TestUtils";
1717

1818
type ProcessResult = {
1919
testItems: vscode.TestItem[];
20-
xcTestArgs: string[];
21-
swiftTestArgs: string[];
20+
xcTestArgs: vscode.TestItem[];
21+
swiftTestArgs: vscode.TestItem[];
2222
};
2323

2424
/**
@@ -30,14 +30,11 @@ export class TestRunArguments {
3030
public xcTestArgs: string[];
3131
public swiftTestArgs: string[];
3232

33-
constructor(
34-
request: vscode.TestRunRequest,
35-
private isDebug: boolean
36-
) {
37-
const { testItems, xcTestArgs, swiftTestArgs } = this.createTestLists(request);
33+
constructor(request: vscode.TestRunRequest, isDebug: boolean) {
34+
const { testItems, xcTestArgs, swiftTestArgs } = this.createTestLists(request, isDebug);
3835
this.testItems = testItems;
39-
this.xcTestArgs = xcTestArgs;
40-
this.swiftTestArgs = swiftTestArgs;
36+
this.xcTestArgs = this.annotateTestArgs(xcTestArgs, isDebug);
37+
this.swiftTestArgs = this.annotateTestArgs(swiftTestArgs, isDebug);
4138
}
4239

4340
public get hasXCTests(): boolean {
@@ -52,9 +49,10 @@ export class TestRunArguments {
5249
* Construct test item list from TestRequest
5350
* @returns list of test items to run and list of test for XCTest arguments
5451
*/
55-
private createTestLists(request: vscode.TestRunRequest): ProcessResult {
52+
private createTestLists(request: vscode.TestRunRequest, isDebug: boolean): ProcessResult {
5653
const includes = request.include ?? [];
57-
return includes.reduce(this.createTestItemReducer(request.include, request.exclude), {
54+
const excludes = request.exclude ?? [];
55+
return includes.reduce(this.createTestItemReducer(includes, excludes, isDebug), {
5856
testItems: this.createIncludeParentList(includes),
5957
xcTestArgs: [],
6058
swiftTestArgs: [],
@@ -78,15 +76,42 @@ export class TestRunArguments {
7876
return Array.from(parents.values());
7977
}
8078

79+
/**
80+
* Converts a list of TestItems to a regex test item ID. Depending on the TestItem's
81+
* tags and whether it is a debug run the ID is converted to a regex pattern that will
82+
* match the correct tests when passed to the `--filter` argument of `swift test`.
83+
*/
84+
private annotateTestArgs(testArgs: vscode.TestItem[], isDebug: boolean): string[] {
85+
return testArgs.map(arg => {
86+
const isTestTarget = !!arg.tags.find(tag => tag.id === "test-target");
87+
if (isTestTarget) {
88+
return `${arg.id}.*`;
89+
}
90+
const isXCTest = !!arg.tags.find(tag => tag.id === "XCTest");
91+
const hasChildren = arg.children.size > 0;
92+
if (isXCTest) {
93+
const terminator = hasChildren ? "/" : "$";
94+
// Debugging XCTests requires exact matches, so we don't need a trailing terminator.
95+
return isDebug ? arg.id : `${arg.id}${terminator}`;
96+
} else {
97+
// Append a trailing slash to match a suite name exactly.
98+
// This prevents TestTarget.MySuite matching TestTarget.MySuite2.
99+
return `${arg.id}/`;
100+
}
101+
});
102+
}
103+
81104
private createTestItemReducer(
82-
include: readonly vscode.TestItem[] | undefined,
83-
exclude: readonly vscode.TestItem[] | undefined
105+
include: readonly vscode.TestItem[],
106+
exclude: readonly vscode.TestItem[],
107+
isDebug: boolean
84108
): (previousValue: ProcessResult, testItem: vscode.TestItem) => ProcessResult {
85109
return (previousValue, testItem) => {
86110
const { testItems, swiftTestArgs, xcTestArgs } = this.processTestItem(
87111
testItem,
88112
include,
89-
exclude
113+
exclude,
114+
isDebug
90115
);
91116

92117
// If no children were added we can skip adding this parent.
@@ -119,63 +144,53 @@ export class TestRunArguments {
119144

120145
private itemContainsAllArgs(
121146
testItem: vscode.TestItem,
122-
xcTestArgs: string[],
123-
swiftTestArgs: string[]
147+
xcTestArgs: vscode.TestItem[],
148+
swiftTestArgs: vscode.TestItem[]
124149
): boolean {
125-
return xcTestArgs.length + swiftTestArgs.length === testItem.children.size;
150+
return (
151+
testItem.children.size > 0 &&
152+
xcTestArgs.length + swiftTestArgs.length === testItem.children.size
153+
);
126154
}
127155

128156
private simplifyTestArgs(
129157
testItem: vscode.TestItem,
130-
xcTestArgs: string[],
131-
swiftTestArgs: string[]
132-
): { xcTestResult: string[]; swiftTestResult: string[] } {
133-
if (
134-
testItem.parent &&
135-
this.itemContainsAllArgs(testItem.parent, xcTestArgs, swiftTestArgs)
136-
) {
137-
return this.simplifyTestArgs(testItem.parent, xcTestArgs, swiftTestArgs);
138-
} else {
139-
// If we've worked all the way up to a test target, it may have both swift-testing
140-
// and XCTests.
141-
const isTestTarget = !!testItem.tags.find(tag => tag.id === "test-target");
142-
if (isTestTarget) {
143-
return {
144-
// Add a trailing .* to match a test target name exactly.
145-
// This prevents TestTarget matching TestTarget2.
146-
xcTestResult: xcTestArgs.length > 0 ? [`${testItem.id}.*`] : [],
147-
swiftTestResult: swiftTestArgs.length > 0 ? [`${testItem.id}.*`] : [],
148-
};
149-
}
150-
151-
// If we've added all the children to the list of arguments, just add
152-
// the parent instead of each individual child. This crafts a minimal set
153-
// of test/suites that run all the test cases requested with the smallest list
154-
// of arguments. The testItem has to have a parent to perform this optimization.
155-
// If it does not we break the ability to run both swift testing tests and XCTests
156-
// in the same run, since test targets can have both types of tests in them.
157-
const isXCTest = !!testItem.tags.find(tag => tag.id === "XCTest");
158+
xcTestArgs: vscode.TestItem[],
159+
swiftTestArgs: vscode.TestItem[]
160+
): { xcTestResult: vscode.TestItem[]; swiftTestResult: vscode.TestItem[] } {
161+
// If we've worked all the way up to a test target, it may have both swift-testing
162+
// and XCTests.
163+
const isTestTarget = !!testItem.tags.find(tag => tag.id === "test-target");
164+
if (isTestTarget) {
158165
return {
159-
swiftTestResult: [
160-
// Append a trailing slash to match a suite name exactly.
161-
// This prevents TestTarget.MySuite matching TestTarget.MySuite2.
162-
...(!isXCTest ? [`${testItem.id}/`] : []),
163-
],
164-
xcTestResult: [
165-
// Debugging XCTests require exact matches, so we don't ned a trailing slash.
166-
...(isXCTest ? [this.isDebug ? testItem.id : `${testItem.id}/`] : []),
167-
],
166+
// Add a trailing .* to match a test target name exactly.
167+
// This prevents TestTarget matching TestTarget2.
168+
xcTestResult: xcTestArgs.length > 0 ? [testItem] : [],
169+
swiftTestResult: swiftTestArgs.length > 0 ? [testItem] : [],
168170
};
169171
}
172+
173+
// If we've added all the children to the list of arguments, just add
174+
// the parent instead of each individual child. This crafts a minimal set
175+
// of test/suites that run all the test cases requested with the smallest list
176+
// of arguments. The testItem has to have a parent to perform this optimization.
177+
// If it does not we break the ability to run both swift testing tests and XCTests
178+
// in the same run, since test targets can have both types of tests in them.
179+
const isXCTest = !!testItem.tags.find(tag => tag.id === "XCTest");
180+
return {
181+
xcTestResult: isXCTest ? [testItem] : [],
182+
swiftTestResult: !isXCTest ? [testItem] : [],
183+
};
170184
}
171185

172186
private processTestItem(
173187
testItem: vscode.TestItem,
174-
include?: readonly vscode.TestItem[],
175-
exclude?: readonly vscode.TestItem[]
188+
include: readonly vscode.TestItem[],
189+
exclude: readonly vscode.TestItem[],
190+
isDebug: boolean
176191
): ProcessResult {
177192
// Skip tests the user asked to exclude
178-
if (exclude?.includes(testItem)) {
193+
if (exclude.includes(testItem)) {
179194
return {
180195
testItems: [],
181196
xcTestArgs: [],
@@ -184,12 +199,13 @@ export class TestRunArguments {
184199
}
185200

186201
const testItems: vscode.TestItem[] = [];
187-
const xcTestArgs: string[] = [];
188-
const swiftTestArgs: string[] = [];
202+
const xcTestArgs: vscode.TestItem[] = [];
203+
const swiftTestArgs: vscode.TestItem[] = [];
189204

190205
// If this test item is included or we are including everything
191-
if (include?.includes(testItem) || !include) {
206+
if (include.includes(testItem) || include.length === 0) {
192207
const isXCTest = testItem.tags.find(tag => tag.id === "XCTest");
208+
const isSwiftTestingTest = testItem.tags.find(tag => tag.id === "swift-testing");
193209

194210
// Collect up a list of all the test items involved in the run
195211
// from the TestExplorer tree and store them in `testItems`. Exclude
@@ -200,20 +216,17 @@ export class TestRunArguments {
200216
// Only add leaf items to the list of arguments to pass to the test runner.
201217
if (this.isLeafTestItem(testItem, !!isXCTest)) {
202218
if (isXCTest) {
203-
// Tests in debug are specified exactly, otherwise
204-
// add the regex line ending character to avoid capturing
205-
// unwanted tests.
206-
xcTestArgs.push(this.isDebug ? testItem.id : `${testItem.id}$`);
207-
} else {
208-
swiftTestArgs.push(testItem.id);
219+
xcTestArgs.push(testItem);
220+
} else if (isSwiftTestingTest) {
221+
swiftTestArgs.push(testItem);
209222
}
210223
}
211224
}
212225
}
213226

214227
return reduceTestItemChildren(
215228
testItem.children,
216-
this.createTestItemReducer(undefined, exclude),
229+
this.createTestItemReducer([], exclude, isDebug),
217230
{
218231
testItems,
219232
xcTestArgs,

0 commit comments

Comments
 (0)