Skip to content

Commit c095a6c

Browse files
committed
Add "Run Test Multiple Times"
Adds two new items to the context menu when you right click a test in the test explorer: - Run Multiple Times - Run Until Failure Selecting either of these promts with a text input where the user can input how many times they want to run the test(s). If the user selected Run Until Failure the tests will be run a maximum number of times, stopping at the first iteration that produces a failure. A current limitation in VS Code is if you have multiple tests selected in the Test Explorer (by shift or ctrl clicking them) only the first is passed to the command. Issue: swiftlang#832
1 parent 6acaab7 commit c095a6c

File tree

5 files changed

+230
-30
lines changed

5 files changed

+230
-30
lines changed

package.json

+22
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,16 @@
201201
"command": "swift.clearDiagnosticsCollection",
202202
"title": "Clear Diagnostics Collection",
203203
"category": "Swift"
204+
},
205+
{
206+
"command": "swift.runTestsMultipleTimes",
207+
"title": "Run Multiple Times...",
208+
"category": "Swift"
209+
},
210+
{
211+
"command": "swift.runTestsUntilFailure",
212+
"title": "Run Until Failure...",
213+
"category": "Swift"
204214
}
205215
],
206216
"configuration": [
@@ -598,6 +608,18 @@
598608
}
599609
],
600610
"menus": {
611+
"testing/item/context": [
612+
{
613+
"command": "swift.runTestsMultipleTimes",
614+
"group": "testExtras",
615+
"when": "testId"
616+
},
617+
{
618+
"command": "swift.runTestsUntilFailure",
619+
"group": "testExtras",
620+
"when": "testId"
621+
}
622+
],
601623
"commandPalette": [
602624
{
603625
"command": "swift.createNewProject",

src/TestExplorer/TestExplorer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class TestExplorer {
4141
private onTestItemsDidChangeEmitter = new vscode.EventEmitter<vscode.TestController>();
4242
public onTestItemsDidChange: vscode.Event<vscode.TestController>;
4343

44-
private onDidCreateTestRunEmitter = new vscode.EventEmitter<TestRunProxy>();
44+
public onDidCreateTestRunEmitter = new vscode.EventEmitter<TestRunProxy>();
4545
public onCreateTestRun: vscode.Event<TestRunProxy>;
4646

4747
constructor(public folderContext: FolderContext) {

src/TestExplorer/TestRunner.ts

+70-19
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,28 @@ export class TestRunProxy {
5252
private runStarted: boolean = false;
5353
private queuedOutput: string[] = [];
5454
private _testItems: vscode.TestItem[];
55+
private iteration: number | undefined;
5556
public coverage: TestCoverage;
5657

58+
public testRunCompleteEmitter = new vscode.EventEmitter<void>();
59+
public onTestRunComplete: vscode.Event<void>;
60+
5761
// Allows for introspection on the state of TestItems after a test run.
58-
public runState = {
59-
failed: [] as {
60-
test: vscode.TestItem;
61-
message: vscode.TestMessage | readonly vscode.TestMessage[];
62-
}[],
63-
passed: [] as vscode.TestItem[],
64-
skipped: [] as vscode.TestItem[],
65-
errored: [] as vscode.TestItem[],
66-
unknown: 0,
67-
output: [] as string[],
68-
};
62+
public runState = TestRunProxy.initialTestRunState();
63+
64+
private static initialTestRunState() {
65+
return {
66+
failed: [] as {
67+
test: vscode.TestItem;
68+
message: vscode.TestMessage | readonly vscode.TestMessage[];
69+
}[],
70+
passed: [] as vscode.TestItem[],
71+
skipped: [] as vscode.TestItem[],
72+
errored: [] as vscode.TestItem[],
73+
unknown: 0,
74+
output: [] as string[],
75+
};
76+
}
6977

7078
public get testItems(): vscode.TestItem[] {
7179
return this._testItems;
@@ -79,6 +87,7 @@ export class TestRunProxy {
7987
) {
8088
this._testItems = args.testItems;
8189
this.coverage = new TestCoverage(folderContext);
90+
this.onTestRunComplete = this.testRunCompleteEmitter.event;
8291
}
8392

8493
public testRunStarted = () => {
@@ -194,20 +203,37 @@ export class TestRunProxy {
194203

195204
public async end() {
196205
this.testRun?.end();
206+
this.testRunCompleteEmitter.fire();
207+
}
208+
209+
public setIteration(iteration: number) {
210+
this.runState = TestRunProxy.initialTestRunState();
211+
this.iteration = iteration;
197212
}
198213

199214
public appendOutput(output: string) {
215+
const tranformedOutput = this.prependIterationToOutput(output);
200216
if (this.testRun) {
201-
this.testRun.appendOutput(output);
202-
this.runState.output.push(output);
217+
this.testRun.appendOutput(tranformedOutput);
218+
this.runState.output.push(tranformedOutput);
203219
} else {
204-
this.queuedOutput.push(output);
220+
this.queuedOutput.push(tranformedOutput);
205221
}
206222
}
207223

208224
public appendOutputToTest(output: string, test: vscode.TestItem, location?: vscode.Location) {
209-
this.testRun?.appendOutput(output, location, test);
210-
this.runState.output.push(output);
225+
const tranformedOutput = this.prependIterationToOutput(output);
226+
this.testRun?.appendOutput(tranformedOutput, location, test);
227+
this.runState.output.push(tranformedOutput);
228+
}
229+
230+
private prependIterationToOutput(output: string): string {
231+
if (this.iteration === undefined) {
232+
return output;
233+
}
234+
const itr = this.iteration + 1;
235+
const lines = output.match(/[^\r\n]*[\r\n]*/g);
236+
return lines?.map(line => (line ? `\x1b[34mRun ${itr}\x1b[0m ${line}` : "")).join("") ?? "";
211237
}
212238

213239
public async computeCoverage() {
@@ -222,7 +248,7 @@ export class TestRunProxy {
222248

223249
/** Class used to run tests */
224250
export class TestRunner {
225-
private testRun: TestRunProxy;
251+
public testRun: TestRunProxy;
226252
private testArgs: TestRunArguments;
227253
private xcTestOutputParser: IXCTestOutputParser;
228254
private swiftTestOutputParser: SwiftTestingOutputParser;
@@ -256,6 +282,20 @@ export class TestRunner {
256282
);
257283
}
258284

285+
/**
286+
* When performing a "Run test multiple times" run set the iteration
287+
* so it can be shown in the logs.
288+
* @param iteration The iteration counter
289+
*/
290+
public setIteration(iteration: number) {
291+
// The SwiftTestingOutputParser holds state and needs to be reset between iterations.
292+
this.swiftTestOutputParser = new SwiftTestingOutputParser(
293+
this.testRun.testRunStarted,
294+
this.testRun.addParameterizedTestCase
295+
);
296+
this.testRun.setIteration(iteration);
297+
}
298+
259299
/**
260300
* If the request has no test items to include in the run,
261301
* default to usig all the items in the `TestController`.
@@ -587,6 +627,7 @@ export class TestRunner {
587627
task.execution.onDidWrite(str => {
588628
const replaced = str
589629
.replace("[1/1] Planning build", "") // Work around SPM still emitting progress when doing --no-build.
630+
.replace(/\[1\/1\] Write swift-version-.*/gm, "")
590631
.replace(
591632
/LLVM Profile Error: Failed to write file "default.profraw": Operation not permitted\r\n/gm,
592633
""
@@ -1051,7 +1092,7 @@ export class TestRunnerTestRunState implements ITestRunState {
10511092
}
10521093

10531094
const isSuite = test.children.size > 0;
1054-
const issues = isSuite ? this.childrensIssues(test) : this.issues.get(index) ?? [];
1095+
const issues = isSuite ? this.childrensIssues(test) : this.pluckIssues(index) ?? [];
10551096
if (issues.length > 0) {
10561097
const allUnknownIssues = issues.filter(({ isKnown }) => !isKnown);
10571098
if (allUnknownIssues.length === 0) {
@@ -1076,14 +1117,24 @@ export class TestRunnerTestRunState implements ITestRunState {
10761117
this.currentTestItem = undefined;
10771118
}
10781119

1120+
// Removes and returns the issues at the supplied test index.
1121+
private pluckIssues(index: number): {
1122+
isKnown: boolean;
1123+
message: vscode.TestMessage;
1124+
}[] {
1125+
const issues = this.issues.get(index) ?? [];
1126+
this.issues.delete(index);
1127+
return issues;
1128+
}
1129+
10791130
// Gather the issues of test children into a flat collection.
10801131
private childrensIssues(test: vscode.TestItem): {
10811132
isKnown: boolean;
10821133
message: vscode.TestMessage;
10831134
}[] {
10841135
const index = this.getTestItemIndex(test.id);
10851136
return [
1086-
...(this.issues.get(index) ?? []),
1137+
...(this.pluckIssues(index) ?? []),
10871138
...reduceTestItemChildren(
10881139
test.children,
10891140
(acc, test) => [

src/commands.ts

+56
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import { SwiftProjectTemplate } from "./toolchain/toolchain";
3333
import { showToolchainSelectionQuickPick, showToolchainError } from "./ui/ToolchainSelection";
3434
import { captureDiagnostics } from "./commands/captureDiagnostics";
3535
import { reindexProjectRequest } from "./sourcekit-lsp/lspExtensions";
36+
import { TestRunner, TestRunnerTestRunState } from "./TestExplorer/TestRunner";
37+
import { TestKind } from "./TestExplorer/TestKind";
3638

3739
/**
3840
* References:
@@ -734,6 +736,54 @@ function openInExternalEditor(packageNode: PackageNode) {
734736
}
735737
}
736738

739+
async function runTestMultipleTimes(
740+
ctx: WorkspaceContext,
741+
test: vscode.TestItem,
742+
untilFailure: boolean
743+
) {
744+
const str = await vscode.window.showInputBox({
745+
prompt: "Label: ",
746+
placeHolder: `${untilFailure ? "Maximum " : ""}# of times to run`,
747+
validateInput: value => (/^[1-9]\d*$/.test(value) ? undefined : "Enter an integer value"),
748+
});
749+
750+
if (!str || !ctx.currentFolder?.testExplorer) {
751+
return;
752+
}
753+
754+
const numExecutions = parseInt(str);
755+
const testExplorer = ctx.currentFolder.testExplorer;
756+
const runner = new TestRunner(
757+
TestKind.standard,
758+
new vscode.TestRunRequest([test]),
759+
ctx.currentFolder,
760+
testExplorer.controller
761+
);
762+
763+
testExplorer.onDidCreateTestRunEmitter.fire(runner.testRun);
764+
765+
const testRunState = new TestRunnerTestRunState(runner.testRun);
766+
const token = new vscode.CancellationTokenSource();
767+
768+
vscode.commands.executeCommand("workbench.panel.testResults.view.focus");
769+
770+
for (let i = 0; i < numExecutions; i++) {
771+
runner.setIteration(i);
772+
runner.testRun.appendOutput(`\x1b[36mBeginning Test Iteration #${i + 1}\x1b[0m\n`);
773+
774+
await runner.runSession(token.token, testRunState);
775+
776+
if (
777+
untilFailure &&
778+
(runner.testRun.runState.failed.length > 0 ||
779+
runner.testRun.runState.errored.length > 0)
780+
) {
781+
break;
782+
}
783+
}
784+
runner.testRun.end();
785+
}
786+
737787
/**
738788
* Switches the target SDK to the platform selected in a QuickPick UI.
739789
*/
@@ -841,6 +891,12 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] {
841891
vscode.commands.registerCommand("swift.run", () => runBuild(ctx)),
842892
vscode.commands.registerCommand("swift.debug", () => debugBuild(ctx)),
843893
vscode.commands.registerCommand("swift.cleanBuild", () => cleanBuild(ctx)),
894+
vscode.commands.registerCommand("swift.runTestsMultipleTimes", item =>
895+
runTestMultipleTimes(ctx, item, false)
896+
),
897+
vscode.commands.registerCommand("swift.runTestsUntilFailure", item =>
898+
runTestMultipleTimes(ctx, item, true)
899+
),
844900
// Note: This is only available on macOS (gated in `package.json`) because its the only OS that has the iOS SDK available.
845901
vscode.commands.registerCommand("swift.switchPlatform", () => switchPlatform()),
846902
vscode.commands.registerCommand("swift.resetPackage", () => resetPackage(ctx)),

0 commit comments

Comments
 (0)