Skip to content

Commit 12df405

Browse files
authored
Add smoke test to detect extension load and python selection (#431)
1 parent 6566656 commit 12df405

File tree

12 files changed

+225
-28
lines changed

12 files changed

+225
-28
lines changed

.github/workflows/pr-check.yml

+21-1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,20 @@ jobs:
112112
cache: 'npm'
113113
cache-dependency-path: ${{ env.special-working-directory-relative }}/package-lock.json
114114

115+
- name: Use Python 3.8
116+
uses: actions/setup-python@v5
117+
with:
118+
python-version: '3.8'
119+
120+
- name: Update pip, install wheel and nox
121+
run: python -m pip install -U pip wheel nox
122+
shell: bash
123+
124+
# This will install libraries to a target directory.
125+
- name: Install bundled python libraries
126+
run: python -m nox --session install_bundled_libs
127+
shell: bash
128+
115129
- name: Install Node dependencies
116130
run: npm ci
117131
shell: bash
@@ -120,8 +134,14 @@ jobs:
120134
run: npm run pretest
121135
shell: bash
122136

123-
- name: Run TS tests
137+
- name: Run TS Unit tests
124138
uses: GabrielBB/xvfb-action@v1.6
125139
with:
126140
run: npm run tests
127141
working-directory: ${{ env.special-working-directory }}
142+
143+
- name: Run TS Smoke tests
144+
uses: GabrielBB/xvfb-action@v1.6
145+
with:
146+
run: npm run smoke-tests
147+
working-directory: ${{ env.special-working-directory }}

.vscode/launch.json

+22-23
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,8 @@
99
"name": "Debug Extension Only",
1010
"type": "extensionHost",
1111
"request": "launch",
12-
"args": [
13-
"--extensionDevelopmentPath=${workspaceFolder}"
14-
],
15-
"outFiles": [
16-
"${workspaceFolder}/dist/**/*.js"
17-
],
12+
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
13+
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
1814
"preLaunchTask": "npm: watch",
1915
"presentation": {
2016
"hidden": false,
@@ -42,20 +38,30 @@
4238
"--extensionDevelopmentPath=${workspaceFolder}",
4339
"--extensionTestsPath=${workspaceFolder}/out/test/ts_tests/index"
4440
],
45-
"outFiles": [
46-
"${workspaceFolder}/out/**/*.js",
47-
"${workspaceFolder}/dist/**/*.js"
41+
"outFiles": ["${workspaceFolder}/out/**/*.js", "${workspaceFolder}/dist/**/*.js"],
42+
"preLaunchTask": "tasks: watch-tests"
43+
},
44+
{
45+
"name": "TS Smoke Tests",
46+
"type": "extensionHost",
47+
"request": "launch",
48+
"args": [
49+
"--extensionDevelopmentPath=${workspaceFolder}",
50+
"--extensionTestsPath=${workspaceFolder}/out/test/ts_tests/index",
51+
"${workspaceFolder}/src/test/ts_tests/test_data/project"
4852
],
53+
"outFiles": ["${workspaceFolder}/out/**/*.js", "${workspaceFolder}/dist/**/*.js"],
54+
"env": {
55+
"SMOKE_TESTS": "true"
56+
},
4957
"preLaunchTask": "tasks: watch-tests"
5058
},
5159
{
5260
"name": "Python Config for test explorer (hidden)",
5361
"type": "python",
5462
"request": "launch",
5563
"console": "integratedTerminal",
56-
"purpose": [
57-
"debug-test"
58-
],
64+
"purpose": ["debug-test"],
5965
"justMyCode": true,
6066
"presentation": {
6167
"hidden": true,
@@ -67,12 +73,8 @@
6773
"name": "Debug Extension (hidden)",
6874
"type": "extensionHost",
6975
"request": "launch",
70-
"args": [
71-
"--extensionDevelopmentPath=${workspaceFolder}"
72-
],
73-
"outFiles": [
74-
"${workspaceFolder}/dist/**/*.js"
75-
],
76+
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
77+
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
7678
"env": {
7779
"USE_DEBUGPY": "True"
7880
},
@@ -101,10 +103,7 @@
101103
"compounds": [
102104
{
103105
"name": "Debug Extension and Python",
104-
"configurations": [
105-
"Python debug server (hidden)",
106-
"Debug Extension (hidden)"
107-
],
106+
"configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"],
108107
"stopAll": true,
109108
"preLaunchTask": "npm: watch",
110109
"presentation": {
@@ -114,4 +113,4 @@
114113
}
115114
}
116115
]
117-
}
116+
}

