Skip to content

Commit b87f8f3

Browse files
authored
Explicitly activate/deactivate extension for cleaner integration test runs (#1194)
* Explicitly activate/deactivate extension to ensure cleaner test runs The `globalWorkspaceContextPromise` would activate the extension once and then always return the same workspace context for every test run. Its a limitation that we cannot actually activate the extension multiple times in tests (there is no extension deactivate API offered by VS Code), however we can dispose and recreate the WorkspaceContext at will to ensure a cleaner test environment. `activateExtension` will call `extension.activate()` the first time its used, and then `deactivateExtension` will dispose of the returned WorkspaceContext. Subsequent calls to `activateExtension` will use the activation method returned by the Swift extension's entrypoint API to recreate the `WorkspaceContext`. If `activateExtension` is called again without a matching call to `deactivateExtension` an error is thrown.
1 parent 88e6968 commit b87f8f3

27 files changed

+611
-258
lines changed

assets/test/.vscode/launch.json

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
{
44
"type": "swift-lldb",
55
"request": "launch",
6-
"sourceLanguages": [
7-
"swift"
8-
],
96
"name": "Debug PackageExe (defaultPackage)",
107
"program": "${workspaceFolder:test}/defaultPackage/.build/debug/PackageExe",
118
"args": [],
@@ -15,9 +12,6 @@
1512
{
1613
"type": "swift-lldb",
1714
"request": "launch",
18-
"sourceLanguages": [
19-
"swift"
20-
],
2115
"name": "Release PackageExe (defaultPackage)",
2216
"program": "${workspaceFolder:test}/defaultPackage/.build/release/PackageExe",
2317
"args": [],

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1275,7 +1275,7 @@
12751275
"pretest": "npm run compile-tests",
12761276
"soundness": "docker compose -f docker/docker-compose.yaml -p swift-vscode-soundness-prb run --rm soundness",
12771277
"test-soundness": "scripts/soundness.sh",
1278-
"test": "vscode-test",
1278+
"test": "VSCODE_TEST=1 vscode-test",
12791279
"test-ci": "docker/test-ci.sh ci",
12801280
"test-nightly": "docker/test-ci.sh nightly",
12811281
"integration-test": "npm test -- --label integrationTests",

src/TestExplorer/TestExplorer.ts

Lines changed: 75 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export class TestExplorer {
3737
private lspTestDiscovery: LSPTestDiscovery;
3838
private subscriptions: { dispose(): unknown }[];
3939
private testFileEdited = true;
40+
private tokenSource = new vscode.CancellationTokenSource();
4041

4142
// Emits after the `vscode.TestController` has been updated.
4243
private onTestItemsDidChangeEmitter = new vscode.EventEmitter<vscode.TestController>();
@@ -56,7 +57,7 @@ export class TestExplorer {
5657

5758
this.controller.resolveHandler = async item => {
5859
if (!item) {
59-
await this.discoverTestsInWorkspace();
60+
await this.discoverTestsInWorkspace(this.tokenSource.token);
6061
}
6162
};
6263

@@ -86,7 +87,7 @@ export class TestExplorer {
8687
this.testFileEdited = false;
8788
// only run discover tests if the library has tests
8889
if (this.folderContext.swiftPackage.getTargets(TargetType.test).length > 0) {
89-
this.discoverTestsInWorkspace();
90+
this.discoverTestsInWorkspace(this.tokenSource.token);
9091
}
9192
}
9293
});
@@ -99,6 +100,7 @@ export class TestExplorer {
99100
});
100101

101102
this.subscriptions = [
103+
this.tokenSource,
102104
fileWatcher,
103105
onDidEndTask,
104106
this.controller,
@@ -110,6 +112,9 @@ export class TestExplorer {
110112
}
111113

112114
dispose() {
115+
this.controller.refreshHandler = undefined;
116+
this.controller.resolveHandler = undefined;
117+
this.tokenSource.cancel();
113118
this.subscriptions.forEach(element => element.dispose());
114119
}
115120

@@ -120,47 +125,64 @@ export class TestExplorer {
120125
* @returns Observer disposable
121126
*/
122127
static observeFolders(workspaceContext: WorkspaceContext): vscode.Disposable {
123-
return workspaceContext.onDidChangeFolders(({ folder, operation, workspace }) => {
124-
switch (operation) {
125-
case FolderOperation.add:
126-
if (folder) {
127-
if (folder.swiftPackage.getTargets(TargetType.test).length > 0) {
128-
folder.addTestExplorer();
129-
// discover tests in workspace but only if disableAutoResolve is not on.
130-
// discover tests will kick off a resolve if required
131-
if (!configuration.folder(folder.workspaceFolder).disableAutoResolve) {
132-
folder.testExplorer?.discoverTestsInWorkspace();
128+
const tokenSource = new vscode.CancellationTokenSource();
129+
const disposable = workspaceContext.onDidChangeFolders(
130+
({ folder, operation, workspace }) => {
131+
switch (operation) {
132+
case FolderOperation.add:
133+
if (folder) {
134+
if (folder.swiftPackage.getTargets(TargetType.test).length > 0) {
135+
folder.addTestExplorer();
136+
// discover tests in workspace but only if disableAutoResolve is not on.
137+
// discover tests will kick off a resolve if required
138+
if (
139+
!configuration.folder(folder.workspaceFolder).disableAutoResolve
140+
) {
141+
folder.testExplorer?.discoverTestsInWorkspace(
142+
tokenSource.token
143+
);
144+
}
133145
}
134146
}
135-
}
136-
break;
137-
case FolderOperation.packageUpdated:
138-
if (folder) {
139-
const hasTestTargets =
140-
folder.swiftPackage.getTargets(TargetType.test).length > 0;
141-
if (hasTestTargets && !folder.hasTestExplorer()) {
142-
folder.addTestExplorer();
143-
// discover tests in workspace but only if disableAutoResolve is not on.
144-
// discover tests will kick off a resolve if required
145-
if (!configuration.folder(folder.workspaceFolder).disableAutoResolve) {
146-
folder.testExplorer?.discoverTestsInWorkspace();
147+
break;
148+
case FolderOperation.packageUpdated:
149+
if (folder) {
150+
const hasTestTargets =
151+
folder.swiftPackage.getTargets(TargetType.test).length > 0;
152+
if (hasTestTargets && !folder.hasTestExplorer()) {
153+
folder.addTestExplorer();
154+
// discover tests in workspace but only if disableAutoResolve is not on.
155+
// discover tests will kick off a resolve if required
156+
if (
157+
!configuration.folder(folder.workspaceFolder).disableAutoResolve
158+
) {
159+
folder.testExplorer?.discoverTestsInWorkspace(
160+
tokenSource.token
161+
);
162+
}
163+
} else if (!hasTestTargets && folder.hasTestExplorer()) {
164+
folder.removeTestExplorer();
165+
} else if (folder.hasTestExplorer()) {
166+
folder.refreshTestExplorer();
147167
}
148-
} else if (!hasTestTargets && folder.hasTestExplorer()) {
149-
folder.removeTestExplorer();
150-
} else if (folder.hasTestExplorer()) {
151-
folder.refreshTestExplorer();
152168
}
153-
}
154-
break;
155-
case FolderOperation.focus:
156-
if (folder) {
157-
workspace.languageClientManager.documentSymbolWatcher = (
158-
document,
159-
symbols
160-
) => TestExplorer.onDocumentSymbols(folder, document, symbols);
161-
}
169+
break;
170+
case FolderOperation.focus:
171+
if (folder) {
172+
workspace.languageClientManager.documentSymbolWatcher = (
173+
document,
174+
symbols
175+
) => TestExplorer.onDocumentSymbols(folder, document, symbols);
176+
}
177+
}
162178
}
163-
});
179+
);
180+
return {
181+
dispose: () => {
182+
tokenSource.dispose();
183+
disposable.dispose();
184+
},
185+
};
164186
}
165187

166188
/**
@@ -224,25 +246,25 @@ export class TestExplorer {
224246
/**
225247
* Discover tests
226248
*/
227-
async discoverTestsInWorkspace() {
249+
async discoverTestsInWorkspace(token: vscode.CancellationToken) {
228250
try {
229251
// If the LSP cannot produce a list of tests it throws and
230252
// we fall back to discovering tests with SPM.
231-
await this.discoverTestsInWorkspaceLSP();
253+
await this.discoverTestsInWorkspaceLSP(token);
232254
} catch {
233255
this.folderContext.workspaceContext.outputChannel.logDiagnostic(
234256
"workspace/tests LSP request not supported, falling back to SPM to discover tests.",
235257
"Test Discovery"
236258
);
237-
await this.discoverTestsInWorkspaceSPM();
259+
await this.discoverTestsInWorkspaceSPM(token);
238260
}
239261
}
240262

241263
/**
242264
* Discover tests
243265
* Uses `swift test --list-tests` to get the list of tests
244266
*/
245-
async discoverTestsInWorkspaceSPM() {
267+
async discoverTestsInWorkspaceSPM(token: vscode.CancellationToken) {
246268
async function runDiscover(explorer: TestExplorer, firstTry: boolean) {
247269
try {
248270
const toolchain = explorer.folderContext.workspaceContext.toolchain;
@@ -263,6 +285,11 @@ export class TestExplorer {
263285
return;
264286
}
265287
}
288+
289+
if (token.isCancellationRequested) {
290+
return;
291+
}
292+
266293
// get list of tests from `swift test --list-tests`
267294
let listTestArguments: string[];
268295
if (toolchain.swiftVersion.isGreaterThanOrEqual(new Version(5, 8, 0))) {
@@ -284,7 +311,7 @@ export class TestExplorer {
284311
explorer.updateTests(explorer.controller, tests);
285312
}
286313
);
287-
await explorer.folderContext.taskQueue.queueOperation(listTestsOperation);
314+
await explorer.folderContext.taskQueue.queueOperation(listTestsOperation, token);
288315
} catch (error) {
289316
// If a test list fails its possible the tests have not been built.
290317
// Build them and try again, and if we still fail then notify the user.
@@ -336,10 +363,14 @@ export class TestExplorer {
336363
/**
337364
* Discover tests
338365
*/
339-
async discoverTestsInWorkspaceLSP() {
366+
async discoverTestsInWorkspaceLSP(token: vscode.CancellationToken) {
340367
const tests = await this.lspTestDiscovery.getWorkspaceTests(
341368
this.folderContext.swiftPackage
342369
);
370+
if (token.isCancellationRequested) {
371+
return;
372+
}
373+
343374
TestDiscovery.updateTestsFromClasses(
344375
this.controller,
345376
this.folderContext.swiftPackage,

src/WorkspaceContext.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,9 @@ export class WorkspaceContext implements vscode.Disposable {
173173

174174
dispose() {
175175
this.folders.forEach(f => f.dispose());
176+
this.folders.length = 0;
176177
this.subscriptions.forEach(item => item.dispose());
178+
this.subscriptions.length = 0;
177179
}
178180

179181
get swiftVersion() {
@@ -388,7 +390,7 @@ export class WorkspaceContext implements vscode.Disposable {
388390
* @param folder folder being removed
389391
*/
390392
async removeWorkspaceFolder(workspaceFolder: vscode.WorkspaceFolder) {
391-
this.folders.forEach(async folder => {
393+
for (const folder of this.folders) {
392394
if (folder.workspaceFolder !== workspaceFolder) {
393395
return;
394396
}
@@ -404,7 +406,7 @@ export class WorkspaceContext implements vscode.Disposable {
404406
await observer({ folder, operation: FolderOperation.remove, workspace: this });
405407
}
406408
folder.dispose();
407-
});
409+
}
408410
this.folders = this.folders.filter(folder => folder.workspaceFolder !== workspaceFolder);
409411
}
410412

src/extension.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,23 @@ import { resolveFolderDependencies } from "./commands/dependencies/resolve";
4444
* or by the integration test runner for VS Code extensions.
4545
*/
4646
export interface Api {
47-
workspaceContext: WorkspaceContext;
47+
workspaceContext?: WorkspaceContext;
48+
outputChannel: SwiftOutputChannel;
49+
activate(): Promise<Api>;
50+
deactivate(): void;
4851
}
4952

5053
/**
5154
* Activate the extension. This is the main entry point.
5255
*/
53-
export async function activate(context: vscode.ExtensionContext): Promise<Api | undefined> {
56+
export async function activate(context: vscode.ExtensionContext): Promise<Api> {
5457
try {
55-
console.debug("Activating Swift for Visual Studio Code...");
56-
const outputChannel = new SwiftOutputChannel("Swift");
58+
const outputChannel = new SwiftOutputChannel("Swift", !process.env["VSCODE_TEST"]);
59+
outputChannel.log("Activating Swift for Visual Studio Code...");
5760

5861
checkAndWarnAboutWindowsSymlinks(outputChannel);
5962

63+
context.subscriptions.push(outputChannel);
6064
context.subscriptions.push(new SwiftEnvironmentVariablesManager(context));
6165
context.subscriptions.push(
6266
vscode.window.registerTerminalProfileProvider(
@@ -106,7 +110,12 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api |
106110

107111
if (!toolchain) {
108112
showToolchainError();
109-
return;
113+
return {
114+
workspaceContext: undefined,
115+
outputChannel,
116+
activate: () => activate(context),
117+
deactivate: () => deactivate(context),
118+
};
110119
}
111120

112121
const workspaceContext = await WorkspaceContext.create(outputChannel, toolchain);
@@ -237,13 +246,19 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api |
237246
logObserver,
238247
languageStatusItem,
239248
pluginTaskProvider,
240-
taskProvider
249+
taskProvider,
250+
workspaceContext
241251
);
242252

243253
// Mark the extension as activated.
244254
contextKeys.isActivated = true;
245255

246-
return { workspaceContext };
256+
return {
257+
workspaceContext,
258+
outputChannel,
259+
activate: () => activate(context),
260+
deactivate: () => deactivate(context),
261+
};
247262
} catch (error) {
248263
const errorMessage = getErrorDescription(error);
249264
// show this error message as the VS Code error message only shows when running
@@ -252,3 +267,10 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api |
252267
throw error;
253268
}
254269
}
270+
271+
async function deactivate(context: vscode.ExtensionContext): Promise<void> {
272+
console.debug("Deactivating Swift for Visual Studio Code...");
273+
contextKeys.isActivated = false;
274+
context.subscriptions.forEach(subscription => subscription.dispose());
275+
context.subscriptions.length = 0;
276+
}

src/tasks/TaskQueue.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ export class TaskOperation implements SwiftOperation {
8585
workspaceContext: WorkspaceContext,
8686
token?: vscode.CancellationToken
8787
): Promise<number | undefined> {
88+
if (token?.isCancellationRequested) {
89+
return Promise.resolve(undefined);
90+
}
8891
workspaceContext.outputChannel.log(`Exec Task: ${this.task.detail ?? this.task.name}`);
8992
return workspaceContext.tasks.executeTaskAndWait(this.task, token);
9093
}
@@ -230,6 +233,7 @@ export class TaskQueue {
230233
if (!this.activeOperation) {
231234
// get task from queue
232235
const operation = this.queue.shift();
236+
233237
if (operation) {
234238
//const task = operation.task;
235239
this.activeOperation = operation;
@@ -250,20 +254,14 @@ export class TaskQueue {
250254
.run(this.workspaceContext)
251255
.then(result => {
252256
// log result
253-
if (operation.log) {
257+
if (operation.log && !operation.token?.isCancellationRequested) {
254258
switch (result) {
255259
case 0:
256260
this.workspaceContext.outputChannel.log(
257261
`${operation.log}: ... done.`,
258262
this.folderContext.name
259263
);
260264
break;
261-
case undefined:
262-
this.workspaceContext.outputChannel.log(
263-
`${operation.log}: ... cancelled.`,
264-
this.folderContext.name
265-
);
266-
break;
267265
default:
268266
this.workspaceContext.outputChannel.log(
269267
`${operation.log}: ... failed.`,

0 commit comments

Comments
 (0)