From 12df4056a0efbf9e320e944c970bcf285d897be3 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 1 Feb 2024 12:54:33 -0800 Subject: [PATCH] Add smoke test to detect extension load and python selection (#431) --- .github/workflows/pr-check.yml | 22 +++- .vscode/launch.json | 45 ++++--- bundled/tool/lsp_server.py | 4 +- package.json | 1 + src/common/python.ts | 1 - src/test/ts_tests/index.ts | 8 +- src/test/ts_tests/runSmokeTest.ts | 40 ++++++ .../test_data/project/.vscode/settings.json | 6 + .../test_data/project/myscript.formatted | 4 + .../ts_tests/test_data/project/myscript.py | 2 + .../test_data/project/myscript.unformatted | 2 + .../tests/common/minimal.smoke.test.ts | 118 ++++++++++++++++++ 12 files changed, 225 insertions(+), 28 deletions(-) create mode 100644 src/test/ts_tests/runSmokeTest.ts create mode 100644 src/test/ts_tests/test_data/project/.vscode/settings.json create mode 100644 src/test/ts_tests/test_data/project/myscript.formatted create mode 100644 src/test/ts_tests/test_data/project/myscript.py create mode 100644 src/test/ts_tests/test_data/project/myscript.unformatted create mode 100644 src/test/ts_tests/tests/common/minimal.smoke.test.ts diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 76debe6..eced055 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -112,6 +112,20 @@ jobs: cache: 'npm' cache-dependency-path: ${{ env.special-working-directory-relative }}/package-lock.json + - name: Use Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: '3.8' + + - name: Update pip, install wheel and nox + run: python -m pip install -U pip wheel nox + shell: bash + + # This will install libraries to a target directory. + - name: Install bundled python libraries + run: python -m nox --session install_bundled_libs + shell: bash + - name: Install Node dependencies run: npm ci shell: bash @@ -120,8 +134,14 @@ jobs: run: npm run pretest shell: bash - - name: Run TS tests + - name: Run TS Unit tests uses: GabrielBB/xvfb-action@v1.6 with: run: npm run tests working-directory: ${{ env.special-working-directory }} + + - name: Run TS Smoke tests + uses: GabrielBB/xvfb-action@v1.6 + with: + run: npm run smoke-tests + working-directory: ${{ env.special-working-directory }} diff --git a/.vscode/launch.json b/.vscode/launch.json index 6e1f4f9..9c874cb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,12 +9,8 @@ "name": "Debug Extension Only", "type": "extensionHost", "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/dist/**/*.js" - ], + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/dist/**/*.js"], "preLaunchTask": "npm: watch", "presentation": { "hidden": false, @@ -42,10 +38,22 @@ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test/ts_tests/index" ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js", - "${workspaceFolder}/dist/**/*.js" + "outFiles": ["${workspaceFolder}/out/**/*.js", "${workspaceFolder}/dist/**/*.js"], + "preLaunchTask": "tasks: watch-tests" + }, + { + "name": "TS Smoke Tests", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/ts_tests/index", + "${workspaceFolder}/src/test/ts_tests/test_data/project" ], + "outFiles": ["${workspaceFolder}/out/**/*.js", "${workspaceFolder}/dist/**/*.js"], + "env": { + "SMOKE_TESTS": "true" + }, "preLaunchTask": "tasks: watch-tests" }, { @@ -53,9 +61,7 @@ "type": "python", "request": "launch", "console": "integratedTerminal", - "purpose": [ - "debug-test" - ], + "purpose": ["debug-test"], "justMyCode": true, "presentation": { "hidden": true, @@ -67,12 +73,8 @@ "name": "Debug Extension (hidden)", "type": "extensionHost", "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/dist/**/*.js" - ], + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/dist/**/*.js"], "env": { "USE_DEBUGPY": "True" }, @@ -101,10 +103,7 @@ "compounds": [ { "name": "Debug Extension and Python", - "configurations": [ - "Python debug server (hidden)", - "Debug Extension (hidden)" - ], + "configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"], "stopAll": true, "preLaunchTask": "npm: watch", "presentation": { @@ -114,4 +113,4 @@ } } ] -} \ No newline at end of file +} diff --git a/bundled/tool/lsp_server.py b/bundled/tool/lsp_server.py index 9b2ad57..f47fb6b 100644 --- a/bundled/tool/lsp_server.py +++ b/bundled/tool/lsp_server.py @@ -277,6 +277,8 @@ def initialize(params: lsp.InitializeParams) -> None: paths = "\r\n ".join(sys.path) log_to_output(f"sys.path used to run Server:\r\n {paths}") + _update_workspace_settings_with_version_info(WORKSPACE_SETTINGS) + @LSP_SERVER.feature(lsp.EXIT) def on_exit(_params: Optional[Any] = None) -> None: @@ -374,8 +376,6 @@ def _update_workspace_settings(settings): "workspaceFS": key, } - _update_workspace_settings_with_version_info(WORKSPACE_SETTINGS) - def _get_settings_by_path(file_path: pathlib.Path): workspaces = {s["workspaceFS"] for s in WORKSPACE_SETTINGS.values()} diff --git a/package.json b/package.json index 3642e87..548d824 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "package": "webpack --mode production --devtool hidden-source-map", "pretest": "npm run compile-tests && npm run compile && npm run lint", "tests": "node ./out/test/ts_tests/runTest.js", + "smoke-tests": "node ./out/test/ts_tests/runSmokeTest.js", "vsce-package": "vsce package -o black-formatter.vsix", "vscode:prepublish": "npm run package", "watch": "webpack --watch", diff --git a/src/common/python.ts b/src/common/python.ts index ccd8a05..b29dff1 100644 --- a/src/common/python.ts +++ b/src/common/python.ts @@ -26,7 +26,6 @@ async function getPythonExtensionAPI(): Promise { export async function initializePython(disposables: Disposable[]): Promise { try { const api = await getPythonExtensionAPI(); - if (api) { disposables.push( api.environments.onDidChangeActiveEnvironmentPath((e) => { diff --git a/src/test/ts_tests/index.ts b/src/test/ts_tests/index.ts index 6a96237..af5f602 100644 --- a/src/test/ts_tests/index.ts +++ b/src/test/ts_tests/index.ts @@ -1,6 +1,7 @@ import * as glob from 'glob'; import Mocha from 'mocha'; import * as path from 'path'; +import { env } from 'process'; export function run(): Promise { // Create the mocha test @@ -12,7 +13,12 @@ export function run(): Promise { const testsRoot = path.resolve(__dirname, './tests'); return new Promise((c, e) => { - const files = glob.globSync('**/**.test.js', { cwd: testsRoot }); + let files = []; + if (env.SMOKE_TESTS) { + files = glob.globSync('**/**.smoke.test.js', { cwd: testsRoot }); + } else { + files = glob.globSync('**/**.unit.test.js', { cwd: testsRoot }); + } // Add files to the test suite files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); diff --git a/src/test/ts_tests/runSmokeTest.ts b/src/test/ts_tests/runSmokeTest.ts new file mode 100644 index 0000000..287ef2b --- /dev/null +++ b/src/test/ts_tests/runSmokeTest.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as cp from 'child_process'; +import * as path from 'path'; + +import { runTests, downloadAndUnzipVSCode, resolveCliArgsFromVSCodeExecutablePath } from '@vscode/test-electron'; +import { EXTENSION_ROOT_DIR } from '../../common/constants'; + +const TEST_PROJECT_DIR = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'ts_tests', 'test_data', 'project'); + +async function main() { + try { + const vscodeExecutablePath = await downloadAndUnzipVSCode('stable'); + + const [cli, ...args] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath); + const command = path.relative(EXTENSION_ROOT_DIR, cli); + cp.spawnSync(command, [...args, '--install-extension', 'ms-python.python'], { + encoding: 'utf-8', + stdio: 'inherit', + }); + + const extensionDevelopmentPath = EXTENSION_ROOT_DIR; + const extensionTestsPath = path.resolve(__dirname, './index'); + + await runTests({ + extensionDevelopmentPath, + extensionTestsPath, + extensionTestsEnv: { SMOKE_TESTS: 'true' }, + launchArgs: [TEST_PROJECT_DIR], + }); + } catch (err) { + console.error('Failed to run tests'); + console.error(err); + process.exit(1); + } +} + +main(); diff --git a/src/test/ts_tests/test_data/project/.vscode/settings.json b/src/test/ts_tests/test_data/project/.vscode/settings.json new file mode 100644 index 0000000..2d70953 --- /dev/null +++ b/src/test/ts_tests/test_data/project/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true + } +} diff --git a/src/test/ts_tests/test_data/project/myscript.formatted b/src/test/ts_tests/test_data/project/myscript.formatted new file mode 100644 index 0000000..39d270e --- /dev/null +++ b/src/test/ts_tests/test_data/project/myscript.formatted @@ -0,0 +1,4 @@ +import os +import sys + +print(os.fspath(sys.executable)) diff --git a/src/test/ts_tests/test_data/project/myscript.py b/src/test/ts_tests/test_data/project/myscript.py new file mode 100644 index 0000000..14566f5 --- /dev/null +++ b/src/test/ts_tests/test_data/project/myscript.py @@ -0,0 +1,2 @@ +import sys +print(sys.executable) \ No newline at end of file diff --git a/src/test/ts_tests/test_data/project/myscript.unformatted b/src/test/ts_tests/test_data/project/myscript.unformatted new file mode 100644 index 0000000..56997f8 --- /dev/null +++ b/src/test/ts_tests/test_data/project/myscript.unformatted @@ -0,0 +1,2 @@ +import os;import sys +print(os.fspath(sys.executable)) \ No newline at end of file diff --git a/src/test/ts_tests/tests/common/minimal.smoke.test.ts b/src/test/ts_tests/tests/common/minimal.smoke.test.ts new file mode 100644 index 0000000..156a8bb --- /dev/null +++ b/src/test/ts_tests/tests/common/minimal.smoke.test.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as fsapi from 'fs-extra'; +import { EXTENSION_ROOT_DIR } from '../../../../common/constants'; +import { assert } from 'chai'; + +const TEST_PROJECT_DIR = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'ts_tests', 'test_data', 'project'); +const TIMEOUT = 120000; // 120 seconds + +suite('Smoke Tests', function () { + this.timeout(TIMEOUT); + + let disposables: vscode.Disposable[] = []; + + setup(async () => { + disposables = []; + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + teardown(async () => { + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + + disposables.forEach((d) => d.dispose()); + disposables = []; + }); + + async function ensurePythonExt(activate?: boolean): Promise { + const pythonExt = vscode.extensions.getExtension('ms-python.python'); + assert.ok(pythonExt, 'Python Extension not found'); + if (activate) { + await pythonExt?.activate(); + } + } + + async function ensureBlackExt(activate?: boolean): Promise { + const extension = vscode.extensions.getExtension('ms-python.black-formatter'); + assert.ok(extension, 'Black Formatter Extension not found'); + if (activate) { + await extension?.activate(); + } + } + + test('Ensure Black Formatter Extension loads', async () => { + await vscode.workspace.openTextDocument(path.join(TEST_PROJECT_DIR, 'myscript.py')); + + await ensurePythonExt(true); + await ensureBlackExt(false); + + const extension = vscode.extensions.getExtension('ms-python.black-formatter'); + if (extension) { + let timeout = TIMEOUT; + while (!extension.isActive && timeout > 0) { + await new Promise((resolve) => setTimeout(resolve, 100)); + timeout -= 100; + } + assert.ok(extension.isActive, `Extension not activated in ${TIMEOUT / 1000} seconds`); + } + }); + + test('Ensure Black Formatter formats a file on save', async () => { + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await ensurePythonExt(true); + const scriptPath = path.join(TEST_PROJECT_DIR, 'myscript.py'); + + const unformatted = await fsapi.readFile(path.join(TEST_PROJECT_DIR, 'myscript.unformatted'), { + encoding: 'utf8', + }); + const formatted = await fsapi.readFile(path.join(TEST_PROJECT_DIR, 'myscript.formatted'), { encoding: 'utf8' }); + await fsapi.writeFile(scriptPath, unformatted, { encoding: 'utf8' }); + + await ensureBlackExt(true); + + const doc = await vscode.workspace.openTextDocument(scriptPath); + await vscode.window.showTextDocument(doc); + + const editor = vscode.window.activeTextEditor; + assert.ok(editor, 'No active editor'); + assert.ok(editor?.document.uri.fsPath.endsWith('myscript.py'), 'Active editor is not myscript.py'); + + const formatDone = new Promise((resolve) => { + const watcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(TEST_PROJECT_DIR, 'myscript.py'), + true, // We don't need create events + false, // We need change events + true, // We don't need delete events + ); + disposables.push( + watcher, + watcher.onDidChange((e) => { + const text = fsapi.readFileSync(e.fsPath, { encoding: 'utf8' }); + if (!text.includes(';')) { + console.log('Saved with format changes'); + resolve(); + } else { + console.log('Saved without format changes'); + } + }), + ); + }); + + const timer = setInterval(() => { + console.log('Saving file'); + vscode.commands.executeCommand('workbench.action.files.save'); + }, 1000); + disposables.push({ dispose: () => clearInterval(timer) }); + + await vscode.commands.executeCommand('workbench.action.files.save'); + await formatDone; + const actualText = await fsapi.readFile(scriptPath, { encoding: 'utf8' }); + assert.equal(actualText, formatted); + + //cleanup + await fsapi.writeFile(scriptPath, '', { encoding: 'utf8' }); + }); +});