bundled/tool/lsp_server.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ def initialize(params: lsp.InitializeParams) -> None:
277277
paths = "\r\n ".join(sys.path)
278278
log_to_output(f"sys.path used to run Server:\r\n {paths}")
279279

280+
_update_workspace_settings_with_version_info(WORKSPACE_SETTINGS)
281+
280282

281283
@LSP_SERVER.feature(lsp.EXIT)
282284
def on_exit(_params: Optional[Any] = None) -> None:
@@ -374,8 +376,6 @@ def _update_workspace_settings(settings):
374376
"workspaceFS": key,
375377
}
376378

377-
_update_workspace_settings_with_version_info(WORKSPACE_SETTINGS)
378-
379379

380380
def _get_settings_by_path(file_path: pathlib.Path):
381381
workspaces = {s["workspaceFS"] for s in WORKSPACE_SETTINGS.values()}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"package": "webpack --mode production --devtool hidden-source-map",
6161
"pretest": "npm run compile-tests && npm run compile && npm run lint",
6262
"tests": "node ./out/test/ts_tests/runTest.js",
63+
"smoke-tests": "node ./out/test/ts_tests/runSmokeTest.js",
6364
"vsce-package": "vsce package -o black-formatter.vsix",
6465
"vscode:prepublish": "npm run package",
6566
"watch": "webpack --watch",

src/common/python.ts

