Skip to content

Commit e1be8cc

Browse files
Merge pull request #5 from microsoft/Add-unit-tests
Add unit tests
2 parents 5c87160 + 424d71f commit e1be8cc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+6521
-1
lines changed

.vscode/tasks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
{
77
"type": "npm",
88
"script": "watch",
9-
"problemMatcher": "$ts-webpack-watch",
9+
"problemMatcher": "$tsc-watch",
1010
"isBackground": true,
1111
"presentation": {
1212
"reveal": "never",

src/test/ciConstants.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
//
7+
// Constants that pertain to CI processes/tests only. No dependencies on vscode!
8+
//
9+
export const PYTHON_VIRTUAL_ENVS_LOCATION = process.env.PYTHON_VIRTUAL_ENVS_LOCATION;
10+
export const IS_APPVEYOR = process.env.APPVEYOR === 'true';
11+
export const IS_TRAVIS = process.env.TRAVIS === 'true';
12+
export const IS_VSTS = process.env.TF_BUILD !== undefined;
13+
export const IS_GITHUB_ACTIONS = process.env.GITHUB_ACTIONS === 'true';
14+
export const IS_CI_SERVER = IS_TRAVIS || IS_APPVEYOR || IS_VSTS || IS_GITHUB_ACTIONS;
15+
16+
// Control JUnit-style output logging for reporting purposes.
17+
let reportJunit: boolean = false;
18+
if (IS_CI_SERVER && process.env.MOCHA_REPORTER_JUNIT !== undefined) {
19+
reportJunit = process.env.MOCHA_REPORTER_JUNIT.toLowerCase() === 'true';
20+
}
21+
export const MOCHA_REPORTER_JUNIT: boolean = reportJunit;
22+
export const IS_CI_SERVER_TEST_DEBUGGER = process.env.IS_CI_SERVER_TEST_DEBUGGER === '1';

src/test/common.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
'use strict';
4+
5+
// IMPORTANT: Do not import anything from the 'client' folder in this file as that folder is not available during smoke tests.
6+
7+
import * as fs from 'fs-extra';
8+
import { ConfigurationTarget, TextDocument, Uri } from 'vscode';
9+
import { IS_MULTI_ROOT_TEST } from './constants';
10+
import { IExtensionApi } from '../extension/apiTypes';
11+
import { assert } from 'chai';
12+
13+
export const PYTHON_PATH = getPythonPath();
14+
15+
export async function clearPythonPathInWorkspaceFolder(resource: string | Uri) {
16+
const vscode = require('vscode') as typeof import('vscode');
17+
return retryAsync(setPythonPathInWorkspace)(resource, vscode.ConfigurationTarget.WorkspaceFolder);
18+
}
19+
20+
export async function setPythonPathInWorkspaceRoot(pythonPath: string) {
21+
const vscode = require('vscode') as typeof import('vscode');
22+
return retryAsync(setPythonPathInWorkspace)(undefined, vscode.ConfigurationTarget.Workspace, pythonPath);
23+
}
24+
25+
export const resetGlobalPythonPathSetting = async () => retryAsync(restoreGlobalPythonPathSetting)();
26+
27+
export async function openFile(file: string): Promise<TextDocument> {
28+
const vscode = require('vscode') as typeof import('vscode');
29+
const textDocument = await vscode.workspace.openTextDocument(file);
30+
await vscode.window.showTextDocument(textDocument);
31+
assert(vscode.window.activeTextEditor, 'No active editor');
32+
return textDocument;
33+
}
34+
35+
export function retryAsync(this: any, wrapped: Function, retryCount: number = 2) {
36+
return async (...args: any[]) => {
37+
return new Promise((resolve, reject) => {
38+
const reasons: any[] = [];
39+
40+
const makeCall = () => {
41+
wrapped.call(this as Function, ...args).then(resolve, (reason: any) => {
42+
reasons.push(reason);
43+
if (reasons.length >= retryCount) {
44+
reject(reasons);
45+
} else {
46+
// If failed once, lets wait for some time before trying again.
47+
setTimeout(makeCall, 500);
48+
}
49+
});
50+
};
51+
52+
makeCall();
53+
});
54+
};
55+
}
56+
57+
async function setPythonPathInWorkspace(
58+
resource: string | Uri | undefined,
59+
config: ConfigurationTarget,
60+
pythonPath?: string,
61+
) {
62+
const vscode = require('vscode') as typeof import('vscode');
63+
if (config === vscode.ConfigurationTarget.WorkspaceFolder && !IS_MULTI_ROOT_TEST) {
64+
return;
65+
}
66+
const resourceUri = typeof resource === 'string' ? vscode.Uri.file(resource) : resource;
67+
const settings = vscode.workspace.getConfiguration('python', resourceUri || null);
68+
const value = settings.inspect<string>('defaultInterpreterPath');
69+
const prop: 'workspaceFolderValue' | 'workspaceValue' =
70+
config === vscode.ConfigurationTarget.Workspace ? 'workspaceValue' : 'workspaceFolderValue';
71+
if (value && value[prop] !== pythonPath) {
72+
await settings.update('defaultInterpreterPath', pythonPath, config);
73+
// await disposePythonSettings();
74+
}
75+
}
76+
async function restoreGlobalPythonPathSetting(): Promise<void> {
77+
const vscode = require('vscode') as typeof import('vscode');
78+
const pythonConfig = vscode.workspace.getConfiguration('python', null as any as Uri);
79+
await Promise.all([
80+
pythonConfig.update('defaultInterpreterPath', undefined, true),
81+
pythonConfig.update('defaultInterpreterPath', undefined, true),
82+
]);
83+
// await disposePythonSettings();
84+
}
85+
86+
function getPythonPath(): string {
87+
if (process.env.CI_PYTHON_PATH && fs.existsSync(process.env.CI_PYTHON_PATH)) {
88+
return process.env.CI_PYTHON_PATH;
89+
}
90+
91+
// TODO: Change this to python3.
92+
// See https://github.com/microsoft/vscode-python/issues/10910.
93+
return 'python';
94+
}
95+
96+
export interface IExtensionTestApi extends IExtensionApi {}

src/test/constants.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as path from 'path';
5+
import { IS_CI_SERVER, IS_CI_SERVER_TEST_DEBUGGER } from './ciConstants';
6+
7+
// Activating extension for Multiroot and Debugger CI tests for Windows takes just over 2 minutes sometimes, so 3 minutes seems like a safe margin
8+
export const MAX_EXTENSION_ACTIVATION_TIME = 180_000;
9+
export const TEST_TIMEOUT = 60_000;
10+
export const TEST_RETRYCOUNT = 3;
11+
export const IS_SMOKE_TEST = process.env.VSC_PYTHON_SMOKE_TEST === '1';
12+
export const IS_PERF_TEST = process.env.VSC_PYTHON_PERF_TEST === '1';
13+
export const IS_MULTI_ROOT_TEST = isMultitrootTest();
14+
15+
// If running on CI server, then run debugger tests ONLY if the corresponding flag is enabled.
16+
export const TEST_DEBUGGER = IS_CI_SERVER ? IS_CI_SERVER_TEST_DEBUGGER : true;
17+
18+
function isMultitrootTest() {
19+
// No need to run smoke nor perf tests in a multi-root environment.
20+
if (IS_SMOKE_TEST || IS_PERF_TEST) {
21+
return false;
22+
}
23+
try {
24+
const vscode = require('vscode');
25+
const workspace = vscode.workspace;
26+
return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 1;
27+
} catch {
28+
// being accessed, when VS Code hasn't been launched.
29+
return false;
30+
}
31+
}
32+
33+
export const EXTENSION_ROOT_DIR_FOR_TESTS = path.join(__dirname, '..', '..');
34+
export const PVSC_EXTENSION_ID_FOR_TESTS = 'ms-python.python';
35+
36+
export const SMOKE_TEST_EXTENSIONS_DIR = path.join(
37+
EXTENSION_ROOT_DIR_FOR_TESTS,
38+
'tmp',
39+
'ext',
40+
'smokeTestExtensionsFolder',
41+
);

src/test/core.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
// File without any dependencies on VS Code.
7+
8+
export async function sleep(milliseconds: number) {
9+
return new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
10+
}
11+
12+
export function noop() {}
13+
14+
export const isWindows = /^win/.test(process.platform);

src/test/fixtures.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as fs from 'fs-extra';
5+
import { PYTHON_PATH } from './common';
6+
import { sleep } from './core';
7+
import { Proc, spawn } from './proc';
8+
9+
export type CleanupFunc = (() => void) | (() => Promise<void>);
10+
11+
export class CleanupFixture {
12+
private cleanups: CleanupFunc[];
13+
constructor() {
14+
this.cleanups = [];
15+
}
16+
17+
public addCleanup(cleanup: CleanupFunc) {
18+
this.cleanups.push(cleanup);
19+
}
20+
public addFSCleanup(filename: string) {
21+
this.addCleanup(async () => {
22+
try {
23+
await fs.unlink(filename);
24+
} catch {
25+
// The file is already gone.
26+
}
27+
});
28+
}
29+
30+
public async cleanUp() {
31+
const cleanups = this.cleanups;
32+
this.cleanups = [];
33+
34+
return Promise.all(
35+
cleanups.map(async (cleanup, i) => {
36+
try {
37+
const res = cleanup();
38+
if (res) {
39+
await res;
40+
}
41+
} catch (err) {
42+
console.error(`cleanup ${i + 1} failed: ${err}`);
43+
44+
console.error('moving on...');
45+
}
46+
}),
47+
);
48+
}
49+
}
50+
51+
export class PythonFixture extends CleanupFixture {
52+
public readonly python: string;
53+
constructor(
54+
// If not provided, we will use the global default.
55+
python?: string,
56+
) {
57+
super();
58+
if (python) {
59+
this.python = python;
60+
} else {
61+
this.python = PYTHON_PATH;
62+
}
63+
}
64+
65+
public runScript(filename: string, ...args: string[]): Proc {
66+
return this.spawn(filename, ...args);
67+
}
68+
69+
public runModule(name: string, ...args: string[]): Proc {
70+
return this.spawn('-m', name, ...args);
71+
}
72+
73+
private spawn(...args: string[]) {
74+
const proc = spawn(this.python, ...args);
75+
this.addCleanup(async () => {
76+
if (!proc.exited) {
77+
await sleep(1000); // Wait a sec before the hammer falls.
78+
try {
79+
proc.raw.kill();
80+
} catch {
81+
// It already finished.
82+
}
83+
}
84+
});
85+
return proc;
86+
}
87+
}

src/test/initialize.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import * as path from 'path';
2+
import * as vscode from 'vscode';
3+
import { IExtensionApi } from '../extension/apiTypes';
4+
import {
5+
clearPythonPathInWorkspaceFolder,
6+
IExtensionTestApi,
7+
PYTHON_PATH,
8+
resetGlobalPythonPathSetting,
9+
setPythonPathInWorkspaceRoot,
10+
} from './common';
11+
import { PVSC_EXTENSION_ID_FOR_TESTS } from './constants';
12+
import { sleep } from './core';
13+
14+
export * from './constants';
15+
export * from './ciConstants';
16+
17+
const dummyPythonFile = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py');
18+
export const multirootPath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc');
19+
const workspace3Uri = vscode.Uri.file(path.join(multirootPath, 'workspace3'));
20+
21+
//First thing to be executed.
22+
process.env.VSC_PYTHON_CI_TEST = '1';
23+
24+
// Ability to use custom python environments for testing
25+
export async function initializePython() {
26+
await resetGlobalPythonPathSetting();
27+
await clearPythonPathInWorkspaceFolder(dummyPythonFile);
28+
await clearPythonPathInWorkspaceFolder(workspace3Uri);
29+
await setPythonPathInWorkspaceRoot(PYTHON_PATH);
30+
}
31+
32+
export async function initialize(): Promise<IExtensionTestApi> {
33+
await initializePython();
34+
const api = await activateExtension();
35+
// if (!IS_SMOKE_TEST) {
36+
// // When running smoke tests, we won't have access to these.
37+
// const configSettings = await import('../client/common/configSettings');
38+
// // Dispose any cached python settings (used only in test env).
39+
// configSettings.PythonSettings.dispose();
40+
// }
41+
42+
return api as any as IExtensionTestApi;
43+
}
44+
export async function activateExtension() {
45+
const extension = vscode.extensions.getExtension<IExtensionApi>(PVSC_EXTENSION_ID_FOR_TESTS)!;
46+
const api = await extension.activate();
47+
return api;
48+
}
49+
50+
export async function initializeTest(): Promise<any> {
51+
await initializePython();
52+
await closeActiveWindows();
53+
// if (!IS_SMOKE_TEST) {
54+
// // When running smoke tests, we won't have access to these.
55+
// const configSettings = await import('../client/common/configSettings');
56+
// // Dispose any cached python settings (used only in test env).
57+
// configSettings.PythonSettings.dispose();
58+
// }
59+
}
60+
export async function closeActiveWindows(): Promise<void> {
61+
await closeActiveNotebooks();
62+
await closeWindowsInteral();
63+
}
64+
export async function closeActiveNotebooks(): Promise<void> {
65+
if (!vscode.env.appName.toLowerCase().includes('insiders') || !isANotebookOpen()) {
66+
return;
67+
}
68+
// We could have untitled notebooks, close them by reverting changes.
69+
70+
while ((vscode as any).window.activeNotebookEditor || vscode.window.activeTextEditor) {
71+
await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor');
72+
}
73+
// Work around VS Code issues (sometimes notebooks do not get closed).
74+
// Hence keep trying.
75+
for (let counter = 0; counter <= 5 && isANotebookOpen(); counter += 1) {
76+
await sleep(counter * 100);
77+
await closeWindowsInteral();
78+
}
79+
}
80+
81+
async function closeWindowsInteral() {
82+
return new Promise<void>((resolve, reject) => {
83+
// Attempt to fix #1301.
84+
// Lets not waste too much time.
85+
const timer = setTimeout(() => {
86+
reject(new Error("Command 'workbench.action.closeAllEditors' timed out"));
87+
}, 15000);
88+
vscode.commands.executeCommand('workbench.action.closeAllEditors').then(
89+
() => {
90+
clearTimeout(timer);
91+
resolve();
92+
},
93+
(ex) => {
94+
clearTimeout(timer);
95+
reject(ex);
96+
},
97+
);
98+
});
99+
}
100+
101+
function isANotebookOpen() {
102+
if (!vscode.window.activeTextEditor?.document) {
103+
return false;
104+
}
105+
106+
return !!(vscode.window.activeTextEditor.document as any).notebook;
107+
}

0 commit comments

Comments
 (0)