Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## {{releaseVersion}} - {{releaseDate}}

### Fixed

- Swift-testing test runs are marked as 'started' in the UI immediately, not after compilation finishes ([#2079](https://github.com/swiftlang/vscode-swift/pull/2079))

## 2.16.1 - 2026-02-02

### Fixed
Expand Down
18 changes: 5 additions & 13 deletions src/TestExplorer/TestParsers/SwiftTestingOutputParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,7 @@ export class SwiftTestingOutputParser {
private path?: string;

constructor(
public testRunStarted: () => void,
public addParameterizedTestCase: (testClass: TestClass, parentIndex: number) => void,
public addParameterizedTestCases: (testClasses: TestClass[], parentIndex: number) => void,
public onAttachment: (testIndex: number, path: string) => void
) {}

Expand Down Expand Up @@ -280,7 +279,6 @@ export class SwiftTestingOutputParser {
private handleEventRecord(payload: EventRecordPayload, runState: ITestRunState) {
switch (payload.kind) {
case "runStarted":
this.handleRunStarted();
break;
case "testStarted":
this.handleTestStarted(payload, runState);
Expand Down Expand Up @@ -322,9 +320,8 @@ export class SwiftTestingOutputParser {

const testIndex = this.testItemIndexFromTestID(item.payload.id, runState);
// If a test has test cases it is paramterized and we need to notify
// the caller that the TestClass should be added to the vscode.TestRun
// before it starts.
item.payload._testCases
// the caller that the TestClass should be added to the vscode.TestRun.
const parameterizedTestCases = item.payload._testCases
.map((testCase, index) =>
this.parameterizedFunctionTestCaseToTestClass(
item.payload.id,
Expand All @@ -337,14 +334,9 @@ export class SwiftTestingOutputParser {
index
)
)
.flatMap(testClass => (testClass ? [testClass] : []))
.forEach(testClass => this.addParameterizedTestCase(testClass, testIndex));
}
.flatMap(testClass => (testClass ? [testClass] : []));

private handleRunStarted() {
// Notify the runner that we've received all the test cases and
// are going to start running tests now.
this.testRunStarted();
this.addParameterizedTestCases(parameterizedTestCases, testIndex);
}

private handleTestStarted(payload: TestStarted, runState: ITestRunState) {
Expand Down
139 changes: 61 additions & 78 deletions src/TestExplorer/TestRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,20 +83,18 @@ export interface TestRunState {

export class TestRunProxy {
private testRun?: vscode.TestRun;
private addedTestItems: { testClass: TestClass; parentIndex: number }[] = [];
private runStarted: boolean = false;
private queuedOutput: string[] = [];
private _testItems: vscode.TestItem[];
private iteration: number | undefined;
private attachments: { [key: string]: string[] } = {};
private testItemFinder: TestItemFinder;
private testRunCompleteEmitter = new vscode.EventEmitter<void>();

public coverage: TestCoverage;
public token: CompositeCancellationToken;

public testRunCompleteEmitter = new vscode.EventEmitter<void>();
public onTestRunComplete: vscode.Event<void>;

// Allows for introspection on the state of TestItems after a test run.
public runState = TestRunProxy.initialTestRunState();

public static initialTestRunState(): TestRunState {
Expand All @@ -112,6 +110,7 @@ export class TestRunProxy {
};
}

/** The list of test items for this test run */
public get testItems(): vscode.TestItem[] {
return this._testItems;
}
Expand Down Expand Up @@ -143,17 +142,26 @@ export class TestRunProxy {
return;
}

this.resetTags(this.controller);
this.runStarted = true;
this.resetTags(this.controller);

this.testRun = this.controller.createTestRun(this.testRunRequest);
this.token.add(this.testRun.token);

// Forward any output captured before the testRun was created.
for (const outputLine of this.queuedOutput) {
this.performAppendOutput(outputLine);
}
this.queuedOutput = [];

// When a test run starts we need to do several things:
// - Create new TestItems for each paramterized test that was added
// and attach them to their parent TestItem.
// - Create a new test run from the TestRunArguments + newly created TestItems.
// - Mark all of these test items as enqueued on the test run.
for (const test of this.testItems) {
this.enqueued(test);
}
};

const addedTestItems = this.addedTestItems
.map(({ testClass, parentIndex }) => {
public addParameterizedTestCases = (testClasses: TestClass[], parentIndex: number) => {
const addedTestItems = testClasses
.map(testClass => {
const parent = this.args.testItems[parentIndex];
// clear out the children before we add the new ones.
parent.children.replace([]);
Expand Down Expand Up @@ -189,34 +197,17 @@ export class TestRunProxy {

return added;
});

this.testRun = this.controller.createTestRun(this.testRunRequest);
this.token.add(this.testRun.token);

const existingTestItemCount = this.testItems.length;
this._testItems = [...this.testItems, ...addedTestItems];

if (this._testItems.length !== existingTestItemCount) {
// Recreate a test item finder with the added test items
this.testItemFinder =
process.platform === "darwin"
? new DarwinTestItemFinder(this.testItems)
: new NonDarwinTestItemFinder(this.testItems, this.folderContext);
}

// Forward any output captured before the testRun was created.
for (const outputLine of this.queuedOutput) {
this.performAppendOutput(this.testRun, outputLine);
}
this.queuedOutput = [];

for (const test of this.testItems) {
for (const test of addedTestItems) {
this.enqueued(test);
}
};

public addParameterizedTestCase = (testClass: TestClass, parentIndex: number) => {
this.addedTestItems.push({ testClass, parentIndex });
// Recreate a test item finder with the added test items
this.testItemFinder =
process.platform === "darwin"
? new DarwinTestItemFinder(this.testItems)
: new NonDarwinTestItemFinder(this.testItems, this.folderContext);
};

public addAttachment = (testIndex: number, attachment: string) => {
Expand Down Expand Up @@ -359,28 +350,50 @@ export class TestRunProxy {
this.runState = TestRunProxy.initialTestRunState();
this.iteration = iteration;
if (this.testRun) {
this.performAppendOutput(this.testRun, "\n\r");
this.performAppendOutput("\n\r");
}
}

public appendOutput(output: string) {
const tranformedOutput = this.prependIterationToOutput(output);
if (this.testRun) {
this.performAppendOutput(this.testRun, tranformedOutput);
} else {
this.queuedOutput.push(tranformedOutput);
public async computeCoverage() {
if (!this.testRun) {
return;
}

// Compute final coverage numbers if any coverage info has been captured during the run.
await this.coverage.computeCoverage(this.testRun);
}

public appendOutput(output: string) {
this.performAppendOutput(output);
}

public appendOutputToTest(output: string, test: vscode.TestItem, location?: vscode.Location) {
this.performAppendOutput(output, test, location);
}

private performAppendOutput(
output: string,
test?: vscode.TestItem,
location?: vscode.Location
) {
const tranformedOutput = this.prependIterationToOutput(output);
if (this.testRun) {
this.performAppendOutput(this.testRun, tranformedOutput, location, test);
this.testRun.appendOutput(output, location, test);
this.runState.output.push(stripAnsi(output));
} else {
this.queuedOutput.push(tranformedOutput);
}
}

private prependIterationToOutput(output: string): string {
if (this.iteration === undefined) {
return output;
}
const itr = this.iteration + 1;
const lines = output.match(/[^\r\n]*[\r\n]*/g);
return lines?.map(line => (line ? `\x1b[34mRun ${itr}\x1b[0m ${line}` : "")).join("") ?? "";
}

private reportAttachments() {
const attachmentKeys = Object.keys(this.attachments);
if (attachmentKeys.length > 0) {
Expand All @@ -400,34 +413,6 @@ export class TestRunProxy {
}
}

private performAppendOutput(
testRun: vscode.TestRun,
output: string,
location?: vscode.Location,
test?: vscode.TestItem
) {
testRun.appendOutput(output, location, test);
this.runState.output.push(stripAnsi(output));
}

private prependIterationToOutput(output: string): string {
if (this.iteration === undefined) {
return output;
}
const itr = this.iteration + 1;
const lines = output.match(/[^\r\n]*[\r\n]*/g);
return lines?.map(line => (line ? `\x1b[34mRun ${itr}\x1b[0m ${line}` : "")).join("") ?? "";
}

public async computeCoverage() {
if (!this.testRun) {
return;
}

// Compute final coverage numbers if any coverage info has been captured during the run.
await this.coverage.computeCoverage(this.testRun);
}

static Tags = {
SKIPPED: "skipped",
HAS_ATTACHMENT: "hasAttachment",
Expand Down Expand Up @@ -486,8 +471,7 @@ export class TestRunner {
)
: new XCTestOutputParser();
this.swiftTestOutputParser = new SwiftTestingOutputParser(
this.testRun.testRunStarted,
this.testRun.addParameterizedTestCase,
this.testRun.addParameterizedTestCases,
this.testRun.addAttachment
);
this.onDebugSessionTerminated = this.debugSessionTerminatedEmitter.event;
Expand All @@ -501,8 +485,7 @@ export class TestRunner {
public setIteration(iteration: number) {
// The SwiftTestingOutputParser holds state and needs to be reset between iterations.
this.swiftTestOutputParser = new SwiftTestingOutputParser(
this.testRun.testRunStarted,
this.testRun.addParameterizedTestCase,
this.testRun.addParameterizedTestCases,
this.testRun.addAttachment
);
this.testRun.setIteration(iteration);
Expand Down Expand Up @@ -830,6 +813,8 @@ export class TestRunner {
// The await simply waits for the watching to be configured.
await this.swiftTestOutputParser.watch(fifoPipePath, runState);

this.testRun.testRunStarted();

await this.launchTests(
runState,
this.testKind === TestKind.parallel ? TestKind.standard : this.testKind,
Expand Down Expand Up @@ -858,7 +843,6 @@ export class TestRunner {
return this.testRun.runState;
}

// XCTestRuns are started immediately
this.testRun.testRunStarted();

await this.launchTests(
Expand Down Expand Up @@ -1238,9 +1222,8 @@ export class TestRunner {
fifoPipePath,
runState
);
} else if (config.testType === TestLibrary.xctest) {
this.testRun.testRunStarted();
}
this.testRun.testRunStarted();

this.workspaceContext.logger.debug(
"Start Test Debugging",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ suite("SwiftTestingOutputParser Suite", () => {

beforeEach(() => {
outputParser = new SwiftTestingOutputParser(
() => {},
() => {},
() => {}
);
Expand Down Expand Up @@ -234,13 +233,14 @@ suite("SwiftTestingOutputParser Suite", () => {
]);

const outputParser = new SwiftTestingOutputParser(
() => {},
testClass => {
testRunState.testItemFinder.tests.push({
name: testClass.id,
status: TestStatus.enqueued,
output: [],
});
testClasses => {
testClasses.forEach(testClass =>
testRunState.testItemFinder.tests.push({
name: testClass.id,
status: TestStatus.enqueued,
output: [],
})
);
},
() => {}
);
Expand Down