-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ async function getPythonExtensionAPI(): Promise<PythonExtension | undefined> {
2626
export async function initializePython(disposables: Disposable[]): Promise<void> {
2727
try {
2828
const api = await getPythonExtensionAPI();
29-
3029
if (api) {
3130
disposables.push(
3231
api.environments.onDidChangeActiveEnvironmentPath((e) => {

src/test/ts_tests/index.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as glob from 'glob';
22
import Mocha from 'mocha';
33
import * as path from 'path';
4+
import { env } from 'process';
45

56
export function run(): Promise<void> {
67
// Create the mocha test
@@ -12,7 +13,12 @@ export function run(): Promise<void> {
1213
const testsRoot = path.resolve(__dirname, './tests');
1314

1415
return new Promise((c, e) => {
15-
const files = glob.globSync('**/**.test.js', { cwd: testsRoot });
16+
let files = [];
17+
if (env.SMOKE_TESTS) {
18+
files = glob.globSync('**/**.smoke.test.js', { cwd: testsRoot });
19+
} else {
20+
files = glob.globSync('**/**.unit.test.js', { cwd: testsRoot });
21+
}
1622

1723
// Add files to the test suite
1824
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));

src/test/ts_tests/runSmokeTest.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
/* eslint-disable @typescript-eslint/naming-convention */
4+
5+
import * as cp from 'child_process';
6+
import * as path from 'path';
7+
8+
import { runTests, downloadAndUnzipVSCode, resolveCliArgsFromVSCodeExecutablePath } from '@vscode/test-electron';
9+
import { EXTENSION_ROOT_DIR } from '../../common/constants';
10+
11+
const TEST_PROJECT_DIR = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'ts_tests', 'test_data', 'project');
12+
13+
async function main() {
14+
try {
15+
const vscodeExecutablePath = await downloadAndUnzipVSCode('stable');
16+
17+
const [cli, ...args] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath);
18+
const command = path.relative(EXTENSION_ROOT_DIR, cli);
19+
cp.spawnSync(command, [...args, '--install-extension', 'ms-python.python'], {
20+
encoding: 'utf-8',
21+
stdio: 'inherit',
22+
});
23+
24+
const extensionDevelopmentPath = EXTENSION_ROOT_DIR;
25+
const extensionTestsPath = path.resolve(__dirname, './index');
26+
27+
await runTests({
28+
extensionDevelopmentPath,
29+
extensionTestsPath,
30+
extensionTestsEnv: { SMOKE_TESTS: 'true' },
31+
launchArgs: [TEST_PROJECT_DIR],
32+
});
33+
} catch (err) {
34+
console.error('Failed to run tests');
35+
console.error(err);
36+
process.exit(1);
37+
}
38+
}
39+
40+
main();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"[python]": {
3+
"editor.defaultFormatter": "ms-python.black-formatter",
4+
"editor.formatOnSave": true
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import os
2+
import sys
3+
4+
print(os.fspath(sys.executable))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import sys
2+
print(sys.executable)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import os;import sys
2+
print(os.fspath(sys.executable))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as path from 'path';
5+
import * as vscode from 'vscode';
6+
import * as fsapi from 'fs-extra';
7+
import { EXTENSION_ROOT_DIR } from '../../../../common/constants';
8+
import { assert } from 'chai';
9+
10+
const TEST_PROJECT_DIR = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'ts_tests', 'test_data', 'project');
11+
const TIMEOUT = 120000; // 120 seconds
12+
13+
suite('Smoke Tests', function () {
14+
this.timeout(TIMEOUT);
15+
16+
let disposables: vscode.Disposable[] = [];
17+
18+
setup(async () => {
19+
disposables = [];
20+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
21+
});
22+
23+
teardown(async () => {
24+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
25+
26+
disposables.forEach((d) => d.dispose());
27+
disposables = [];
28+
});
29+
30+
async function ensurePythonExt(activate?: boolean): Promise<void> {
31+
const pythonExt = vscode.extensions.getExtension('ms-python.python');
32+
assert.ok(pythonExt, 'Python Extension not found');
33+
if (activate) {
34+
await pythonExt?.activate();
35+
}
36+
}
37+
38+
async function ensureBlackExt(activate?: boolean): Promise<void> {
39+
const extension = vscode.extensions.getExtension('ms-python.black-formatter');
40+
assert.ok(extension, 'Black Formatter Extension not found');
41+
if (activate) {
42+
await extension?.activate();
43+
}
44+
}
45+
46+
test('Ensure Black Formatter Extension loads', async () => {
47+
await vscode.workspace.openTextDocument(path.join(TEST_PROJECT_DIR, 'myscript.py'));
48+
49+
await ensurePythonExt(true);
50+
await ensureBlackExt(false);
51+
52+
const extension = vscode.extensions.getExtension('ms-python.black-formatter');
53+
if (extension) {
54+
let timeout = TIMEOUT;
55+
while (!extension.isActive && timeout > 0) {
56+
await new Promise((resolve) => setTimeout(resolve, 100));
57+
timeout -= 100;
58+
}
59+
assert.ok(extension.isActive, `Extension not activated in ${TIMEOUT / 1000} seconds`);
60+
}
61+
});
62+
63+
test('Ensure Black Formatter formats a file on save', async () => {
64+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
65+
await ensurePythonExt(true);
66+
const scriptPath = path.join(TEST_PROJECT_DIR, 'myscript.py');
67+
68+
const unformatted = await fsapi.readFile(path.join(TEST_PROJECT_DIR, 'myscript.unformatted'), {
69+
encoding: 'utf8',
70+
});
71+
const formatted = await fsapi.readFile(path.join(TEST_PROJECT_DIR, 'myscript.formatted'), { encoding: 'utf8' });
72+
await fsapi.writeFile(scriptPath, unformatted, { encoding: 'utf8' });
73+
74+
await ensureBlackExt(true);
75+
76+
const doc = await vscode.workspace.openTextDocument(scriptPath);
77+
await vscode.window.showTextDocument(doc);
78+
79+
const editor = vscode.window.activeTextEditor;
80+
assert.ok(editor, 'No active editor');
81+
assert.ok(editor?.document.uri.fsPath.endsWith('myscript.py'), 'Active editor is not myscript.py');
82+
83+
const formatDone = new Promise<void>((resolve) => {
84+
const watcher = vscode.workspace.createFileSystemWatcher(
85+
new vscode.RelativePattern(TEST_PROJECT_DIR, 'myscript.py'),
86+
true, // We don't need create events
87+
false, // We need change events
88+
true, // We don't need delete events
89+
);
90+
disposables.push(
91+
watcher,
92+
watcher.onDidChange((e) => {
93+
const text = fsapi.readFileSync(e.fsPath, { encoding: 'utf8' });
94+
if (!text.includes(';')) {
95+
console.log('Saved with format changes');
96+
resolve();
97+
} else {
98+
console.log('Saved without format changes');
99+
}
100+
}),
101+
);
102+
});
103+
104+
const timer = setInterval(() => {
105+
console.log('Saving file');
106+
vscode.commands.executeCommand('workbench.action.files.save');
107+
}, 1000);
108+
disposables.push({ dispose: () => clearInterval(timer) });
109+
110+
await vscode.commands.executeCommand('workbench.action.files.save');
111+
await formatDone;
112+
const actualText = await fsapi.readFile(scriptPath, { encoding: 'utf8' });
113+
assert.equal(actualText, formatted);
114+
115+
//cleanup
116+
await fsapi.writeFile(scriptPath, '', { encoding: 'utf8' });
117+
});
118+
});

0 commit comments

Comments
 (0)