Skip to content

Commit 90251e3

Browse files
authoredJul 25, 2024
Test strategy proposal (#960)
* Propose levels of testing and the test matrix * Propose a single mocking library to introduce unit testing * Add test strategy documentation
1 parent 058e1a6 commit 90251e3

File tree

9 files changed

+683
-0
lines changed

9 files changed

+683
-0
lines changed
 

Diff for: ‎.vscode-test.js

+15
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ module.exports = defineConfig({
3333
mocha: {
3434
ui: "tdd",
3535
color: true,
36+
// so doesn't timeout when breakpoint is hit
37+
timeout: isDebugRun ? Number.MAX_SAFE_INTEGER : 3000,
3638
timeout,
3739
forbidOnly: isCIBuild,
3840
grep: isFastTestRun ? "@slow" : undefined,
@@ -41,6 +43,19 @@ module.exports = defineConfig({
4143
installExtensions: ["vadimcn.vscode-lldb"],
4244
reuseMachineInstall: !isCIBuild,
4345
},
46+
{
47+
label: "unitTests",
48+
files: ["out/test/unit-tests/**/*.test.js"],
49+
version: "stable",
50+
mocha: {
51+
ui: "tdd",
52+
color: true,
53+
// so doesn't timeout when breakpoint is hit
54+
timeout: isDebugRun ? Number.MAX_SAFE_INTEGER : 3000,
55+
forbidOnly: isCIBuild,
56+
},
57+
reuseMachineInstall: !isCIBuild,
58+
},
4459
// you can specify additional test configurations, too
4560
],
4661
coverage: {

Diff for: ‎.vscode/launch.json

+11
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@
3737
"VSCODE_DEBUG": "1"
3838
},
3939
"preLaunchTask": "compile-tests"
40+
},
41+
{
42+
"name": "Unit Tests",
43+
"type": "extensionHost",
44+
"request": "launch",
45+
"testConfiguration": "${workspaceFolder}/.vscode-test.js",
46+
"testConfigurationLabel": "unitTests",
47+
"outFiles": [
48+
"${workspaceFolder}/out/**/*.js"
49+
],
50+
"preLaunchTask": "compile-tests"
4051
}
4152
]
4253
}

Diff for: ‎CONTRIBUTING.md

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ Please keep your PRs to a minimal number of changes. If a PR is large, try to sp
7676

7777
Where possible any new feature should have tests that go along with it, to ensure it works and will continue to work in the future. When a PR is submitted one of the prerequisites for it to be merged is that all tests pass.
7878

79+
For information on levels of testing done in this extension, see the [test strategy](docs/contributor/test-strategy.md).
80+
7981
To get started running tests first import the `testing-debug.code-profile` VS Code profile used by the tests. Run the `> Profiles: Import Profile...` command then `Select File` and pick `./.vscode/testing-debug.code-profile`.
8082

8183
Now you can run tests locally using either of the following methods:

Diff for: ‎docs/contributor/test-strategy.md

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Swift for Visual Studio Code test strategy
2+
3+
## Overview
4+
5+
The recommended way for [testing extensions](https://code.visualstudio.com/api/working-with-extensions/testing-extension) involves using either the new [vscode-test-cli](https://github.com/microsoft/vscode-test-cli) or creating your [own mocha test runner](https://code.visualstudio.com/api/working-with-extensions/testing-extension#advanced-setup-your-own-runner). Either approach results in Visual Studio Code getting downloaded, and a window spawned. This is necessary to have access the the APIs of the `vscode` namespace, to stimulate behaviour (ex. `vscode.tasks.executeTasks`) and obtain state (ex. `vscode.languages.getDiagnostics`).
6+
7+
There are some testing gaps when only using this approach. Relying on using the `vscode` APIs makes it difficult to easily write unit tests. It ends up testing the communication between a lot of components in the `vscode-swift` extension and associated dependencies. Additionally, there is a lot of code that remains unverified. This code may get executed so that it shows up in the coverage report, but the behaviour is unobserved. Some examples of behaviour that is not observed, includes prompting the user for input, or verifying if a notification gets shown. See https://devblogs.microsoft.com/ise/testing-vscode-extensions-with-typescript/ for a more detailed overview.
8+
9+
In addition to gaps in testing, the current approach tests at the integration level which results in slower and more brittle tests, as they rely on the communication between several components.
10+
11+
## Unit testing
12+
13+
For unit testing [ts-mockito](https://github.com/NagRock/ts-mockito) is used for mocking out user interaction and observing behaviour that cannot otherwise be observed. Many helpful utility functions exist in [MockUtils.ts](../../test/unit-tests/MockUtils.ts). These utility methods take care of the setup and teardown of the mocks so the developer does not need to remember to do this for each suite/test.
14+
15+
### Mocking `vscode` namespace
16+
17+
The `mockNamespace` function provides a mocked implementation of one of the `vscode` API namespaces. Below is an example of how to employ mocking to test if the [showReloadExtensionNotification](../../src/ui/ReloadExtension.ts) function shows a notification and mock the button click.
18+
19+
```ts
20+
suite("ReloadExtension Unit Test Suite", async function () {
21+
const windowMock = mockNamespace(vscode, "window");
22+
const commandsMock = mockNamespace(vscode, "commands");
23+
24+
test('"Reload Extensions" is clicked', async () => {
25+
// What happens if they click this button?
26+
when(windowMock.showWarningMessage(anyString(), "Reload Extensions")).thenReturn(
27+
Promise.resolve("Reload Extensions")
28+
);
29+
await showReloadExtensionNotification("Want to reload?");
30+
verify(commandsMock.executeCommand("workbench.action.reloadWindow")).called();
31+
});
32+
});
33+
```
34+
35+
### Mocking event emitter
36+
37+
The `eventListenerMock` function captures components listening for a given event and fires the event emitter with the provided test data. Below is an example of mocking the `onDidStartTask` event.
38+
39+
```ts
40+
suite("Event emitter example", async function () {
41+
const listenerMock = eventListenerMock(vscode.tasks, "onDidStartTask");
42+
43+
test("Fire event", async () => {
44+
const mockedTask = mock(vscode.Task);
45+
mockedTaskExecution = { task: instance(mockedTask), terminate: () => {} };
46+
47+
listenerMock.notifyAll({ execution: mockedTaskExecution });
48+
});
49+
});
50+
```
51+
52+
### Mocking global variables
53+
54+
The `globalVariableMock` function allows for overriding the value for some global constant.
55+
56+
```ts
57+
suite("Environment variable example", async function () {
58+
const envMock = globalVariableMock(process, "env");
59+
60+
test("Linux", async () => {
61+
env.setValue({ DEVELOPER_DIR: '/path/to/Xcode.app' });
62+
63+
// Test DEVELOPER_DIR usage
64+
});
65+
});
66+
```
67+
68+
It can also be used to mock the extension [configuration](../../src/configuration.ts).
69+
70+
```ts
71+
import configuration from "../../../src/configuration";
72+
suite("SwiftBuildStatus Unit Test Suite", async function () {
73+
const statusConfig = globalVariableMock(configuration, "showBuildStatus");
74+
75+
test("Shows notification", async () => {
76+
statusConfig.setValue("notification");
77+
78+
// Test shows as notification
79+
});
80+
81+
test("Shows status bar", async () => {
82+
statusConfig.setValue("swiftStatus");
83+
84+
// Test shows in status bar
85+
});
86+
});
87+
```
88+
89+
## Test Pyramid
90+
91+
Tests are grouped into 3 levels. The biggest distinguishing factors between the various levels will be the runtime of the test, and the number of "real" vs. mocked dependencies.
92+
93+
### 1. Unit (`/test/unit`)
94+
95+
- Employ stubbing or mocking techniques to allow for user interaction, AND to mock slow APIs like `executeTask`
96+
- Mocked SwiftPM commands return hardcoded output, such as compile errors
97+
- Any sourcekit-lsp interaction is mocked, with hardcoded responses
98+
- Runs with a fast timeout of 100ms
99+
- No usages of assets/test projects
100+
- Use [mock-fs](https://www.npmjs.com/package/mock-fs) for testing fs usage
101+
- Run in CI build for new PRs
102+
- Ideally the vast majority of tests are at this level
103+
104+
### 2. Integration (`/test/integration`)
105+
106+
- Tests interaction between components, with some mocking for slow or fragile dependencies
107+
- Stimulate actions using the VS Code APIs
108+
- Use actual output from SwiftPM
109+
- Use actual responses from sourcekit-lsp
110+
- Use a moderate maximum timeout of up to 30s
111+
- The CI job timeout is 15 minutes
112+
- Use curated `assets/test` projects
113+
- Run in CI and nightly builds
114+
- Test key integrations with the VS Code API and key communication between our components
115+
116+
### 3. Smoke (`/test/smoke`)
117+
118+
- No mocking at all
119+
- For now only stimulate actions using the VS Code APIs, testing via the UI is a different beast
120+
- Use curated `assets/test` projects
121+
- No need to enforce a maximum timeout (per test)
122+
- Only run in nightly build
123+
- Should only have a handful of these tests, for complex features
124+
125+
## Test Matrix
126+
127+
### CI Build
128+
129+
- Run for new PRs (`@swift-server-bot test this please`)
130+
- Run macOS, Linux and Windows
131+
- Currently only Linux, macOS and Windows is being explored
132+
- Expect Windows to fail short term, annotate to disable these tests
133+
- Ideally run against Swift versions 5.6 - 6.0 + main
134+
- Run `unit` and `integration` test suites
135+
- Run test against latest `stable` VS Code
136+
137+
### Nightly Build
138+
139+
- Run macOS, Linux and Windows
140+
- Currently only Linux, macOS and Windows is being explored
141+
- Ideally run against Swift versions 5.6 - 6.0 + main
142+
- Run `integration` and `smoke` test suites
143+
- Run test against latest `stable` and `insiders` VS Code

Diff for: ‎package-lock.json

+31
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: ‎package.json

+2
Original file line numberDiff line numberDiff line change
@@ -1215,6 +1215,7 @@
12151215
"format": "prettier --check src test",
12161216
"pretest": "npm run compile && find ./assets/test -type d -name '.build' -exec rm -rf {} + && find . -type d -name 'Package.resolved' -exec rm -rf {} + && tsc -p ./",
12171217
"test": "vscode-test",
1218+
"unit-test": "vscode-test --label unitTests",
12181219
"coverage": "npm run pretest && vscode-test --coverage",
12191220
"compile-tests": "find ./assets/test -type d -name '.build' -exec rm -rf {} + && npm run compile && npm run esbuild",
12201221
"package": "vsce package",
@@ -1243,6 +1244,7 @@
12431244
"node-pty": "^1.0.0",
12441245
"prettier": "3.3.2",
12451246
"strip-ansi": "^6.0.1",
1247+
"ts-mockito": "^2.6.1",
12461248
"typescript": "^5.5.3"
12471249
},
12481250
"dependencies": {

Diff for: ‎test/unit-tests/MockUtils.ts

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2024 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
/* eslint-disable @typescript-eslint/no-explicit-any */
15+
import { setup, teardown } from "mocha";
16+
import { Disposable, Event, EventEmitter } from "vscode";
17+
import { instance, mock } from "ts-mockito";
18+
19+
export function getMochaHooks(): { setup: typeof setup; teardown: typeof teardown } {
20+
if (!("setup" in global && "teardown" in global)) {
21+
throw new Error("MockUtils can only be used when running under mocha");
22+
}
23+
return {
24+
setup: global.setup,
25+
teardown: global.teardown,
26+
};
27+
}
28+
29+
/**
30+
* Create a new mock for each test that gets cleaned up automatically afterwards. This function makes use of the fact that
31+
* Mocha's setup() and teardown() methods can be called from anywhere. The resulting object is a proxy to the real
32+
* mock since it won't be created until the test actually begins.
33+
*
34+
* The proxy lets us avoid boilerplate by creating a mock in one line:
35+
*
36+
* const windowMock = mockNamespace(vscode, "window");
37+
*
38+
* test('Some test', () => {
39+
* expect(windowMock.showErrorMessage).to.have.been.calledWith('Some error message');
40+
* })
41+
*
42+
* @param obj The object to create the stub inside
43+
* @param property The method inside the object to be stubbed
44+
*/
45+
export function mockNamespace<T, K extends keyof T>(obj: T, property: K): T[K] {
46+
let realMock: T[K];
47+
const originalValue: T[K] = obj[property];
48+
const mocha = getMochaHooks();
49+
// Create the mock at setup
50+
mocha.setup(() => {
51+
realMock = mock<T[K]>();
52+
Object.defineProperty(obj, property, { value: instance(realMock) });
53+
});
54+
// Restore original value at teardown
55+
mocha.teardown(() => {
56+
Object.defineProperty(obj, property, { value: originalValue });
57+
});
58+
// Return the proxy to the real mock
59+
return new Proxy(originalValue, {
60+
get: (target: any, property: string): any => {
61+
if (!realMock) {
62+
throw Error("Mock proxy accessed before setup()");
63+
}
64+
return (realMock as any)[property];
65+
},
66+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
67+
set: (target: any, property: string, value: any): boolean => {
68+
// Ignore
69+
return true;
70+
},
71+
});
72+
}
73+
74+
/** Retrieves the type of event given to the generic vscode.Event<T> */
75+
export type EventType<T> = T extends Event<infer E> ? E : never;
76+
77+
/** The listener function that receives events. */
78+
export type Listener<T> = T extends Event<infer E> ? (e: E) => any : never;
79+
80+
/** Allows sending an event to all intercepted listeners. */
81+
export interface ListenerInterceptor<T> {
82+
onDidAddListener: Event<(event: T) => any>;
83+
84+
/** Send an event notification to the intercepted listener(s). */
85+
notifyAll(event: T): Promise<void>;
86+
}
87+
88+
class ListenerInterceptorImpl<T> implements ListenerInterceptor<T> {
89+
private listeners: ((event: T) => any)[];
90+
private _onDidChangeListeners: EventEmitter<(event: T) => any>;
91+
92+
constructor() {
93+
this.listeners = [];
94+
this._onDidChangeListeners = new EventEmitter();
95+
this.onDidAddListener = this._onDidChangeListeners.event;
96+
}
97+
98+
async notifyAll(event: T): Promise<void> {
99+
await Promise.all(this.listeners.map(async listener => await listener(event)));
100+
}
101+
102+
addListener: Event<T> = (listener, thisArgs) => {
103+
if (thisArgs) {
104+
listener = listener.bind(thisArgs);
105+
}
106+
this.listeners.push(listener);
107+
this._onDidChangeListeners.fire(listener);
108+
return { dispose: () => mock(Disposable) };
109+
};
110+
111+
onDidAddListener: Event<(event: T) => any>;
112+
}
113+
114+
/** Retrieves all properties of an object that are of the type vscode.Event<T>. */
115+
type EventsOf<T> = {
116+
[K in keyof T]: T[K] extends Event<any> ? K : never;
117+
}[keyof T];
118+
119+
/**
120+
* Create a ListenerInterceptor that intercepts all listeners attached to a VS Code event function. This function stubs out the
121+
* given method during Mocha setup and restores it during teardown.
122+
*
123+
* This lets us avoid boilerplate by creating an interceptor in one line:
124+
*
125+
* const interceptor = eventListenerMock(vscode.workspace, 'onDidChangeWorkspaceFolders');
126+
*
127+
* test('Some test', () => {
128+
* interceptor.notify(event);
129+
* })
130+
*
131+
* @param obj The object to create the stub inside
132+
* @param method The name of the method to stub within the object. Must be of the type Event<any>.
133+
*/
134+
export function eventListenerMock<T, K extends EventsOf<T>>(
135+
obj: T,
136+
method: K
137+
): ListenerInterceptor<EventType<T[K]>> {
138+
const interceptor = new ListenerInterceptorImpl<EventType<T[K]>>();
139+
const originalValue: T[K] = obj[method];
140+
const mocha = getMochaHooks();
141+
mocha.setup(() => {
142+
Object.defineProperty(obj, method, { value: interceptor.addListener });
143+
});
144+
// Restore original value at teardown
145+
mocha.teardown(() => {
146+
Object.defineProperty(obj, method, { value: originalValue });
147+
});
148+
return interceptor;
149+
}
150+
151+
/**
152+
* Allows setting the global value.
153+
*/
154+
export interface GlobalVariableMock<T> {
155+
setValue(value: T): void;
156+
}
157+
158+
/**
159+
* Create a new GlobalVariableMock that is restored after a test completes.
160+
*/
161+
export function globalVariableMock<T, K extends keyof T>(
162+
obj: T,
163+
property: K
164+
): GlobalVariableMock<T[K]> {
165+
let setupComplete: boolean = false;
166+
let originalValue: T[K];
167+
const mocha = getMochaHooks();
168+
// Grab the original value during setup
169+
mocha.setup(() => {
170+
originalValue = obj[property];
171+
setupComplete = true;
172+
});
173+
// Restore the original value on teardown
174+
mocha.teardown(() => {
175+
Object.defineProperty(obj, property, { value: originalValue });
176+
setupComplete = false;
177+
});
178+
// Return a GlobalVariableMock that allows for easy mocking of the value
179+
return {
180+
setValue(value: T[K]): void {
181+
if (!setupComplete) {
182+
throw new Error(
183+
`'${String(property)}' cannot be set before globalVariableMock() completes its setup through Mocha`
184+
);
185+
}
186+
Object.defineProperty(obj, property, { value: value });
187+
},
188+
};
189+
}

Diff for: ‎test/unit-tests/ui/ReloadExtension.test.ts

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2024 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import { anyString, anything, verify, when } from "ts-mockito";
15+
import { equal } from "assert";
16+
import { showReloadExtensionNotification } from "../../../src/ui/ReloadExtension";
17+
import * as vscode from "vscode";
18+
import { mockNamespace } from "../MockUtils";
19+
20+
suite("ReloadExtension Unit Test Suite", async function () {
21+
const windowMock = mockNamespace(vscode, "window");
22+
const commandsMock = mockNamespace(vscode, "commands");
23+
24+
test("Shows user a warning", async () => {
25+
// No behaviour setup, let's just check if we showed them the notification
26+
await showReloadExtensionNotification("Want to reload?");
27+
verify(windowMock.showWarningMessage("Want to reload?", "Reload Extensions")).called();
28+
});
29+
30+
test('"Reload Extensions" is clicked', async () => {
31+
// What happens if they click this button?
32+
when(windowMock.showWarningMessage(anyString(), "Reload Extensions")).thenReturn(
33+
Promise.resolve("Reload Extensions")
34+
);
35+
await showReloadExtensionNotification("Want to reload?");
36+
verify(commandsMock.executeCommand("workbench.action.reloadWindow")).called();
37+
});
38+
39+
test("Provide a different button", async () => {
40+
// What if we provide another option?
41+
when(
42+
windowMock.showWarningMessage("Want to reload?", "Reload Extensions", "Ignore")
43+
).thenReturn(Promise.resolve("Ignore"));
44+
const result = await showReloadExtensionNotification("Want to reload?", "Ignore");
45+
equal(result, "Ignore");
46+
verify(commandsMock.executeCommand(anything())).never();
47+
});
48+
});

Diff for: ‎test/unit-tests/ui/SwiftBuildStatus.test.ts

+242
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2024 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import {
15+
anyFunction,
16+
anything,
17+
capture,
18+
deepEqual,
19+
instance,
20+
mock,
21+
verify,
22+
when,
23+
} from "ts-mockito";
24+
import configuration from "../../../src/configuration";
25+
import * as vscode from "vscode";
26+
import { eventListenerMock, globalVariableMock, mockNamespace } from "../MockUtils";
27+
import { SwiftExecution } from "../../../src/tasks/SwiftExecution";
28+
import { TestSwiftProcess } from "../../fixtures";
29+
import { StatusItem } from "../../../src/ui/StatusItem";
30+
import { SwiftBuildStatus } from "../../../src/ui/SwiftBuildStatus";
31+
32+
suite("SwiftBuildStatus Unit Test Suite", async function () {
33+
const windowMock = mockNamespace(vscode, "window");
34+
const listenerMock = eventListenerMock(vscode.tasks, "onDidStartTask");
35+
const configurationMock = globalVariableMock(configuration, "showBuildStatus");
36+
37+
let mockedStatusItem: StatusItem;
38+
let mockedTask: vscode.Task;
39+
let mockedExecution: SwiftExecution;
40+
let mockedProcess: TestSwiftProcess;
41+
let mockedTaskExecution: vscode.TaskExecution;
42+
43+
setup(() => {
44+
mockedStatusItem = mock(StatusItem);
45+
mockedTask = mock(vscode.Task);
46+
mockedProcess = new TestSwiftProcess("swift", ["build"]);
47+
mockedExecution = new SwiftExecution(
48+
mockedProcess.command,
49+
mockedProcess.args,
50+
{},
51+
mockedProcess
52+
);
53+
when(mockedTask.definition).thenReturn({ type: "swift" });
54+
when(mockedTask.execution).thenReturn(mockedExecution);
55+
when(mockedTask.name).thenReturn("My Task");
56+
57+
// https://github.com/NagRock/ts-mockito/issues/204
58+
const task = instance(mockedTask);
59+
Object.setPrototypeOf(task, vscode.Task.prototype);
60+
mockedTaskExecution = { task, terminate: () => {} };
61+
});
62+
63+
test("Never show status", async () => {
64+
configurationMock.setValue("never");
65+
66+
new SwiftBuildStatus(instance(mockedStatusItem));
67+
listenerMock.notifyAll({ execution: mockedTaskExecution });
68+
69+
verify(
70+
mockedStatusItem.showStatusWhileRunning(mockedTaskExecution.task, anyFunction())
71+
).never();
72+
verify(windowMock.withProgress(anything(), anyFunction())).never();
73+
});
74+
75+
test("Ignore non-swift task", async () => {
76+
when(mockedTask.execution).thenReturn(new vscode.ShellExecution("swift"));
77+
configurationMock.setValue("swiftStatus");
78+
79+
new SwiftBuildStatus(instance(mockedStatusItem));
80+
listenerMock.notifyAll({ execution: mockedTaskExecution });
81+
82+
verify(
83+
mockedStatusItem.showStatusWhileRunning(mockedTaskExecution.task, anyFunction())
84+
).never();
85+
verify(windowMock.withProgress(anything(), anyFunction())).never();
86+
});
87+
88+
test("Show swift status", async () => {
89+
configurationMock.setValue("swiftStatus");
90+
91+
new SwiftBuildStatus(instance(mockedStatusItem));
92+
listenerMock.notifyAll({ execution: mockedTaskExecution });
93+
94+
verify(
95+
mockedStatusItem.showStatusWhileRunning(mockedTaskExecution.task, anyFunction())
96+
).called();
97+
verify(windowMock.withProgress(anything(), anyFunction())).never();
98+
});
99+
100+
test("Show status bar progress", async () => {
101+
configurationMock.setValue("progress");
102+
103+
new SwiftBuildStatus(instance(mockedStatusItem));
104+
listenerMock.notifyAll({ execution: mockedTaskExecution });
105+
106+
verify(
107+
windowMock.withProgress(
108+
deepEqual({ location: vscode.ProgressLocation.Window }),
109+
anyFunction()
110+
)
111+
).called();
112+
verify(mockedStatusItem.showStatusWhileRunning(anything(), anyFunction())).never();
113+
});
114+
115+
test("Show notification progress", async () => {
116+
configurationMock.setValue("notification");
117+
118+
new SwiftBuildStatus(instance(mockedStatusItem));
119+
listenerMock.notifyAll({ execution: mockedTaskExecution });
120+
121+
verify(
122+
windowMock.withProgress(
123+
deepEqual({ location: vscode.ProgressLocation.Notification }),
124+
anyFunction()
125+
)
126+
).called();
127+
verify(mockedStatusItem.showStatusWhileRunning(anything(), anyFunction())).never();
128+
});
129+
130+
test("Update fetching", async () => {
131+
// Setup progress
132+
configurationMock.setValue("progress");
133+
new SwiftBuildStatus(instance(mockedStatusItem));
134+
listenerMock.notifyAll({ execution: mockedTaskExecution });
135+
136+
// Setup swiftStatus
137+
configurationMock.setValue("swiftStatus");
138+
new SwiftBuildStatus(instance(mockedStatusItem));
139+
listenerMock.notifyAll({ execution: mockedTaskExecution });
140+
141+
const mockReporter = mock<vscode.Progress<{ message: string }>>();
142+
const [, statusCallback] = capture<vscode.Task, () => void>(
143+
mockedStatusItem.showStatusWhileRunning
144+
).last();
145+
const [, progressCallback] = capture<
146+
vscode.ProgressOptions,
147+
(p: vscode.Progress<unknown>) => Promise<void>
148+
>(windowMock.withProgress).last();
149+
150+
// Execute callback reporters
151+
statusCallback();
152+
progressCallback(instance(mockReporter));
153+
154+
mockedProcess.write(
155+
"Fetching https://github.com/apple/example-package-figlet from cache\n" +
156+
"Fetched https://github.com/apple/example-package-figlet from cache (0.43s)\n" +
157+
"Fetching https://github.com/apple/swift-testing.git from cache\n" +
158+
"Fetched https://github.com/apple/swift-testing.git from cache (0.77s)\n"
159+
);
160+
161+
const expected = "My Task fetching dependencies";
162+
verify(mockReporter.report(deepEqual({ message: expected }))).called();
163+
verify(mockedStatusItem.update(mockedTaskExecution.task, expected)).called();
164+
});
165+
166+
test("Update build progress", async () => {
167+
// Setup progress
168+
configurationMock.setValue("progress");
169+
new SwiftBuildStatus(instance(mockedStatusItem));
170+
listenerMock.notifyAll({ execution: mockedTaskExecution });
171+
172+
// Setup swiftStatus
173+
configurationMock.setValue("swiftStatus");
174+
new SwiftBuildStatus(instance(mockedStatusItem));
175+
listenerMock.notifyAll({ execution: mockedTaskExecution });
176+
177+
const mockReporter = mock<vscode.Progress<{ message: string }>>();
178+
const [, statusCallback] = capture<vscode.Task, () => void>(
179+
mockedStatusItem.showStatusWhileRunning
180+
).last();
181+
const [, progressCallback] = capture<
182+
vscode.ProgressOptions,
183+
(p: vscode.Progress<unknown>) => Promise<void>
184+
>(windowMock.withProgress).last();
185+
186+
// Execute callback reporters
187+
statusCallback();
188+
progressCallback(instance(mockReporter));
189+
190+
mockedProcess.write(
191+
"Fetching https://github.com/apple/example-package-figlet from cache\n" +
192+
"[6/7] Building main.swift\n" +
193+
"[7/7] Applying MyCLI\n"
194+
);
195+
196+
const expected = "My Task [7/7]";
197+
verify(mockReporter.report(deepEqual({ message: expected }))).called();
198+
verify(mockedStatusItem.update(mockedTaskExecution.task, expected)).called();
199+
200+
// Ignore old stuff
201+
verify(
202+
mockReporter.report(deepEqual({ message: "My Task fetching dependencies" }))
203+
).never();
204+
verify(mockReporter.report(deepEqual({ message: "My Task [6/7]" }))).never();
205+
});
206+
207+
test("Build complete", async () => {
208+
// Setup progress
209+
configurationMock.setValue("progress");
210+
new SwiftBuildStatus(instance(mockedStatusItem));
211+
listenerMock.notifyAll({ execution: mockedTaskExecution });
212+
213+
// Setup swiftStatus
214+
configurationMock.setValue("swiftStatus");
215+
new SwiftBuildStatus(instance(mockedStatusItem));
216+
listenerMock.notifyAll({ execution: mockedTaskExecution });
217+
218+
const mockReporter = mock<vscode.Progress<{ message: string }>>();
219+
const [, statusCallback] = capture<vscode.Task, () => void>(
220+
mockedStatusItem.showStatusWhileRunning
221+
).last();
222+
const [, progressCallback] = capture<
223+
vscode.ProgressOptions,
224+
(p: vscode.Progress<unknown>) => Promise<void>
225+
>(windowMock.withProgress).last();
226+
227+
// Execute callback reporters
228+
statusCallback();
229+
progressCallback(instance(mockReporter));
230+
231+
mockedProcess.write(
232+
"Fetching https://github.com/apple/example-package-figlet from cache\n" +
233+
"[6/7] Building main.swift\n" +
234+
"[7/7] Applying MyCLI\n" +
235+
"Build complete!"
236+
);
237+
238+
// Report nothing
239+
verify(mockReporter.report(anything())).never();
240+
verify(mockedStatusItem.update(anything(), anything())).never();
241+
});
242+
});

0 commit comments

Comments
 (0)
Please sign in to comment.