From eb9fde3100e9c78363d803b083370315bc202d58 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 10 May 2023 09:03:23 -0700 Subject: [PATCH 0001/1136] Add `createEnvironment.contentButton` setting (#21212) Closes https://github.com/microsoft/vscode-python/issues/20982 --------- Co-authored-by: Luciana Abud <45497113+luabud@users.noreply.github.com> --- package.json | 17 +- package.nls.json | 1 + src/client/common/vscodeApis/workspaceApis.ts | 8 +- src/client/extensionActivation.ts | 4 +- ...CreateEnv.ts => createEnvButtonContext.ts} | 33 +- .../createEnvButtonContext.unit.test.ts | 346 ++++++++++++++++++ .../pyprojectTomlCreateEnv.unit.test.ts | 216 ----------- 7 files changed, 395 insertions(+), 230 deletions(-) rename src/client/pythonEnvironments/creation/{pyprojectTomlCreateEnv.ts => createEnvButtonContext.ts} (50%) create mode 100644 src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts delete mode 100644 src/test/pythonEnvironments/creation/pyprojectTomlCreateEnv.unit.test.ts diff --git a/package.json b/package.json index 34d76ae4d9e8..fd92481c7f84 100644 --- a/package.json +++ b/package.json @@ -404,6 +404,19 @@ "type": "array", "uniqueItems": true }, + "python.createEnvironment.contentButton": { + "default": "show", + "markdownDescription": "%python.createEnvironment.contentButton.description%", + "scope": "machine-overridable", + "type": "string", + "enum": [ + "show", + "hide" + ], + "tags": [ + "experimental" + ] + }, "python.condaPath": { "default": "", "description": "%python.condaPath.description%", @@ -1707,12 +1720,12 @@ { "group": "Python", "command": "python.createEnvironment-button", - "when": "resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" + "when": "showCreateEnvButton && resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" }, { "group": "Python", "command": "python.createEnvironment-button", - "when": "resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" + "when": "showCreateEnvButton && resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" } ], "editor/context": [ diff --git a/package.nls.json b/package.nls.json index cfbbeb7d41d5..187b0d832743 100644 --- a/package.nls.json +++ b/package.nls.json @@ -25,6 +25,7 @@ "python.command.python.analysis.restartLanguageServer.title": "Restart Language Server", "python.command.python.launchTensorBoard.title": "Launch TensorBoard", "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", + "python.createEnvironment.contentButton.description": "Show or hide Create Environment button in the editor for `requirements.txt` or other dependency files.", "python.menu.createNewFile.title": "Python File", "python.editor.context.submenu.runPython": "Run Python", "python.editor.context.submenu.runPythonInteractive": "Run in Interactive window", diff --git a/src/client/common/vscodeApis/workspaceApis.ts b/src/client/common/vscodeApis/workspaceApis.ts index 74200ba46924..20528c17ec11 100644 --- a/src/client/common/vscodeApis/workspaceApis.ts +++ b/src/client/common/vscodeApis/workspaceApis.ts @@ -48,10 +48,14 @@ export function getOpenTextDocuments(): readonly vscode.TextDocument[] { return vscode.workspace.textDocuments; } -export function onDidOpenTextDocument(handler: (doc: vscode.TextDocument) => void): vscode.Disposable { +export function onDidOpenTextDocument(handler: (doc: vscode.TextDocument) => unknown): vscode.Disposable { return vscode.workspace.onDidOpenTextDocument(handler); } -export function onDidChangeTextDocument(handler: (e: vscode.TextDocumentChangeEvent) => void): vscode.Disposable { +export function onDidChangeTextDocument(handler: (e: vscode.TextDocumentChangeEvent) => unknown): vscode.Disposable { return vscode.workspace.onDidChangeTextDocument(handler); } + +export function onDidChangeConfiguration(handler: (e: vscode.ConfigurationChangeEvent) => unknown): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration(handler); +} diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index ba7bdf61c8f2..542a6ccc3010 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -54,7 +54,7 @@ import { DynamicPythonDebugConfigurationService } from './debugger/extension/con import { registerCreateEnvironmentFeatures } from './pythonEnvironments/creation/createEnvApi'; import { IInterpreterQuickPick } from './interpreter/configuration/types'; import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt'; -import { registerPyProjectTomlCreateEnvFeatures } from './pythonEnvironments/creation/pyprojectTomlCreateEnv'; +import { registerCreateEnvButtonFeatures } from './pythonEnvironments/creation/createEnvButtonContext'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -98,7 +98,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components): ); const pathUtils = ext.legacyIOC.serviceContainer.get(IPathUtils); registerCreateEnvironmentFeatures(ext.disposables, interpreterQuickPick, interpreterPathService, pathUtils); - registerPyProjectTomlCreateEnvFeatures(ext.disposables); + registerCreateEnvButtonFeatures(ext.disposables); } /// ////////////////////////// diff --git a/src/client/pythonEnvironments/creation/pyprojectTomlCreateEnv.ts b/src/client/pythonEnvironments/creation/createEnvButtonContext.ts similarity index 50% rename from src/client/pythonEnvironments/creation/pyprojectTomlCreateEnv.ts rename to src/client/pythonEnvironments/creation/createEnvButtonContext.ts index 5ead37b80dc9..cd009bc6118a 100644 --- a/src/client/pythonEnvironments/creation/pyprojectTomlCreateEnv.ts +++ b/src/client/pythonEnvironments/creation/createEnvButtonContext.ts @@ -8,6 +8,8 @@ import { onDidOpenTextDocument, onDidChangeTextDocument, getOpenTextDocuments, + getConfiguration, + onDidChangeConfiguration, } from '../../common/vscodeApis/workspaceApis'; import { isPipInstallableToml } from './provider/venvUtils'; @@ -19,7 +21,13 @@ async function setPyProjectTomlContextKey(doc: TextDocument): Promise { } } -export function registerPyProjectTomlCreateEnvFeatures(disposables: IDisposableRegistry): void { +async function setShowCreateEnvButtonContextKey(): Promise { + const config = getConfiguration('python'); + const showCreateEnvButton = config.get('createEnvironment.contentButton', 'show') === 'show'; + await executeCommand('setContext', 'showCreateEnvButton', showCreateEnvButton); +} + +export function registerCreateEnvButtonFeatures(disposables: IDisposableRegistry): void { disposables.push( onDidOpenTextDocument(async (doc: TextDocument) => { if (doc.fileName.endsWith('pyproject.toml')) { @@ -27,15 +35,24 @@ export function registerPyProjectTomlCreateEnvFeatures(disposables: IDisposableR } }), onDidChangeTextDocument(async (e: TextDocumentChangeEvent) => { - if (e.document.fileName.endsWith('pyproject.toml')) { - await setPyProjectTomlContextKey(e.document); + const doc = e.document; + if (doc.fileName.endsWith('pyproject.toml')) { + await setPyProjectTomlContextKey(doc); } }), + onDidChangeConfiguration(async () => { + await setShowCreateEnvButtonContextKey(); + }), ); - getOpenTextDocuments().forEach(async (doc: TextDocument) => { - if (doc.fileName.endsWith('pyproject.toml')) { - await setPyProjectTomlContextKey(doc); - } - }); + setShowCreateEnvButtonContextKey(); + + const docs = getOpenTextDocuments().filter( + (doc) => doc.fileName.endsWith('pyproject.toml') && isPipInstallableToml(doc.getText()), + ); + if (docs.length > 0) { + executeCommand('setContext', 'pipInstallableToml', true); + } else { + executeCommand('setContext', 'pipInstallableToml', false); + } } diff --git a/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts new file mode 100644 index 000000000000..31842420dd59 --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { TextDocument, TextDocumentChangeEvent, WorkspaceConfiguration } from 'vscode'; +import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { registerCreateEnvButtonFeatures } from '../../../client/pythonEnvironments/creation/createEnvButtonContext'; + +chaiUse(chaiAsPromised); + +class FakeDisposable { + public dispose() { + // Do nothing + } +} + +function getInstallableToml(): typemoq.IMock { + const pyprojectTomlPath = 'pyproject.toml'; + const pyprojectToml = typemoq.Mock.ofType(); + pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); + pyprojectToml + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + ); + return pyprojectToml; +} + +function getNonInstallableToml(): typemoq.IMock { + const pyprojectTomlPath = 'pyproject.toml'; + const pyprojectToml = typemoq.Mock.ofType(); + pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); + pyprojectToml + .setup((p) => p.getText(typemoq.It.isAny())) + .returns(() => '[project]\nname = "spam"\nversion = "2020.0.0"\n'); + return pyprojectToml; +} + +function getSomeFile(): typemoq.IMock { + const someFilePath = 'something.py'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); + return someFile; +} + +suite('PyProject.toml Create Env Features', () => { + let executeCommandStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let getOpenTextDocumentsStub: sinon.SinonStub; + let onDidOpenTextDocumentStub: sinon.SinonStub; + let onDidChangeTextDocumentStub: sinon.SinonStub; + let onDidChangeConfigurationStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); + onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); + onDidChangeTextDocumentStub = sinon.stub(workspaceApis, 'onDidChangeTextDocument'); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + onDidChangeConfigurationStub = sinon.stub(workspaceApis, 'onDidChangeConfiguration'); + + onDidOpenTextDocumentStub.returns(new FakeDisposable()); + onDidChangeTextDocumentStub.returns(new FakeDisposable()); + onDidChangeConfigurationStub.returns(new FakeDisposable()); + + configMock = typemoq.Mock.ofType(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + getConfigurationStub.returns(configMock.object); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('python.createEnvironment.contentButton setting is set to "show", no files open', async () => { + getOpenTextDocumentsStub.returns([]); + + registerCreateEnvButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('python.createEnvironment.contentButton setting is set to "hide", no files open', async () => { + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + getOpenTextDocumentsStub.returns([]); + + registerCreateEnvButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('python.createEnvironment.contentButton setting changed from "hide" to "show"', async () => { + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + getOpenTextDocumentsStub.returns([]); + + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeConfigurationStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + handler(); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + }); + + test('python.createEnvironment.contentButton setting changed from "show" to "hide"', async () => { + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + getOpenTextDocumentsStub.returns([]); + + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeConfigurationStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + configMock.reset(); + configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + handler(); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + }); + + test('Installable pyproject.toml is already open in the editor on extension activate', async () => { + const pyprojectToml = getInstallableToml(); + getOpenTextDocumentsStub.returns([pyprojectToml.object]); + + registerCreateEnvButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non installable pyproject.toml is already open in the editor on extension activate', async () => { + const pyprojectToml = getNonInstallableToml(); + getOpenTextDocumentsStub.returns([pyprojectToml.object]); + + registerCreateEnvButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Some random file open in the editor on extension activate', async () => { + const someFile = getSomeFile(); + getOpenTextDocumentsStub.returns([someFile.object]); + + registerCreateEnvButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Installable pyproject.toml is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getInstallableToml(); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', true)); + + handler(pyprojectToml.object); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non Installable pyproject.toml is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getNonInstallableToml(); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Some random file is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const someFile = getSomeFile(); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(someFile.object); + + assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', false)); + }); + + test('Installable pyproject.toml is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocumentChangeEvent) => void = () => { + /* do nothing */ + }; + onDidChangeTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getInstallableToml(); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + + handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined }); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non Installable pyproject.toml is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocumentChangeEvent) => void = () => { + /* do nothing */ + }; + onDidChangeTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getNonInstallableToml(); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined }); + + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Non Installable pyproject.toml is changed to Installable', async () => { + getOpenTextDocumentsStub.returns([]); + + let openHandler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + openHandler = callback; + return new FakeDisposable(); + }); + + let changeHandler: (d: TextDocumentChangeEvent) => void = () => { + /* do nothing */ + }; + onDidChangeTextDocumentStub.callsFake((callback) => { + changeHandler = callback; + return new FakeDisposable(); + }); + + const nonInatallablePyprojectToml = getNonInstallableToml(); + const installablePyprojectToml = getInstallableToml(); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + openHandler(nonInatallablePyprojectToml.object); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + changeHandler({ contentChanges: [], document: installablePyprojectToml.object, reason: undefined }); + + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Some random file is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocumentChangeEvent) => void = () => { + /* do nothing */ + }; + onDidChangeTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const someFile = getSomeFile(); + + registerCreateEnvButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler({ contentChanges: [], document: someFile.object, reason: undefined }); + + assert.ok(executeCommandStub.notCalled); + }); +}); diff --git a/src/test/pythonEnvironments/creation/pyprojectTomlCreateEnv.unit.test.ts b/src/test/pythonEnvironments/creation/pyprojectTomlCreateEnv.unit.test.ts deleted file mode 100644 index 3f19aa5775b3..000000000000 --- a/src/test/pythonEnvironments/creation/pyprojectTomlCreateEnv.unit.test.ts +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinon from 'sinon'; -import * as typemoq from 'typemoq'; -import { assert, use as chaiUse } from 'chai'; -import { TextDocument, TextDocumentChangeEvent } from 'vscode'; -import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; -import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; -import { IDisposableRegistry } from '../../../client/common/types'; -import { registerPyProjectTomlCreateEnvFeatures } from '../../../client/pythonEnvironments/creation/pyprojectTomlCreateEnv'; - -chaiUse(chaiAsPromised); - -class FakeDisposable { - public dispose() { - // Do nothing - } -} - -function getInstallableToml(): typemoq.IMock { - const pyprojectTomlPath = 'pyproject.toml'; - const pyprojectToml = typemoq.Mock.ofType(); - pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); - pyprojectToml - .setup((p) => p.getText(typemoq.It.isAny())) - .returns( - () => - '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', - ); - return pyprojectToml; -} - -function getNonInstallableToml(): typemoq.IMock { - const pyprojectTomlPath = 'pyproject.toml'; - const pyprojectToml = typemoq.Mock.ofType(); - pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); - pyprojectToml - .setup((p) => p.getText(typemoq.It.isAny())) - .returns(() => '[project]\nname = "spam"\nversion = "2020.0.0"\n'); - return pyprojectToml; -} - -function getSomeFile(): typemoq.IMock { - const someFilePath = 'something.py'; - const someFile = typemoq.Mock.ofType(); - someFile.setup((p) => p.fileName).returns(() => someFilePath); - someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); - return someFile; -} - -suite('PyProject.toml Create Env Features', () => { - let executeCommandStub: sinon.SinonStub; - const disposables: IDisposableRegistry = []; - let getOpenTextDocumentsStub: sinon.SinonStub; - let onDidOpenTextDocumentStub: sinon.SinonStub; - let onDidChangeTextDocumentStub: sinon.SinonStub; - - setup(() => { - executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); - getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); - onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); - onDidChangeTextDocumentStub = sinon.stub(workspaceApis, 'onDidChangeTextDocument'); - - onDidOpenTextDocumentStub.returns(new FakeDisposable()); - onDidChangeTextDocumentStub.returns(new FakeDisposable()); - }); - - teardown(() => { - sinon.restore(); - disposables.forEach((d) => d.dispose()); - }); - - test('Installable pyproject.toml is already open in the editor on extension activate', async () => { - const pyprojectToml = getInstallableToml(); - getOpenTextDocumentsStub.returns([pyprojectToml.object]); - - registerPyProjectTomlCreateEnvFeatures(disposables); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); - }); - - test('Non installable pyproject.toml is already open in the editor on extension activate', async () => { - const pyprojectToml = getNonInstallableToml(); - getOpenTextDocumentsStub.returns([pyprojectToml.object]); - - registerPyProjectTomlCreateEnvFeatures(disposables); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); - }); - - test('Some random file open in the editor on extension activate', async () => { - const someFile = getSomeFile(); - getOpenTextDocumentsStub.returns([someFile.object]); - - registerPyProjectTomlCreateEnvFeatures(disposables); - - assert.ok(executeCommandStub.notCalled); - }); - - test('Installable pyproject.toml is opened in the editor', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (doc: TextDocument) => void = () => { - /* do nothing */ - }; - onDidOpenTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getInstallableToml(); - - registerPyProjectTomlCreateEnvFeatures(disposables); - handler(pyprojectToml.object); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); - }); - - test('Non Installable pyproject.toml is opened in the editor', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (doc: TextDocument) => void = () => { - /* do nothing */ - }; - onDidOpenTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getNonInstallableToml(); - - registerPyProjectTomlCreateEnvFeatures(disposables); - handler(pyprojectToml.object); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); - }); - - test('Some random file is opened in the editor', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (doc: TextDocument) => void = () => { - /* do nothing */ - }; - onDidOpenTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const someFile = getSomeFile(); - - registerPyProjectTomlCreateEnvFeatures(disposables); - handler(someFile.object); - - assert.ok(executeCommandStub.notCalled); - }); - - test('Installable pyproject.toml is changed', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (d: TextDocumentChangeEvent) => void = () => { - /* do nothing */ - }; - onDidChangeTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getInstallableToml(); - - registerPyProjectTomlCreateEnvFeatures(disposables); - handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined }); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); - }); - - test('Non Installable pyproject.toml is changed', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (d: TextDocumentChangeEvent) => void = () => { - /* do nothing */ - }; - onDidChangeTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getNonInstallableToml(); - - registerPyProjectTomlCreateEnvFeatures(disposables); - handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined }); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); - }); - - test('Some random file is changed', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (d: TextDocumentChangeEvent) => void = () => { - /* do nothing */ - }; - onDidChangeTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const someFile = getSomeFile(); - - registerPyProjectTomlCreateEnvFeatures(disposables); - handler({ contentChanges: [], document: someFile.object, reason: undefined }); - - assert.ok(executeCommandStub.notCalled); - }); -}); From 15338189257cdf264a9a00966c8909d6a06123e8 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 10 May 2023 16:09:01 -0700 Subject: [PATCH 0002/1136] Added option to run multiple Python files in separate terminals (#21223) Closes https://github.com/microsoft/vscode-python/issues/21215 https://github.com/microsoft/vscode-python/issues/14094 Added the option to assign a dedicated terminal for each Python file: ![image](https://github.com/microsoft/vscode-python/assets/13199757/b01248e4-c826-4de0-b15f-cde959965e68) --- package.json | 19 +++++++ package.nls.json | 1 + src/client/common/constants.ts | 1 + src/client/common/terminal/factory.ts | 16 ++++-- src/client/common/terminal/types.ts | 2 +- src/client/telemetry/index.ts | 6 ++ .../codeExecution/codeExecutionManager.ts | 56 ++++++++++++------- .../codeExecution/terminalCodeExecution.ts | 19 +++---- src/client/terminals/types.ts | 2 +- .../common/terminals/factory.unit.test.ts | 47 +++++++++++++++- .../codeExecutionManager.unit.test.ts | 25 ++++++--- 11 files changed, 147 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index fd92481c7f84..df393d7934d2 100644 --- a/package.json +++ b/package.json @@ -304,6 +304,12 @@ "icon": "$(play)", "title": "%python.command.python.execInTerminalIcon.title%" }, + { + "category": "Python", + "command": "python.execInNewTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInNewTerminal.title%" + }, { "category": "Python", "command": "python.debugInTerminal", @@ -1626,6 +1632,13 @@ "title": "%python.command.python.execInTerminalIcon.title%", "when": "false && editorLangId == python" }, + { + "category": "Python", + "command": "python.execInNewTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInNewTerminal.title%", + "when": "false && editorLangId == python" + }, { "category": "Python", "command": "python.debugInTerminal", @@ -1784,6 +1797,12 @@ "title": "%python.command.python.execInTerminalIcon.title%", "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" }, + { + "command": "python.execInNewTerminal", + "group": "navigation@0", + "title": "%python.command.python.execInNewTerminal.title%", + "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" + }, { "command": "python.debugInTerminal", "group": "navigation@1", diff --git a/package.nls.json b/package.nls.json index 187b0d832743..e8e0bb803d10 100644 --- a/package.nls.json +++ b/package.nls.json @@ -7,6 +7,7 @@ "python.command.python.execInTerminal.title": "Run Python File in Terminal", "python.command.python.debugInTerminal.title": "Debug Python File", "python.command.python.execInTerminalIcon.title": "Run Python File", + "python.command.python.execInNewTerminal.title": "Run Python File in Separate Terminal", "python.command.python.setInterpreter.title": "Select Interpreter", "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", "python.command.python.viewOutput.title": "Show Output", diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index b285667aaa6a..aae792ec2e39 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -44,6 +44,7 @@ export namespace Commands { export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; export const Exec_In_Terminal = 'python.execInTerminal'; export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; + export const Exec_In_Separate_Terminal = 'python.execInNewTerminal'; export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; export const GetSelectedInterpreterPath = 'python.interpreterPath'; diff --git a/src/client/common/terminal/factory.ts b/src/client/common/terminal/factory.ts index 3cbe123b5629..39cc88c4b024 100644 --- a/src/client/common/terminal/factory.ts +++ b/src/client/common/terminal/factory.ts @@ -24,14 +24,14 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { ) { this.terminalServices = new Map(); } - public getTerminalService(options: TerminalCreationOptions): ITerminalService { + public getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService { const resource = options?.resource; const title = options?.title; let terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; const interpreter = options?.interpreter; - const id = this.getTerminalId(terminalTitle, resource, interpreter); + const id = this.getTerminalId(terminalTitle, resource, interpreter, options.newTerminalPerFile); if (!this.terminalServices.has(id)) { - if (this.terminalServices.size >= 1 && resource) { + if (resource && options.newTerminalPerFile) { terminalTitle = `${terminalTitle}: ${path.basename(resource.fsPath).replace('.py', '')}`; } options.title = terminalTitle; @@ -51,13 +51,19 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { title = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; return new TerminalService(this.serviceContainer, { resource, title }); } - private getTerminalId(title: string, resource?: Uri, interpreter?: PythonEnvironment): string { + private getTerminalId( + title: string, + resource?: Uri, + interpreter?: PythonEnvironment, + newTerminalPerFile?: boolean, + ): string { if (!resource && !interpreter) { return title; } const workspaceFolder = this.serviceContainer .get(IWorkspaceService) .getWorkspaceFolder(resource || undefined); - return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}:${resource?.fsPath || ''}`; + const fileId = resource && newTerminalPerFile ? resource.fsPath : ''; + return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}:${fileId}`; } } diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts index 880bf0dd72fb..303188682378 100644 --- a/src/client/common/terminal/types.ts +++ b/src/client/common/terminal/types.ts @@ -97,7 +97,7 @@ export interface ITerminalServiceFactory { * @returns {ITerminalService} * @memberof ITerminalServiceFactory */ - getTerminalService(options: TerminalCreationOptions): ITerminalService; + getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService; createTerminalService(resource?: Uri, title?: string): ITerminalService; } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 5dff35067196..d0b9d463c070 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -825,6 +825,12 @@ export interface IEventNamePropertyMapping { * @type {('command' | 'icon')} */ trigger?: 'command' | 'icon'; + /** + * Whether user chose to execute this Python file in a separate terminal or not. + * + * @type {boolean} + */ + newTerminalPerFile?: boolean; }; /** * Telemetry Event sent when user executes code against Django Shell. diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index ed671f2846a2..9f1ba6e90d90 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -36,25 +36,31 @@ export class CodeExecutionManager implements ICodeExecutionManager { } public registerCommands() { - [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon].forEach((cmd) => { - this.disposableRegistry.push( - this.commandManager.registerCommand(cmd as any, async (file: Resource) => { - const interpreterService = this.serviceContainer.get(IInterpreterService); - const interpreter = await interpreterService.getActiveInterpreter(file); - if (!interpreter) { - this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); - return; - } - const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; - await this.executeFileInTerminal(file, trigger) - .then(() => { - if (this.shouldTerminalFocusOnStart(file)) - this.commandManager.executeCommand('workbench.action.terminal.focus'); + [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon, Commands.Exec_In_Separate_Terminal].forEach( + (cmd) => { + this.disposableRegistry.push( + this.commandManager.registerCommand(cmd as any, async (file: Resource) => { + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager + .executeCommand(Commands.TriggerEnvironmentSelection, file) + .then(noop, noop); + return; + } + const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; + await this.executeFileInTerminal(file, trigger, { + newTerminalPerFile: cmd === Commands.Exec_In_Separate_Terminal, }) - .catch((ex) => traceError('Failed to execute file in terminal', ex)); - }), - ); - }); + .then(() => { + if (this.shouldTerminalFocusOnStart(file)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); + }) + .catch((ex) => traceError('Failed to execute file in terminal', ex)); + }), + ); + }, + ); this.disposableRegistry.push( this.commandManager.registerCommand(Commands.Exec_Selection_In_Terminal as any, async (file: Resource) => { const interpreterService = this.serviceContainer.get(IInterpreterService); @@ -87,8 +93,16 @@ export class CodeExecutionManager implements ICodeExecutionManager { ), ); } - private async executeFileInTerminal(file: Resource, trigger: 'command' | 'icon') { - sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'file', trigger }); + private async executeFileInTerminal( + file: Resource, + trigger: 'command' | 'icon', + options?: { newTerminalPerFile: boolean }, + ): Promise { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { + scope: 'file', + trigger, + newTerminalPerFile: options?.newTerminalPerFile, + }); const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); file = file instanceof Uri ? file : undefined; let fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); @@ -110,7 +124,7 @@ export class CodeExecutionManager implements ICodeExecutionManager { } const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); - await executionService.executeFile(fileToExecute); + await executionService.executeFile(fileToExecute, options); } @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index b604d062e81e..ca7488530f75 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -19,7 +19,7 @@ import { ICodeExecutionService } from '../../terminals/types'; export class TerminalCodeExecutionProvider implements ICodeExecutionService { private hasRanOutsideCurrentDrive = false; protected terminalTitle!: string; - private replActive = new Map>(); + private replActive?: Promise; constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, @inject(IConfigurationService) protected readonly configurationService: IConfigurationService, @@ -29,13 +29,13 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { @inject(IInterpreterService) protected readonly interpreterService: IInterpreterService, ) {} - public async executeFile(file: Uri) { + public async executeFile(file: Uri, options?: { newTerminalPerFile: boolean }) { await this.setCwdForFileExecution(file); const { command, args } = await this.getExecuteFileArgs(file, [ file.fsPath.fileToCommandArgumentForPythonExt(), ]); - await this.getTerminalService(file).sendCommand(command, args); + await this.getTerminalService(file, options).sendCommand(command, args); } public async execute(code: string, resource?: Uri): Promise { @@ -48,26 +48,24 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { } public async initializeRepl(resource?: Uri) { const terminalService = this.getTerminalService(resource); - let replActive = this.replActive.get(resource?.fsPath || ''); - if (replActive && (await replActive)) { + if (this.replActive && (await this.replActive)) { await terminalService.show(); return; } - replActive = new Promise(async (resolve) => { + this.replActive = new Promise(async (resolve) => { const replCommandArgs = await this.getExecutableInfo(resource); terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args); // Give python repl time to start before we start sending text. setTimeout(() => resolve(true), 1000); }); - this.replActive.set(resource?.fsPath || '', replActive); this.disposables.push( terminalService.onDidCloseTerminal(() => { - this.replActive.delete(resource?.fsPath || ''); + this.replActive = undefined; }), ); - await replActive; + await this.replActive; } public async getExecutableInfo(resource?: Uri, args: string[] = []): Promise { @@ -83,10 +81,11 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise { return this.getExecutableInfo(resource, executeArgs); } - private getTerminalService(resource?: Uri): ITerminalService { + private getTerminalService(resource?: Uri, options?: { newTerminalPerFile: boolean }): ITerminalService { return this.terminalServiceFactory.getTerminalService({ resource, title: this.terminalTitle, + newTerminalPerFile: options?.newTerminalPerFile, }); } private async setCwdForFileExecution(file: Uri) { diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index cf31f4ef1dd0..47ac16d9e08b 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -8,7 +8,7 @@ export const ICodeExecutionService = Symbol('ICodeExecutionService'); export interface ICodeExecutionService { execute(code: string, resource?: Uri): Promise; - executeFile(file: Uri): Promise; + executeFile(file: Uri, options?: { newTerminalPerFile: boolean }): Promise; initializeRepl(resource?: Uri): Promise; } diff --git a/src/test/common/terminals/factory.unit.test.ts b/src/test/common/terminals/factory.unit.test.ts index f01d5a85fbb5..5ad2da8e793a 100644 --- a/src/test/common/terminals/factory.unit.test.ts +++ b/src/test/common/terminals/factory.unit.test.ts @@ -105,7 +105,7 @@ suite('Terminal Service Factory', () => { expect(notSameAsThirdInstance).to.not.equal(true, 'Instances are the same'); }); - test('Ensure different terminal is returned when using different resources from the same workspace', () => { + test('Ensure same terminal is returned when using different resources from the same workspace', () => { const file1A = Uri.file('1a'); const file2A = Uri.file('2a'); const fileB = Uri.file('b'); @@ -130,6 +130,51 @@ suite('Terminal Service Factory', () => { const terminalForFile2A = factory.getTerminalService({ resource: file2A }) as SynchronousTerminalService; const terminalForFileB = factory.getTerminalService({ resource: fileB }) as SynchronousTerminalService; + const terminalsAreSameForWorkspaceA = terminalForFile1A.terminalService === terminalForFile2A.terminalService; + expect(terminalsAreSameForWorkspaceA).to.equal(true, 'Instances are not the same for Workspace A'); + + const terminalsForWorkspaceABAreDifferent = + terminalForFile1A.terminalService === terminalForFileB.terminalService; + expect(terminalsForWorkspaceABAreDifferent).to.equal( + false, + 'Instances should be different for different workspaces', + ); + }); + + test('When `newTerminalPerFile` is true, ensure different terminal is returned when using different resources from the same workspace', () => { + const file1A = Uri.file('1a'); + const file2A = Uri.file('2a'); + const fileB = Uri.file('b'); + const workspaceUriA = Uri.file('A'); + const workspaceUriB = Uri.file('B'); + const workspaceFolderA = TypeMoq.Mock.ofType(); + workspaceFolderA.setup((w) => w.uri).returns(() => workspaceUriA); + const workspaceFolderB = TypeMoq.Mock.ofType(); + workspaceFolderB.setup((w) => w.uri).returns(() => workspaceUriB); + + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file1A))) + .returns(() => workspaceFolderA.object); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file2A))) + .returns(() => workspaceFolderA.object); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(fileB))) + .returns(() => workspaceFolderB.object); + + const terminalForFile1A = factory.getTerminalService({ + resource: file1A, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalForFile2A = factory.getTerminalService({ + resource: file2A, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalForFileB = factory.getTerminalService({ + resource: fileB, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalsAreSameForWorkspaceA = terminalForFile1A.terminalService === terminalForFile2A.terminalService; expect(terminalsAreSameForWorkspaceA).to.equal(false, 'Instances are the same for Workspace A'); diff --git a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts index 3676834873a0..30f95c94d217 100644 --- a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +++ b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -77,12 +77,15 @@ suite('Terminal - Code Execution Manager', () => { executionManager.registerCommands(); const sorted = registered.sort(); - expect(sorted).to.deep.equal([ - Commands.Exec_In_Terminal, - Commands.Exec_In_Terminal_Icon, - Commands.Exec_Selection_In_Django_Shell, - Commands.Exec_Selection_In_Terminal, - ]); + expect(sorted).to.deep.equal( + [ + Commands.Exec_In_Separate_Terminal, + Commands.Exec_In_Terminal, + Commands.Exec_In_Terminal_Icon, + Commands.Exec_Selection_In_Django_Shell, + Commands.Exec_Selection_In_Terminal, + ].sort(), + ); }); test('Ensure executeFileInterTerminal will do nothing if no file is avialble', async () => { @@ -135,7 +138,10 @@ suite('Terminal - Code Execution Manager', () => { const fileToExecute = Uri.file('x'); await commandHandler!(fileToExecute); helper.verify(async (h) => h.getFileToExecute(), TypeMoq.Times.never()); - executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + executionService.verify( + async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); test('Ensure executeFileInterTerminal will use active file', async () => { @@ -164,7 +170,10 @@ suite('Terminal - Code Execution Manager', () => { .returns(() => executionService.object); await commandHandler!(fileToExecute); - executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + executionService.verify( + async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); async function testExecutionOfSelectionWithoutAnyActiveDocument(commandId: string, executionSericeId: string) { From 0c4fa40d28bcdd2848c36499be4be61a1a90a4e5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 11 May 2023 10:21:04 -0700 Subject: [PATCH 0003/1136] Change name of command to run Python files in separate terminals (#21229) Closes https://github.com/microsoft/vscode-python/issues/14094 --- package.json | 12 ++++++------ package.nls.json | 2 +- src/client/common/constants.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index df393d7934d2..2109483b817f 100644 --- a/package.json +++ b/package.json @@ -306,9 +306,9 @@ }, { "category": "Python", - "command": "python.execInNewTerminal", + "command": "python.execInDedicatedTerminal", "icon": "$(play)", - "title": "%python.command.python.execInNewTerminal.title%" + "title": "%python.command.python.execInDedicatedTerminal.title%" }, { "category": "Python", @@ -1634,9 +1634,9 @@ }, { "category": "Python", - "command": "python.execInNewTerminal", + "command": "python.execInDedicatedTerminal", "icon": "$(play)", - "title": "%python.command.python.execInNewTerminal.title%", + "title": "%python.command.python.execInDedicatedTerminal.title%", "when": "false && editorLangId == python" }, { @@ -1798,9 +1798,9 @@ "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" }, { - "command": "python.execInNewTerminal", + "command": "python.execInDedicatedTerminal", "group": "navigation@0", - "title": "%python.command.python.execInNewTerminal.title%", + "title": "%python.command.python.execInDedicatedTerminal.title%", "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" }, { diff --git a/package.nls.json b/package.nls.json index e8e0bb803d10..18254fd05468 100644 --- a/package.nls.json +++ b/package.nls.json @@ -7,7 +7,7 @@ "python.command.python.execInTerminal.title": "Run Python File in Terminal", "python.command.python.debugInTerminal.title": "Debug Python File", "python.command.python.execInTerminalIcon.title": "Run Python File", - "python.command.python.execInNewTerminal.title": "Run Python File in Separate Terminal", + "python.command.python.execInDedicatedTerminal.title": "Run Python File in Dedicated Terminal", "python.command.python.setInterpreter.title": "Select Interpreter", "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", "python.command.python.viewOutput.title": "Show Output", diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index aae792ec2e39..bea0ef9e235c 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -44,7 +44,7 @@ export namespace Commands { export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; export const Exec_In_Terminal = 'python.execInTerminal'; export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; - export const Exec_In_Separate_Terminal = 'python.execInNewTerminal'; + export const Exec_In_Separate_Terminal = 'python.execInDedicatedTerminal'; export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; export const GetSelectedInterpreterPath = 'python.interpreterPath'; From b0da28cd3d595728944034773a8dc68ea425e3c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Pi=C3=B1a=20Martinez?= Date: Thu, 11 May 2023 22:06:08 +0200 Subject: [PATCH 0004/1136] Remove IS_WINDOWS constant in favor of PlatformService (#21157) Solves partially #8542 --- src/client/common/configSettings.ts | 4 ++-- src/client/common/platform/constants.ts | 7 ------- src/client/common/platform/platformService.ts | 7 ++++++- src/client/common/serviceRegistry.ts | 4 ++-- src/client/linters/pydocstyle.ts | 4 ++-- src/test/common/configSettings.test.ts | 4 ++-- src/test/serviceRegistry.ts | 5 ++--- 7 files changed, 16 insertions(+), 19 deletions(-) delete mode 100644 src/client/common/platform/constants.ts diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 3e3525d5b2a4..db5944cc794b 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -23,7 +23,6 @@ import { ITestingSettings } from '../testing/configuration/types'; import { IWorkspaceService } from './application/types'; import { WorkspaceService } from './application/workspace'; import { DEFAULT_INTERPRETER_SETTING, isTestExecution } from './constants'; -import { IS_WINDOWS } from './platform/constants'; import { IAutoCompleteSettings, IDefaultLanguageServer, @@ -41,6 +40,7 @@ import { import { debounceSync } from './utils/decorators'; import { SystemVariables } from './variables/systemVariables'; import { getOSType, OSType } from './utils/platform'; +import { isWindows } from './platform/platformService'; const untildify = require('untildify'); @@ -654,7 +654,7 @@ function getPythonExecutable(pythonPath: string): string { for (let executableName of KnownPythonExecutables) { // Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows'. - if (IS_WINDOWS) { + if (isWindows()) { executableName = `${executableName}.exe`; if (isValidPythonPath(path.join(pythonPath, executableName))) { return path.join(pythonPath, executableName); diff --git a/src/client/common/platform/constants.ts b/src/client/common/platform/constants.ts deleted file mode 100644 index 808a63188c1d..000000000000 --- a/src/client/common/platform/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// TODO : Drop all these in favor of IPlatformService. -// See https://github.com/microsoft/vscode-python/issues/8542. - -export const IS_WINDOWS = /^win/.test(process.platform); diff --git a/src/client/common/platform/platformService.ts b/src/client/common/platform/platformService.ts index 0277c1bcd2a2..aa139eeeebc0 100644 --- a/src/client/common/platform/platformService.ts +++ b/src/client/common/platform/platformService.ts @@ -50,8 +50,9 @@ export class PlatformService implements IPlatformService { } } + // eslint-disable-next-line class-methods-use-this public get isWindows(): boolean { - return this.osType === OSType.Windows; + return isWindows(); } public get isMac(): boolean { @@ -72,3 +73,7 @@ export class PlatformService implements IPlatformService { return getArchitecture() === Architecture.x64; } } + +export function isWindows(): boolean { + return getOSType() === OSType.Windows; +} diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 5b527499460a..be0559496ace 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -57,7 +57,6 @@ import { ProductInstaller } from './installer/productInstaller'; import { InterpreterPathService } from './interpreterPathService'; import { BrowserService } from './net/browser'; import { PersistentStateFactory } from './persistentState'; -import { IS_WINDOWS } from './platform/constants'; import { PathUtils } from './platform/pathUtils'; import { CurrentProcess } from './process/currentProcess'; import { ProcessLogger } from './process/logger'; @@ -91,9 +90,10 @@ import { Random } from './utils/random'; import { ContextKeyManager } from './application/contextKeyManager'; import { CreatePythonFileCommandHandler } from './application/commands/createPythonFile'; import { RequireJupyterPrompt } from '../jupyter/requireJupyterPrompt'; +import { isWindows } from './platform/platformService'; export function registerTypes(serviceManager: IServiceManager): void { - serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); + serviceManager.addSingletonInstance(IsWindows, isWindows()); serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); diff --git a/src/client/linters/pydocstyle.ts b/src/client/linters/pydocstyle.ts index 93c059440fe7..4851190a92ac 100644 --- a/src/client/linters/pydocstyle.ts +++ b/src/client/linters/pydocstyle.ts @@ -4,9 +4,9 @@ import '../common/extensions'; import { Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { traceError } from '../logging'; -import { IS_WINDOWS } from '../common/platform/constants'; import { BaseLinter } from './baseLinter'; import { ILintMessage, LintMessageSeverity } from './types'; +import { isWindows } from '../common/platform/platformService'; export class PyDocStyle extends BaseLinter { constructor(serviceContainer: IServiceContainer) { @@ -47,7 +47,7 @@ export class PyDocStyle extends BaseLinter { .filter((value, index) => index < maxLines && value.indexOf(':') >= 0) .map((line) => { // Windows will have a : after the drive letter (e.g. c:\). - if (IS_WINDOWS) { + if (isWindows()) { return line.substring(line.indexOf(`${baseFileName}:`) + baseFileName.length + 1).trim(); } return line.substring(line.indexOf(':') + 1).trim(); diff --git a/src/test/common/configSettings.test.ts b/src/test/common/configSettings.test.ts index 75c20f512bbe..8630835081e2 100644 --- a/src/test/common/configSettings.test.ts +++ b/src/test/common/configSettings.test.ts @@ -1,10 +1,10 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; -import { IS_WINDOWS } from '../../client/common/platform/constants'; import { SystemVariables } from '../../client/common/variables/systemVariables'; import { getExtensionSettings } from '../extensionSettings'; import { initialize } from './../initialize'; +import { isWindows } from '../../client/common/platform/platformService'; const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); @@ -27,7 +27,7 @@ suite('Configuration Settings', () => { } const pythonSettingValue = (pythonSettings as any)[key] as string; - if (key.endsWith('Path') && IS_WINDOWS) { + if (key.endsWith('Path') && isWindows()) { assert.strictEqual( settingValue.toUpperCase(), pythonSettingValue.toUpperCase(), diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index 1b8a9d78d580..c20a84b1e25a 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -5,10 +5,9 @@ import { Container } from 'inversify'; import { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Disposable, Memento } from 'vscode'; -import { IS_WINDOWS } from '../client/common/platform/constants'; import { FileSystem } from '../client/common/platform/fileSystem'; import { PathUtils } from '../client/common/platform/pathUtils'; -import { PlatformService } from '../client/common/platform/platformService'; +import { PlatformService, isWindows } from '../client/common/platform/platformService'; import { RegistryImplementation } from '../client/common/platform/registry'; import { registerTypes as platformRegisterTypes } from '../client/common/platform/serviceRegistry'; import { IFileSystem, IPlatformService, IRegistry } from '../client/common/platform/types'; @@ -195,7 +194,7 @@ export class IocContainer { } public registerMockProcess(): void { - this.serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); + this.serviceManager.addSingletonInstance(IsWindows, isWindows()); this.serviceManager.addSingleton(IPathUtils, PathUtils); this.serviceManager.addSingleton(ICurrentProcess, MockProcess); From b3d43e5f7ee97d30c578593d52720b805d10e0f7 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 11 May 2023 15:48:43 -0700 Subject: [PATCH 0005/1136] Do not open "save as" window when running existing Python files (#21232) Closes https://github.com/microsoft/vscode-python/issues/21209 --- src/client/common/application/types.ts | 12 ++++++------ src/client/common/application/workspace.ts | 4 ++-- src/client/terminals/codeExecution/helper.ts | 4 ++-- src/test/terminals/codeExecution/helper.test.ts | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index e57bac656d19..72cd357d62d8 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -852,15 +852,15 @@ export interface IWorkspaceService { */ openTextDocument(options?: { language?: string; content?: string }): Thenable; /** - * Saves the editor identified by the given resource to a new file name as provided by the user and - * returns the resulting resource or `undefined` if save was not successful or cancelled. + * Saves the editor identified by the given resource and returns the resulting resource or `undefined` + * if save was not successful. * - * **Note** that an editor with the provided resource must be opened in order to be saved as. + * **Note** that an editor with the provided resource must be opened in order to be saved. * - * @param uri the associated uri for the opened editor to save as. - * @return A thenable that resolves when the save-as operation has finished. + * @param uri the associated uri for the opened editor to save. + * @return A thenable that resolves when the save operation has finished. */ - saveAs(uri: Uri): Thenable; + save(uri: Uri): Thenable; } export const ITerminalManager = Symbol('ITerminalManager'); diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index 11ed98cf0076..a76a78777bef 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -113,10 +113,10 @@ export class WorkspaceService implements IWorkspaceService { return `{${enabledSearchExcludes.join(',')}}`; } - public async saveAs(uri: Uri): Promise { + public async save(uri: Uri): Promise { try { // This is a proposed API hence putting it inside try...catch. - const result = await workspace.saveAs(uri); + const result = await workspace.save(uri); return result; } catch (ex) { return undefined; diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index 0a1ae9f0be06..0d5694b4a28d 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -122,9 +122,9 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { public async saveFileIfDirty(file: Uri): Promise { const docs = this.documentManager.textDocuments.filter((d) => d.uri.path === file.path); - if (docs.length === 1 && docs[0].isDirty) { + if (docs.length === 1 && (docs[0].isDirty || docs[0].isUntitled)) { const workspaceService = this.serviceContainer.get(IWorkspaceService); - return workspaceService.saveAs(docs[0].uri); + return workspaceService.save(docs[0].uri); } return undefined; } diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts index 684ca22b6c75..ac47037a2344 100644 --- a/src/test/terminals/codeExecution/helper.test.ts +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -383,7 +383,7 @@ suite('Terminal - Code Execution Helper', () => { const untitledUri = Uri.file('Untitled-1'); document.setup((doc) => doc.uri).returns(() => untitledUri); const expectedSavedUri = Uri.file('one.py'); - workspaceService.setup((w) => w.saveAs(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedSavedUri)); + workspaceService.setup((w) => w.save(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedSavedUri)); const savedUri = await helper.saveFileIfDirty(untitledUri); From fcfc54c43e01942d8d7bd89214a5e94c0c34d469 Mon Sep 17 00:00:00 2001 From: Jonathan Rayner Date: Fri, 12 May 2023 23:42:24 +0100 Subject: [PATCH 0006/1136] Add option for pyenv interpreters when creating environments with venv (#21219) Resolves #20881 . Testing: Behaves as expected when testing with Extension Development Host: ![image](https://github.com/microsoft/vscode-python/assets/30149293/d114d9ab-f2d8-4273-877b-d7dd030cfe76) --- .../creation/provider/venvCreationProvider.ts | 3 ++- src/client/pythonEnvironments/info/index.ts | 4 +++- src/client/pythonEnvironments/legacyIOC.ts | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index e18f2fa79fde..b00682c3cb5d 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -162,7 +162,8 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { EnvironmentType.System, EnvironmentType.MicrosoftStore, EnvironmentType.Global, - ].includes(i.envType), + EnvironmentType.Pyenv, + ].includes(i.envType) && i.type === undefined, // only global intepreters { skipRecommended: true, showBackButton: true, diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index 70abbb0fad76..60628f61314e 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -68,10 +68,11 @@ export type InterpreterInformation = { * * @prop companyDisplayName - the user-facing name of the distro publisher * @prop displayName - the user-facing name for the environment - * @prop type - the kind of Python environment + * @prop envType - the kind of Python environment * @prop envName - the environment's name, if applicable (else `envPath` is set) * @prop envPath - the environment's root dir, if applicable (else `envName`) * @prop cachedEntry - whether or not the info came from a cache + * @prop type - the type of Python environment, if applicable */ // Note that "cachedEntry" is specific to the caching machinery // and doesn't really belong here. @@ -84,6 +85,7 @@ export type PythonEnvironment = InterpreterInformation & { envName?: string; envPath?: string; cachedEntry?: boolean; + type?: string; }; /** diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index 0c80c3414728..f116bdb63a89 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -75,6 +75,7 @@ function convertEnvInfo(info: PythonEnvInfo): PythonEnvironment { } env.displayName = info.display; env.detailedDisplayName = info.detailedDisplayName; + env.type = info.type; // We do not worry about using distro.defaultDisplayName. return env; From b4a47bbc2d27ae476db14f4f406f166c49214930 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Mon, 15 May 2023 14:53:09 -0700 Subject: [PATCH 0007/1136] Add reload flag on fastApi provider (#21241) --- .../configuration/dynamicdebugConfigurationService.ts | 2 +- .../extension/configuration/providers/fastapiLaunch.ts | 6 +++--- .../configuration/providers/fastapiLaunch.unit.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts b/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts index 2d80f0e3d6e8..e79f201d9367 100644 --- a/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts +++ b/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts @@ -69,7 +69,7 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf type: DebuggerTypeName, request: 'launch', module: 'uvicorn', - args: [`${fastApiPath}:app`], + args: [`${fastApiPath}:app`, '--reload'], jinja: true, justMyCode: true, }); diff --git a/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts b/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts index 25aaf3d25c08..38a9b7ccf1a2 100644 --- a/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts +++ b/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts @@ -25,12 +25,12 @@ export async function buildFastAPILaunchDebugConfiguration( type: DebuggerTypeName, request: 'launch', module: 'uvicorn', - args: ['main:app'], + args: ['main:app', '--reload'], jinja: true, justMyCode: true, }; - if (!application) { + if (!application && config.args) { const selectedPath = await input.showInputBox({ title: DebugConfigStrings.fastapi.enterAppPathOrNamePath.title, value: 'main.py', @@ -44,7 +44,7 @@ export async function buildFastAPILaunchDebugConfiguration( }); if (selectedPath) { manuallyEnteredAValue = true; - config.args = [`${path.basename(selectedPath, '.py').replace('/', '.')}:app`]; + config.args[0] = `${path.basename(selectedPath, '.py').replace('/', '.')}:app`; } } diff --git a/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts index f6c20985e4da..80ce37167024 100644 --- a/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts +++ b/src/test/debugger/extension/configuration/providers/fastapiLaunch.unit.test.ts @@ -53,7 +53,7 @@ suite('Debugging - Configuration Provider FastAPI', () => { type: DebuggerTypeName, request: 'launch', module: 'uvicorn', - args: ['main:app'], + args: ['main:app', '--reload'], jinja: true, justMyCode: true, }; @@ -73,7 +73,7 @@ suite('Debugging - Configuration Provider FastAPI', () => { type: DebuggerTypeName, request: 'launch', module: 'uvicorn', - args: ['main:app'], + args: ['main:app', '--reload'], jinja: true, justMyCode: true, }; From be9662f0316856b8e119a3441d5502d75cf5cc11 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 16 May 2023 08:38:46 -0700 Subject: [PATCH 0008/1136] revert testing to using socket (#21242) switch back to using a socket instead of an output file for use in the plugin communication during testing. This should work now that we resolved the issue with python path for windows. --- pythonFiles/tests/pytestadapter/helpers.py | 83 ++++++++++++---------- pythonFiles/vscode_pytest/__init__.py | 40 ++++------- 2 files changed, 59 insertions(+), 64 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index b078439f6eac..e84c46037c2f 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -10,6 +10,7 @@ import socket import subprocess import sys +import threading import uuid from typing import Any, Dict, List, Optional, Union @@ -17,20 +18,6 @@ from typing_extensions import TypedDict -@contextlib.contextmanager -def test_output_file(root: pathlib.Path, ext: str = ".txt"): - """Creates a temporary python file with a random name.""" - basename = ( - "".join(random.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(9)) + ext - ) - fullpath = root / basename - try: - fullpath.write_text("", encoding="utf-8") - yield fullpath - finally: - os.unlink(str(fullpath)) - - def create_server( host: str = "127.0.0.1", port: int = 0, @@ -116,31 +103,51 @@ def runner(args: List[str]) -> Optional[Dict[str, Any]]: "-p", "vscode_pytest", ] + args + listener: socket.socket = create_server() + _, port = listener.getsockname() + listener.listen() + + env = os.environ.copy() + env.update( + { + "TEST_UUID": str(uuid.uuid4()), + "TEST_PORT": str(port), + "PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent), + } + ) + + result: list = [] + t1: threading.Thread = threading.Thread( + target=_listen_on_socket, args=(listener, result) + ) + t1.start() + + t2 = threading.Thread( + target=lambda proc_args, proc_env, proc_cwd: subprocess.run( + proc_args, env=proc_env, cwd=proc_cwd + ), + args=(process_args, env, TEST_DATA_PATH), + ) + t2.start() + + t1.join() + t2.join() + + return process_rpc_json(result[0]) if result else None + - with test_output_file(TEST_DATA_PATH) as output_path: - env = os.environ.copy() - env.update( - { - "TEST_UUID": str(uuid.uuid4()), - "TEST_PORT": str(12345), # port is not used for tests - "PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent), - "TEST_OUTPUT_FILE": os.fspath(output_path), - } - ) - - result = subprocess.run( - process_args, - env=env, - cwd=os.fspath(TEST_DATA_PATH), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - if result.returncode != 0: - print("Subprocess Run failed with:") - print(result.stdout.decode(encoding="utf-8")) - print(result.stderr.decode(encoding="utf-8")) - - return process_rpc_json(output_path.read_text(encoding="utf-8")) +def _listen_on_socket(listener: socket.socket, result: List[str]): + """Listen on the socket for the JSON data from the server. + Created as a seperate function for clarity in threading. + """ + sock, (other_host, other_port) = listener.accept() + all_data: list = [] + while True: + data: bytes = sock.recv(1024 * 1024) + if not data: + break + all_data.append(data.decode("utf-8")) + result.append("".join(all_data)) def find_test_line_number(test_name: str, test_file_path) -> str: diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 6063e4113d55..39330f4e8a38 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -437,19 +437,13 @@ def execution_post( Request-uuid: {testuuid} {data}""" - test_output_file: Optional[str] = os.getenv("TEST_OUTPUT_FILE", None) - if test_output_file == "stdout": - print(request) - elif test_output_file: - pathlib.Path(test_output_file).write_text(request, encoding="utf-8") - else: - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Plugin error connection error[vscode-pytest]: {e}") - print(f"[vscode-pytest] data: {request}") + try: + with socket_manager.SocketManager(addr) as s: + if s.socket is not None: + s.socket.sendall(request.encode("utf-8")) + except Exception as e: + print(f"Plugin error connection error[vscode-pytest]: {e}") + print(f"[vscode-pytest] data: {request}") def post_response(cwd: str, session_node: TestNode) -> None: @@ -477,16 +471,10 @@ def post_response(cwd: str, session_node: TestNode) -> None: Request-uuid: {testuuid} {data}""" - test_output_file: Optional[str] = os.getenv("TEST_OUTPUT_FILE", None) - if test_output_file == "stdout": - print(request) - elif test_output_file: - pathlib.Path(test_output_file).write_text(request, encoding="utf-8") - else: - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Plugin error connection error[vscode-pytest]: {e}") - print(f"[vscode-pytest] data: {request}") + try: + with socket_manager.SocketManager(addr) as s: + if s.socket is not None: + s.socket.sendall(request.encode("utf-8")) + except Exception as e: + print(f"Plugin error connection error[vscode-pytest]: {e}") + print(f"[vscode-pytest] data: {request}") From b0ebc9ba50c3f89a8be713acc4fb49190f390d2f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 16 May 2023 09:16:48 -0700 Subject: [PATCH 0009/1136] Enable debug pytest (#21228) fixes https://github.com/microsoft/vscode-python/issues/21147 --------- Co-authored-by: Aidos Kanapyanov <65722512+aidoskanapyanov@users.noreply.github.com> Co-authored-by: Karthik Nadig --- .vscode/launch.json | 3 +- pythonFiles/vscode_pytest/__init__.py | 7 ++- src/client/testing/common/debugLauncher.ts | 48 ++++++++++++++----- src/client/testing/common/types.ts | 2 + .../testing/testController/common/types.ts | 3 +- .../testing/testController/controller.ts | 2 + .../pytest/pytestExecutionAdapter.ts | 37 ++++++++++---- .../testController/workspaceTestAdapter.ts | 3 ++ 8 files changed, 80 insertions(+), 25 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 82981a93305d..1ca0db3dc858 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,7 +22,8 @@ // Enable this to log telemetry to the output during debugging "XVSC_PYTHON_LOG_TELEMETRY": "1", // Enable this to log debugger output. Directory must exist ahead of time - "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output_Ex" + "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output_Ex", + "ENABLE_PYTHON_TESTING_REWRITE": "1" } }, { diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 39330f4e8a38..0f0bcbd1d323 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -179,6 +179,12 @@ def pytest_sessionfinish(session, exitstatus): 4: Pytest encountered an internal error or exception during test execution. 5: Pytest was unable to find any tests to run. """ + print( + "pytest session has finished, exit status: ", + exitstatus, + "in discovery? ", + IS_DISCOVERY, + ) cwd = pathlib.Path.cwd() if IS_DISCOVERY: try: @@ -209,7 +215,6 @@ def pytest_sessionfinish(session, exitstatus): f"Pytest exited with error status: {exitstatus}, {ERROR_MESSAGE_CONST[exitstatus]}" ) exitstatus_bool = "error" - execution_post( os.fsdecode(cwd), exitstatus_bool, diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 36432c0bd831..5b39bd97a740 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -15,6 +15,7 @@ import { ITestDebugLauncher, LaunchOptions } from './types'; import { getConfigurationsForWorkspace } from '../../debugger/extension/configuration/launch.json/launchJsonReader'; import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis/workspaceApis'; import { showErrorMessage } from '../../common/vscodeApis/windowApis'; +import { createDeferred } from '../../common/utils/async'; @injectable() export class DebugLauncher implements ITestDebugLauncher { @@ -42,16 +43,12 @@ export class DebugLauncher implements ITestDebugLauncher { ); const debugManager = this.serviceContainer.get(IDebugService); - return debugManager.startDebugging(workspaceFolder, launchArgs).then( - // Wait for debug session to be complete. - () => - new Promise((resolve) => { - debugManager.onDidTerminateDebugSession(() => { - resolve(); - }); - }), - (ex) => traceError('Failed to start debugging tests', ex), - ); + const deferred = createDeferred(); + debugManager.onDidTerminateDebugSession(() => { + deferred.resolve(); + }); + debugManager.startDebugging(workspaceFolder, launchArgs); + return deferred.promise; } private static resolveWorkspaceFolder(cwd: string): WorkspaceFolder { @@ -181,6 +178,12 @@ export class DebugLauncher implements ITestDebugLauncher { const args = script(testArgs); const [program] = args; configArgs.program = program; + // if the test provider is pytest, then use the pytest module instead of using a program + const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; + if (options.testProvider === 'pytest' && rewriteTestingEnabled) { + configArgs.module = 'pytest'; + configArgs.program = undefined; + } configArgs.args = args.slice(1); // We leave configArgs.request as "test" so it will be sent in telemetry. @@ -201,6 +204,21 @@ export class DebugLauncher implements ITestDebugLauncher { throw Error(`Invalid debug config "${debugConfig.name}"`); } launchArgs.request = 'launch'; + if (options.testProvider === 'pytest' && rewriteTestingEnabled) { + if (options.pytestPort && options.pytestUUID) { + launchArgs.env = { + ...launchArgs.env, + TEST_PORT: options.pytestPort, + TEST_UUID: options.pytestUUID, + }; + } else { + throw Error( + `Missing value for debug setup, both port and uuid need to be defined. port: "${options.pytestPort}" uuid: "${options.pytestUUID}"`, + ); + } + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); + launchArgs.env.PYTHONPATH = pluginPath; + } // Clear out purpose so we can detect if the configuration was used to // run via F5 style debugging. @@ -210,13 +228,19 @@ export class DebugLauncher implements ITestDebugLauncher { } private static getTestLauncherScript(testProvider: TestProvider) { + const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; switch (testProvider) { case 'unittest': { + if (rewriteTestingEnabled) { + return internalScripts.execution_py_testlauncher; // this is the new way to run unittest execution, debugger + } return internalScripts.visualstudio_py_testlauncher; // old way unittest execution, debugger - // return internalScripts.execution_py_testlauncher; // this is the new way to run unittest execution, debugger } case 'pytest': { - return internalScripts.testlauncher; + if (rewriteTestingEnabled) { + return (testArgs: string[]) => testArgs; + } + return internalScripts.testlauncher; // old way pytest execution, debugger } default: { throw new Error(`Unknown test provider '${testProvider}'`); diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts index b1476e74435f..a5d263058525 100644 --- a/src/client/testing/common/types.ts +++ b/src/client/testing/common/types.ts @@ -25,6 +25,8 @@ export type LaunchOptions = { testProvider: TestProvider; token?: CancellationToken; outChannel?: OutputChannel; + pytestPort?: string; + pytestUUID?: string; }; export type ParserOptions = TestDiscoveryOptions; diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 52c6c787040c..4336fee3a4b6 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -12,7 +12,7 @@ import { Uri, WorkspaceFolder, } from 'vscode'; -import { TestDiscoveryOptions } from '../../common/types'; +import { ITestDebugLauncher, TestDiscoveryOptions } from '../../common/types'; import { IPythonExecutionFactory } from '../../../common/process/types'; export type TestRunInstanceOptions = TestRunOptions & { @@ -193,6 +193,7 @@ export interface ITestExecutionAdapter { testIds: string[], debugBool?: boolean, executionFactory?: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, ): Promise; } diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index fb176a30af88..58eaa9c890d6 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -409,6 +409,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc token, request.profile?.kind === TestRunProfileKind.Debug, this.pythonExecFactory, + this.debugLauncher, ); } return this.pytest.runTests( @@ -438,6 +439,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc testItems, token, request.profile?.kind === TestRunProfileKind.Debug, + this.pythonExecFactory, ); } // below is old way of running unittest execution diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index d2cbd3151e6f..623fd1ff3a8c 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -12,11 +12,13 @@ import { IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { EXTENSION_ROOT_DIR } from '../../../constants'; import { removePositionalFoldersAndFiles } from './arguments'; +import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; +import { PYTEST_PROVIDER } from '../../common/constants'; +import { EXTENSION_ROOT_DIR } from '../../../common/constants'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -(global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; +// (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; /** * Wrapper Class for pytest test execution. This is where we call `runTestCommand`? */ @@ -47,11 +49,12 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testIds: string[], debugBool?: boolean, executionFactory?: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, ): Promise { traceVerbose(uri, testIds, debugBool); if (executionFactory !== undefined) { // ** new version of run tests. - return this.runTestsNew(uri, testIds, debugBool, executionFactory); + return this.runTestsNew(uri, testIds, debugBool, executionFactory, debugLauncher); } // if executionFactory is undefined, we are using the old method signature of run tests. this.outputChannel.appendLine('Running tests.'); @@ -64,6 +67,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testIds: string[], debugBool?: boolean, executionFactory?: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, ): Promise { const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; @@ -106,16 +110,29 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testArgs.splice(0, 0, '--rootdir', uri.fsPath); } + // why is this needed? if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { testArgs.push('--capture', 'no'); } - - console.debug(`Running test with arguments: ${testArgs.join(' ')}\r\n`); - console.debug(`Current working directory: ${uri.fsPath}\r\n`); - - const argArray = ['-m', 'pytest', '-p', 'vscode_pytest'].concat(testArgs).concat(testIds); - console.debug('argArray', argArray); - execService?.exec(argArray, spawnOptions); + const pluginArgs = ['-p', 'vscode_pytest', '-v'].concat(testArgs).concat(testIds); + if (debugBool) { + const pytestPort = this.testServer.getPort().toString(); + const pytestUUID = uuid.toString(); + const launchOptions: LaunchOptions = { + cwd: uri.fsPath, + args: pluginArgs, + token: spawnOptions.token, + testProvider: PYTEST_PROVIDER, + pytestPort, + pytestUUID, + }; + console.debug(`Running debug test with arguments: ${pluginArgs.join(' ')}\r\n`); + await debugLauncher!.launchDebugger(launchOptions); + } else { + const runArgs = ['-m', 'pytest'].concat(pluginArgs); + console.debug(`Running test with arguments: ${runArgs.join(' ')}\r\n`); + execService?.exec(runArgs, spawnOptions); + } } catch (ex) { console.debug(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); return Promise.reject(ex); diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index 39efc67f7c7e..b22fee69d295 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -38,6 +38,7 @@ import { } from './common/types'; import { fixLogLines } from './common/utils'; import { IPythonExecutionFactory } from '../../common/process/types'; +import { ITestDebugLauncher } from '../common/types'; /** * This class exposes a test-provider-agnostic way of discovering tests. @@ -77,6 +78,7 @@ export class WorkspaceTestAdapter { token?: CancellationToken, debugBool?: boolean, executionFactory?: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, ): Promise { if (this.executing) { return this.executing.promise; @@ -110,6 +112,7 @@ export class WorkspaceTestAdapter { testCaseIds, debugBool, executionFactory, + debugLauncher, ); traceVerbose('executionFactory defined'); } else { From a74f1d15b58e2a835f5564449918452ff30d62d6 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 16 May 2023 11:18:18 -0700 Subject: [PATCH 0010/1136] Detect installed packages in the selected environment (#21231) Fixes https://github.com/microsoft/vscode-python/issues/21140 --- package.json | 10 +- pythonFiles/installed_check.py | 129 +++++++ pythonFiles/tests/test_data/missing-deps.data | 121 +++++++ .../tests/test_data/no-missing-deps.data | 13 + .../test_data/pyproject-missing-deps.data | 9 + .../test_data/pyproject-no-missing-deps.data | 9 + pythonFiles/tests/test_installed_check.py | 90 +++++ requirements.in | 5 + requirements.txt | 20 +- .../common/process/internal/scripts/index.ts | 5 + src/client/common/vscodeApis/languageApis.ts | 12 + src/client/common/vscodeApis/windowApis.ts | 4 + src/client/common/vscodeApis/workspaceApis.ts | 12 +- src/client/extensionActivation.ts | 6 +- .../creation/common/installCheckUtils.ts | 60 ++++ .../creation/createEnvButtonContext.ts | 40 +-- .../creation/installedPackagesDiagnostic.ts | 88 +++++ .../creation/pyProjectTomlContext.ts | 44 +++ .../creation/registrations.ts | 21 ++ .../common/installCheckUtils.unit.test.ts | 65 ++++ .../createEnvButtonContext.unit.test.ts | 255 +------------- .../installedPackagesDiagnostics.unit.test.ts | 333 ++++++++++++++++++ .../pyProjectTomlContext.unit.test.ts | 266 ++++++++++++++ 23 files changed, 1314 insertions(+), 303 deletions(-) create mode 100644 pythonFiles/installed_check.py create mode 100644 pythonFiles/tests/test_data/missing-deps.data create mode 100644 pythonFiles/tests/test_data/no-missing-deps.data create mode 100644 pythonFiles/tests/test_data/pyproject-missing-deps.data create mode 100644 pythonFiles/tests/test_data/pyproject-no-missing-deps.data create mode 100644 pythonFiles/tests/test_installed_check.py create mode 100644 src/client/common/vscodeApis/languageApis.ts create mode 100644 src/client/pythonEnvironments/creation/common/installCheckUtils.ts create mode 100644 src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts create mode 100644 src/client/pythonEnvironments/creation/pyProjectTomlContext.ts create mode 100644 src/client/pythonEnvironments/creation/registrations.ts create mode 100644 src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts create mode 100644 src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts create mode 100644 src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts diff --git a/package.json b/package.json index 2109483b817f..8b364b5d60e6 100644 --- a/package.json +++ b/package.json @@ -1526,10 +1526,8 @@ ], "configuration": "./languages/pip-requirements.json", "filenamePatterns": [ - "**/*-requirements.{txt, in}", - "**/*-constraints.txt", - "**/requirements-*.{txt, in}", - "**/constraints-*.txt", + "**/*requirements*.{txt, in}", + "**/*constraints*.txt", "**/requirements/*.{txt,in}", "**/constraints/*.txt" ], @@ -1733,12 +1731,12 @@ { "group": "Python", "command": "python.createEnvironment-button", - "when": "showCreateEnvButton && resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" + "when": "showCreateEnvButton && resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && pythonDepsNotInstalled" }, { "group": "Python", "command": "python.createEnvironment-button", - "when": "showCreateEnvButton && resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor" + "when": "showCreateEnvButton && resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && pythonDepsNotInstalled" } ], "editor/context": [ diff --git a/pythonFiles/installed_check.py b/pythonFiles/installed_check.py new file mode 100644 index 000000000000..f0e1c268d270 --- /dev/null +++ b/pythonFiles/installed_check.py @@ -0,0 +1,129 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import json +import os +import pathlib +import sys +from typing import Dict, List, Optional, Sequence, Tuple, Union + +LIB_ROOT = pathlib.Path(__file__).parent / "lib" / "python" +sys.path.insert(0, os.fspath(LIB_ROOT)) + +import tomli +from importlib_metadata import metadata +from packaging.requirements import Requirement + +DEFAULT_SEVERITY = 3 + + +def parse_args(argv: Optional[Sequence[str]] = None): + if argv is None: + argv = sys.argv[1:] + parser = argparse.ArgumentParser( + description="Check for installed packages against requirements" + ) + parser.add_argument("FILEPATH", type=str, help="Path to requirements.[txt, in]") + + return parser.parse_args(argv) + + +def parse_requirements(line: str) -> Optional[Requirement]: + try: + req = Requirement(line.strip("\\")) + if req.marker is None: + return req + elif req.marker.evaluate(): + return req + except: + return None + + +def process_requirements(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]: + diagnostics = [] + for n, line in enumerate(req_file.read_text(encoding="utf-8").splitlines()): + if line.startswith(("#", "-", " ")) or line == "": + continue + + req = parse_requirements(line) + if req: + try: + # Check if package is installed + metadata(req.name) + except: + diagnostics.append( + { + "line": n, + "character": 0, + "endLine": n, + "endCharacter": len(req.name), + "package": req.name, + "code": "not-installed", + "severity": DEFAULT_SEVERITY, + } + ) + return diagnostics + + +def get_pos(lines: List[str], text: str) -> Tuple[int, int, int, int]: + for n, line in enumerate(lines): + index = line.find(text) + if index >= 0: + return n, index, n, index + len(text) + return (0, 0, 0, 0) + + +def process_pyproject(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]: + diagnostics = [] + try: + raw_text = req_file.read_text(encoding="utf-8") + pyproject = tomli.loads(raw_text) + except: + return diagnostics + + lines = raw_text.splitlines() + reqs = pyproject.get("project", {}).get("dependencies", []) + for raw_req in reqs: + req = parse_requirements(raw_req) + n, start, _, end = get_pos(lines, raw_req) + if req: + try: + # Check if package is installed + metadata(req.name) + except: + diagnostics.append( + { + "line": n, + "character": start, + "endLine": n, + "endCharacter": end, + "package": req.name, + "code": "not-installed", + "severity": DEFAULT_SEVERITY, + } + ) + return diagnostics + + +def get_diagnostics(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]: + diagnostics = [] + if not req_file.exists(): + return diagnostics + + if req_file.name == "pyproject.toml": + diagnostics = process_pyproject(req_file) + else: + diagnostics = process_requirements(req_file) + + return diagnostics + + +def main(): + args = parse_args() + diagnostics = get_diagnostics(pathlib.Path(args.FILEPATH)) + print(json.dumps(diagnostics, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/pythonFiles/tests/test_data/missing-deps.data b/pythonFiles/tests/test_data/missing-deps.data new file mode 100644 index 000000000000..c42d23c7dd67 --- /dev/null +++ b/pythonFiles/tests/test_data/missing-deps.data @@ -0,0 +1,121 @@ +# +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: +# +# pip-compile --generate-hashes --resolver=backtracking requirements-test.in +# +flake8-csv==0.2.0 \ + --hash=sha256:246e07207fefbf8f80a59ff7e878f153635f562ebaf20cf796a2b00b1528ea9a \ + --hash=sha256:bf3ac6aecbaebe36a2c7d5d275f310996fcc33b7370cdd81feec04b79af2e07c + # via -r requirements-test.in +levenshtein==0.21.0 \ + --hash=sha256:01dd427cf72b4978b09558e3d36e3f92c8eef467e3eb4653c3fdccd8d70aaa08 \ + --hash=sha256:0236c8ff4648c50ebd81ac3692430d2241b134936ac9d86d7ca32ba6ab4a4e63 \ + --hash=sha256:023ca95c833ca548280e444e9a4c34fdecb3be3851e96af95bad290ae0c708b9 \ + --hash=sha256:024302c82d49fc1f1d044794997ef7aa9d01b509a9040e222480b64a01cd4b80 \ + --hash=sha256:04046878a57129da4e2352c032df7c1fceaa54870916d12772cad505ef998290 \ + --hash=sha256:04850a0719e503014acb3fee6d4ec7d7f170a2c7375ffbc5833c7256b7cd10ee \ + --hash=sha256:0cc3679978cd0250bf002963cf2e08855b93f70fa0fc9f74956115c343983fbb \ + --hash=sha256:0f42b8dba2cce257cd34efd1ce9678d06f3248cb0bb2a92a5db8402e1e4a6f30 \ + --hash=sha256:13e8a5b1b58de49befea555bb913dc394614f2d3553bc5b86bc672c69ef1a85a \ + --hash=sha256:1f19fe25ea0dd845d0f48505e8947f6080728e10b7642ba0dad34e9b48c81130 \ + --hash=sha256:1fde464f937878e6f5c30c234b95ce2cb969331a175b3089367e077113428062 \ + --hash=sha256:2290732763e3b75979888364b26acce79d72b8677441b5762a4e97b3630cc3d9 \ + --hash=sha256:24843f28cbbdcbcfc18b08e7d3409dbaad7896fb7113442592fa978590a7bbf0 \ + --hash=sha256:25576ad9c337ecb342306fe87166b54b2f49e713d4ff592c752cc98e0046296e \ + --hash=sha256:26c6fb012538a245d78adea786d2cfe3c1506b835762c1c523a4ed6b9e08dc0b \ + --hash=sha256:31cb59d86a5f99147cd4a67ebced8d6df574b5d763dcb63c033a642e29568746 \ + --hash=sha256:32dfda2e64d0c50553e47d0ab2956413970f940253351c196827ad46f17916d5 \ + --hash=sha256:3305262cb85ff78ace9e2d8d2dfc029b34dc5f93aa2d24fd20b6ed723e2ad501 \ + --hash=sha256:37a99d858fa1d88b1a917b4059a186becd728534e5e889d583086482356b7ca1 \ + --hash=sha256:3c6858cfd84568bc1df3ad545553b5c27af6ed3346973e8f4b57d23c318cf8f4 \ + --hash=sha256:3e1723d515ab287b9b2c2e4a111894dc6b474f5d28826fff379647486cae98d2 \ + --hash=sha256:3e22d31375d5fea5797c9b7aa0f8cc36579c31dcf5754e9931ca86c27d9011f8 \ + --hash=sha256:426883be613d912495cf6ee2a776d2ab84aa6b3de5a8d82c43a994267ea6e0e3 \ + --hash=sha256:4357bf8146cbadb10016ad3a950bba16e042f79015362a575f966181d95b4bc7 \ + --hash=sha256:4515f9511cb91c66d254ee30154206aad76b57d8b25f64ba1402aad43efdb251 \ + --hash=sha256:457442911df185e28a32fd8b788b14ca22ab3a552256b556e7687173d5f18bc4 \ + --hash=sha256:46dab8c6e8fae563ca77acfaeb3824c4dd4b599996328b8a081b06f16befa6a0 \ + --hash=sha256:4b2156f32e46d16b74a055ccb4f64ee3c64399372a6aaf1ee98f6dccfadecee1 \ + --hash=sha256:4bbceef2caba4b2ae613b0e853a7aaab990c1a13bddb9054ba1328a84bccdbf7 \ + --hash=sha256:4c8eaaa6f0df2838437d1d8739629486b145f7a3405d3ef0874301a9f5bc7dcd \ + --hash=sha256:4dc79033140f82acaca40712a6d26ed190cc2dd403e104020a87c24f2771aa72 \ + --hash=sha256:4ec2ef9836a34a3bb009a81e5efe4d9d43515455fb5f182c5d2cf8ae61c79496 \ + --hash=sha256:5369827ace536c6df04e0e670d782999bc17bf9eb111e77435fdcdaecb10c2a3 \ + --hash=sha256:5378a8139ba61d7271c0f9350201259c11eb90bfed0ac45539c4aeaed3907230 \ + --hash=sha256:545635d9e857711d049dcdb0b8609fb707b34b032517376c531ca159fcd46265 \ + --hash=sha256:587ad51770de41eb491bea1bfb676abc7ff9a94dbec0e2bc51fc6a25abef99c4 \ + --hash=sha256:5cfbc4ed7ee2965e305bf81388fea377b795dabc82ee07f04f31d1fb8677a885 \ + --hash=sha256:5e748c2349719cb1bc90f802d9d7f07310633dcf166d468a5bd821f78ed17698 \ + --hash=sha256:608beb1683508c3cdbfff669c1c872ea02b47965e1bbb8a630de548e2490f96a \ + --hash=sha256:6338a47b6f8c7f1ee8b5636cc8b245ad2d1d0ee47f7bb6f33f38a522ef0219cc \ + --hash=sha256:668ea30b311944c643f866ce5e45edf346f05e920075c0056f2ba7f74dde6071 \ + --hash=sha256:66d303cd485710fe6d62108209219b7a695bdd10a722f4e86abdaf26f4bf2202 \ + --hash=sha256:6ebabcf982ae161534f8729d13fe05eebc977b497ac34936551f97cf8b07dd9e \ + --hash=sha256:6ede583155f24c8b2456a7720fbbfa5d9c1154ae04b4da3cf63368e2406ea099 \ + --hash=sha256:709a727f58d31a5ee1e5e83b247972fe55ef0014f6222256c9692c5efa471785 \ + --hash=sha256:742b785c93d16c63289902607219c200bd2b6077dafc788073c74337cae382fb \ + --hash=sha256:76d5d34a8e21de8073c66ae801f053520f946d499fa533fbba654712775f8132 \ + --hash=sha256:7bc550d0986ace95bde003b8a60e622449baf2bdf24d8412f7a50f401a289ec3 \ + --hash=sha256:7c2d67220867d640e36931b3d63b8349369b485d52cf6f4a2635bec8da92d678 \ + --hash=sha256:7ce3f14a8e006fb7e3fc7bab965ab7da5817f48fc48d25cf735fcec8f1d2e39a \ + --hash=sha256:7e40a4bac848c9a8883225f926cfa7b2bc9f651e989a8b7006cdb596edc7ac9b \ + --hash=sha256:80e67bd73a05592ecd52aede4afa8ea49575de70f9d5bfbe2c52ebd3541b20be \ + --hash=sha256:8446f8da38857482ec0cfd616fe5e7dcd3695fd323cc65f37366a9ff6a31c9cb \ + --hash=sha256:8476862a5c3150b8d63a7475563a4bff6dc50bbc0447894eb6b6a116ced0809d \ + --hash=sha256:84b55b732e311629a8308ad2778a0f9824e29e3c35987eb35610fc52eb6d4634 \ + --hash=sha256:88ccdc8dc20c16e8059ace00fb58d353346a04fd24c0733b009678b2554801d2 \ + --hash=sha256:8aa92b05156dfa2e248c3743670d5deb41a45b5789416d5fa31be009f4f043ab \ + --hash=sha256:8ac4ed77d3263eac7f9b6ed89d451644332aecd55cda921201e348803a1e5c57 \ + --hash=sha256:8bdbcd1570340b07549f71e8a5ba3f0a6d84408bf86c4051dc7b70a29ae342bb \ + --hash=sha256:8c031cbe3685b0343f5cc2dcf2172fd21b82f8ccc5c487179a895009bf0e4ea8 \ + --hash=sha256:8c27a5178ce322b56527a451185b4224217aa81955d9b0dad6f5a8de81ffe80f \ + --hash=sha256:8cf87a5e2962431d7260dd81dc1ca0697f61aad81036145d3666f4c0d514ce3a \ + --hash=sha256:8d4ba0df46bb41d660d77e7cc6b4d38c8d5b6f977d51c48ed1217db6a8474cde \ + --hash=sha256:8dd8ef4239b24fb1c9f0b536e48e55194d5966d351d349af23e67c9eb3875c68 \ + --hash=sha256:92bf2370b01d7a4862abf411f8f60f39f064cebebce176e3e9ee14e744db8288 \ + --hash=sha256:9485f2a5c88113410153256657072bc93b81bf5c8690d47e4cc3df58135dbadb \ + --hash=sha256:9ff1255c499fcb41ba37a578ad8c1b8dab5c44f78941b8e1c1d7fab5b5e831bc \ + --hash=sha256:a18c8e4d1aae3f9950797d049020c64a8a63cc8b4e43afcca91ec400bf6304c5 \ + --hash=sha256:a68b05614d25cc2a5fbcc4d2fd124be7668d075fd5ac3d82f292eec573157361 \ + --hash=sha256:a7adaabe07c5ceb6228332b9184f06eb9cda89c227d198a1b8a6f78c05b3c672 \ + --hash=sha256:aa39bb773915e4df330d311bb6c100a8613e265cc50d5b25b015c8db824e1c47 \ + --hash=sha256:ac8b6266799645827980ab1af4e0bfae209c1f747a10bdf6e5da96a6ebe511a2 \ + --hash=sha256:b0ba9723c7d67a61e160b3457259552f7d679d74aaa144b892eb68b7e2a5ebb6 \ + --hash=sha256:b167b32b3e336c5ec5e0212f025587f9248344ae6e73ed668270eba5c6a506e5 \ + --hash=sha256:b646ace5085a60d4f89b28c81301c9d9e8cd6a9bdda908181b2fa3dfac7fc10d \ + --hash=sha256:bd0bfa71b1441be359e99e77709885b79c22857bf9bb7f4e84c09e501f6c5fad \ + --hash=sha256:be038321695267a8faa5ae1b1a83deb3748827f0b6f72471e0beed36afcbd72a \ + --hash=sha256:be87998ffcbb5fb0c37a76d100f63b4811f48527192677da0ec3624b49ab8a64 \ + --hash=sha256:c270487d60b33102efea73be6dcd5835f3ddc3dc06e77499f0963df6cba2ec71 \ + --hash=sha256:c290a7211f1b4f87c300df4424cc46b7379cead3b6f37fa8d3e7e6c6212ccd39 \ + --hash=sha256:cc36ba40027b4f8821155c9e3e0afadffccdccbe955556039d1d1169dfc659c9 \ + --hash=sha256:ce7e76c6341abb498368d42b8081f2f45c245ac2a221af6a0394349d41302c08 \ + --hash=sha256:cefd5a668f6d7af1279aca10104b43882fdd83f9bdc68933ba5429257a628abe \ + --hash=sha256:cf2dee0f8c71598f8be51e3feceb9142ac01576277b9e691e25740987761c86e \ + --hash=sha256:d23c647b03acbb5783f9bdfd51cfa5365d51f7df9f4029717a35eff5cc32bbcc \ + --hash=sha256:d647f1e0c30c7a73f70f4de7376ed7dafc2b856b67fe480d32a81af133edbaeb \ + --hash=sha256:d932cb21e40beb93cfc8973de7f25fbf25ba4a07d1dccac3b9ba977164cf9887 \ + --hash=sha256:db7567997ffbc2feb999e30002a92461a76f17a596a142bdb463b5f7037f160c \ + --hash=sha256:de2dfd6498454c7d89036d56a53c0a01fd9bcf1c2970253e469b5e8bb938b69f \ + --hash=sha256:df9b0f8f511270ad259c7bfba22ab6d5a0c33d81cd594461668e67cd80dd9052 \ + --hash=sha256:e043b79e39f165026bc941c95582bfc4bfdd297a1de6f13ace0d0a7abf486288 \ + --hash=sha256:e2686c37d22faf27d02a19e83b55812d248b32b7ba3aa638e768d0ea032e1f3c \ + --hash=sha256:e9a6251818b9eb6d519bffd7a0b745f3a99b3e99563a4c9d3cad26e34f6ac880 \ + --hash=sha256:eab6c253983a6659e749f4c44fcc2215194c2e00bf7b1c5e90fe683ea3b7b00f \ + --hash=sha256:ec64b7b3fb95bc9c20c72548277794b81281a6ba9da85eda2c87324c218441ff \ + --hash=sha256:ee62ec5882a857b252faffeb7867679f7e418052ca6bf7d6b56099f6498a2b0e \ + --hash=sha256:ee757fd36bad66ad8b961958840894021ecaad22194f65219a666432739393ff \ + --hash=sha256:f55623094b665d79a3b82ba77386ac34fa85049163edfe65387063e5127d4184 \ + --hash=sha256:f622f542bd065ffec7d26b26d44d0c9a25c9c1295fd8ba6e4d77778e2293a12c \ + --hash=sha256:f873af54014cac12082c7f5ccec6bbbeb5b57f63466e7f9c61a34588621313fb \ + --hash=sha256:fae24c875c4ecc8c5f34a9715eb2a459743b4ca21d35c51819b640ee2f71cb51 \ + --hash=sha256:fb26e69fc6c12534fbaa1657efed3b6482f1a166ba8e31227fa6f6f062a59070 + # via -r requirements-test.in +pytest==7.3.1 \ + --hash=sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362 \ + --hash=sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3 + +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f diff --git a/pythonFiles/tests/test_data/no-missing-deps.data b/pythonFiles/tests/test_data/no-missing-deps.data new file mode 100644 index 000000000000..5c2f1178bbdf --- /dev/null +++ b/pythonFiles/tests/test_data/no-missing-deps.data @@ -0,0 +1,13 @@ +# +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: +# +# pip-compile --generate-hashes --resolver=backtracking requirements-test.in +# +pytest==7.3.1 \ + --hash=sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362 \ + --hash=sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3 + +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f diff --git a/pythonFiles/tests/test_data/pyproject-missing-deps.data b/pythonFiles/tests/test_data/pyproject-missing-deps.data new file mode 100644 index 000000000000..f217a0bdade6 --- /dev/null +++ b/pythonFiles/tests/test_data/pyproject-missing-deps.data @@ -0,0 +1,9 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "something" +version = "2023.0.0" +requires-python = ">=3.7" +dependencies = ["pytest==7.3.1", "flake8-csv"] diff --git a/pythonFiles/tests/test_data/pyproject-no-missing-deps.data b/pythonFiles/tests/test_data/pyproject-no-missing-deps.data new file mode 100644 index 000000000000..729bc9169e6f --- /dev/null +++ b/pythonFiles/tests/test_data/pyproject-no-missing-deps.data @@ -0,0 +1,9 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "something" +version = "2023.0.0" +requires-python = ">=3.7" +dependencies = [jedi-language-server"] diff --git a/pythonFiles/tests/test_installed_check.py b/pythonFiles/tests/test_installed_check.py new file mode 100644 index 000000000000..f76070d197be --- /dev/null +++ b/pythonFiles/tests/test_installed_check.py @@ -0,0 +1,90 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import contextlib +import json +import os +import pathlib +import subprocess +import sys + +import pytest +from typing import Dict, List, Union + +SCRIPT_PATH = pathlib.Path(__file__).parent.parent / "installed_check.py" +TEST_DATA = pathlib.Path(__file__).parent / "test_data" +DEFAULT_SEVERITY = 3 + + +@contextlib.contextmanager +def generate_file(base_file: pathlib.Path): + basename = "pyproject.toml" if "pyproject" in base_file.name else "requirements.txt" + fullpath = base_file.parent / basename + if fullpath.exists(): + os.unlink(os.fspath(fullpath)) + fullpath.write_text(base_file.read_text(encoding="utf-8")) + try: + yield fullpath + finally: + os.unlink(str(fullpath)) + + +def run_on_file(file_path: pathlib.Path) -> List[Dict[str, Union[str, int]]]: + result = subprocess.run( + [ + sys.executable, + os.fspath(SCRIPT_PATH), + os.fspath(file_path), + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + assert result.returncode == 0 + assert result.stderr == b"" + return json.loads(result.stdout) + + +EXPECTED_DATA = { + "missing-deps": [ + { + "line": 6, + "character": 0, + "endLine": 6, + "endCharacter": 10, + "package": "flake8-csv", + "code": "not-installed", + "severity": 3, + }, + { + "line": 10, + "character": 0, + "endLine": 10, + "endCharacter": 11, + "package": "levenshtein", + "code": "not-installed", + "severity": 3, + }, + ], + "no-missing-deps": [], + "pyproject-missing-deps": [ + { + "line": 8, + "character": 34, + "endLine": 8, + "endCharacter": 44, + "package": "flake8-csv", + "code": "not-installed", + "severity": 3, + } + ], + "pyproject-no-missing-deps": [], +} + + +@pytest.mark.parametrize("test_name", EXPECTED_DATA.keys()) +def test_installed_check(test_name: str): + base_file = TEST_DATA / f"{test_name}.data" + with generate_file(base_file) as file_path: + result = run_on_file(file_path) + assert result == EXPECTED_DATA[test_name] diff --git a/requirements.in b/requirements.in index 8b76e392917e..bdd5e10dfc47 100644 --- a/requirements.in +++ b/requirements.in @@ -8,3 +8,8 @@ typing-extensions==4.5.0 # Fallback env creator for debian microvenv + +# Checker for installed packages +importlib_metadata +packaging +tomli diff --git a/requirements.txt b/requirements.txt index 07145e1832d5..1419e0528ccc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,11 +4,29 @@ # # pip-compile --generate-hashes requirements.in # +importlib-metadata==6.6.0 \ + --hash=sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed \ + --hash=sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705 + # via -r requirements.in microvenv==2023.2.0 \ --hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \ --hash=sha256:a07e88a8fb5ee90219b86dd90095cb5646462d45d30285ea3b1a3c7cf33616d3 # via -r requirements.in +packaging==23.1 \ + --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \ + --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f + # via -r requirements.in +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f + # via -r requirements.in typing-extensions==4.5.0 \ --hash=sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb \ --hash=sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4 - # via -r requirements.in + # via + # -r requirements.in + # importlib-metadata +zipp==3.15.0 \ + --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ + --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 + # via importlib-metadata diff --git a/src/client/common/process/internal/scripts/index.ts b/src/client/common/process/internal/scripts/index.ts index c52983d9910b..7240d7be67f8 100644 --- a/src/client/common/process/internal/scripts/index.ts +++ b/src/client/common/process/internal/scripts/index.ts @@ -149,3 +149,8 @@ export function createCondaScript(): string { const script = path.join(SCRIPTS_DIR, 'create_conda.py'); return script; } + +export function installedCheckScript(): string { + const script = path.join(SCRIPTS_DIR, 'installed_check.py'); + return script; +} diff --git a/src/client/common/vscodeApis/languageApis.ts b/src/client/common/vscodeApis/languageApis.ts new file mode 100644 index 000000000000..87681507693d --- /dev/null +++ b/src/client/common/vscodeApis/languageApis.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { DiagnosticChangeEvent, DiagnosticCollection, Disposable, languages } from 'vscode'; + +export function createDiagnosticCollection(name: string): DiagnosticCollection { + return languages.createDiagnosticCollection(name); +} + +export function onDidChangeDiagnostics(handler: (e: DiagnosticChangeEvent) => void): Disposable { + return languages.onDidChangeDiagnostics(handler); +} diff --git a/src/client/common/vscodeApis/windowApis.ts b/src/client/common/vscodeApis/windowApis.ts index 5c279b890a9f..1c242314cb87 100644 --- a/src/client/common/vscodeApis/windowApis.ts +++ b/src/client/common/vscodeApis/windowApis.ts @@ -77,6 +77,10 @@ export function getActiveTextEditor(): TextEditor | undefined { return activeTextEditor; } +export function onDidChangeActiveTextEditor(handler: (e: TextEditor | undefined) => void): Disposable { + return window.onDidChangeActiveTextEditor(handler); +} + export enum MultiStepAction { Back = 'Back', Cancel = 'Cancel', diff --git a/src/client/common/vscodeApis/workspaceApis.ts b/src/client/common/vscodeApis/workspaceApis.ts index 20528c17ec11..0e860743a32d 100644 --- a/src/client/common/vscodeApis/workspaceApis.ts +++ b/src/client/common/vscodeApis/workspaceApis.ts @@ -36,12 +36,12 @@ export function findFiles( return vscode.workspace.findFiles(include, exclude, maxResults, token); } -export function onDidSaveTextDocument( - listener: (e: vscode.TextDocument) => unknown, - thisArgs?: unknown, - disposables?: vscode.Disposable[], -): vscode.Disposable { - return vscode.workspace.onDidSaveTextDocument(listener, thisArgs, disposables); +export function onDidCloseTextDocument(handler: (e: vscode.TextDocument) => unknown): vscode.Disposable { + return vscode.workspace.onDidCloseTextDocument(handler); +} + +export function onDidSaveTextDocument(handler: (e: vscode.TextDocument) => unknown): vscode.Disposable { + return vscode.workspace.onDidSaveTextDocument(handler); } export function getOpenTextDocuments(): readonly vscode.TextDocument[] { diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 542a6ccc3010..8dcea063676a 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -51,10 +51,9 @@ import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHan import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; import { WorkspaceService } from './common/application/workspace'; import { DynamicPythonDebugConfigurationService } from './debugger/extension/configuration/dynamicdebugConfigurationService'; -import { registerCreateEnvironmentFeatures } from './pythonEnvironments/creation/createEnvApi'; import { IInterpreterQuickPick } from './interpreter/configuration/types'; import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt'; -import { registerCreateEnvButtonFeatures } from './pythonEnvironments/creation/createEnvButtonContext'; +import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -97,8 +96,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components): IInterpreterPathService, ); const pathUtils = ext.legacyIOC.serviceContainer.get(IPathUtils); - registerCreateEnvironmentFeatures(ext.disposables, interpreterQuickPick, interpreterPathService, pathUtils); - registerCreateEnvButtonFeatures(ext.disposables); + registerAllCreateEnvironmentFeatures(ext.disposables, interpreterQuickPick, interpreterPathService, pathUtils); } /// ////////////////////////// diff --git a/src/client/pythonEnvironments/creation/common/installCheckUtils.ts b/src/client/pythonEnvironments/creation/common/installCheckUtils.ts new file mode 100644 index 000000000000..8c9817f83b7a --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/installCheckUtils.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Diagnostic, DiagnosticSeverity, l10n, Range, TextDocument, Uri } from 'vscode'; +import { installedCheckScript } from '../../../common/process/internal/scripts'; +import { plainExec } from '../../../common/process/rawProcessApis'; +import { IInterpreterPathService } from '../../../common/types'; +import { traceInfo, traceVerbose, traceError } from '../../../logging'; + +interface PackageDiagnostic { + package: string; + line: number; + character: number; + endLine: number; + endCharacter: number; + code: string; + severity: DiagnosticSeverity; +} + +export const INSTALL_CHECKER_SOURCE = 'Python-InstalledPackagesChecker'; + +function parseDiagnostics(data: string): Diagnostic[] { + let diagnostics: Diagnostic[] = []; + try { + const raw = JSON.parse(data) as PackageDiagnostic[]; + diagnostics = raw.map((item) => { + const d = new Diagnostic( + new Range(item.line, item.character, item.endLine, item.endCharacter), + l10n.t(`Package \`${item.package}\` is not installed in the selected environment.`), + item.severity, + ); + d.code = { value: item.code, target: Uri.parse(`https://pypi.org/p/${item.package}`) }; + d.source = INSTALL_CHECKER_SOURCE; + return d; + }); + } catch { + diagnostics = []; + } + return diagnostics; +} + +export async function getInstalledPackagesDiagnostics( + interpreterPathService: IInterpreterPathService, + doc: TextDocument, +): Promise { + const interpreter = interpreterPathService.get(doc.uri); + const scriptPath = installedCheckScript(); + try { + traceInfo('Running installed packages checker: ', interpreter, scriptPath, doc.uri.fsPath); + const result = await plainExec(interpreter, [scriptPath, doc.uri.fsPath]); + traceVerbose('Installed packages check result:\n', result.stdout); + if (result.stderr) { + traceError('Installed packages check error:\n', result.stderr); + } + return parseDiagnostics(result.stdout); + } catch (ex) { + traceError('Error while getting installed packages check result:\n', ex); + } + return []; +} diff --git a/src/client/pythonEnvironments/creation/createEnvButtonContext.ts b/src/client/pythonEnvironments/creation/createEnvButtonContext.ts index cd009bc6118a..4ce7d07ad69d 100644 --- a/src/client/pythonEnvironments/creation/createEnvButtonContext.ts +++ b/src/client/pythonEnvironments/creation/createEnvButtonContext.ts @@ -1,25 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { TextDocument, TextDocumentChangeEvent } from 'vscode'; import { IDisposableRegistry } from '../../common/types'; import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { - onDidOpenTextDocument, - onDidChangeTextDocument, - getOpenTextDocuments, - getConfiguration, - onDidChangeConfiguration, -} from '../../common/vscodeApis/workspaceApis'; -import { isPipInstallableToml } from './provider/venvUtils'; - -async function setPyProjectTomlContextKey(doc: TextDocument): Promise { - if (isPipInstallableToml(doc.getText())) { - await executeCommand('setContext', 'pipInstallableToml', true); - } else { - await executeCommand('setContext', 'pipInstallableToml', false); - } -} +import { getConfiguration, onDidChangeConfiguration } from '../../common/vscodeApis/workspaceApis'; async function setShowCreateEnvButtonContextKey(): Promise { const config = getConfiguration('python'); @@ -27,32 +11,12 @@ async function setShowCreateEnvButtonContextKey(): Promise { await executeCommand('setContext', 'showCreateEnvButton', showCreateEnvButton); } -export function registerCreateEnvButtonFeatures(disposables: IDisposableRegistry): void { +export function registerCreateEnvironmentButtonFeatures(disposables: IDisposableRegistry): void { disposables.push( - onDidOpenTextDocument(async (doc: TextDocument) => { - if (doc.fileName.endsWith('pyproject.toml')) { - await setPyProjectTomlContextKey(doc); - } - }), - onDidChangeTextDocument(async (e: TextDocumentChangeEvent) => { - const doc = e.document; - if (doc.fileName.endsWith('pyproject.toml')) { - await setPyProjectTomlContextKey(doc); - } - }), onDidChangeConfiguration(async () => { await setShowCreateEnvButtonContextKey(); }), ); setShowCreateEnvButtonContextKey(); - - const docs = getOpenTextDocuments().filter( - (doc) => doc.fileName.endsWith('pyproject.toml') && isPipInstallableToml(doc.getText()), - ); - if (docs.length > 0) { - executeCommand('setContext', 'pipInstallableToml', true); - } else { - executeCommand('setContext', 'pipInstallableToml', false); - } } diff --git a/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts b/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts new file mode 100644 index 000000000000..a46a32ce8276 --- /dev/null +++ b/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Diagnostic, DiagnosticCollection, TextDocument, Uri } from 'vscode'; +import { IDisposableRegistry, IInterpreterPathService } from '../../common/types'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { createDiagnosticCollection, onDidChangeDiagnostics } from '../../common/vscodeApis/languageApis'; +import { getActiveTextEditor, onDidChangeActiveTextEditor } from '../../common/vscodeApis/windowApis'; +import { + getOpenTextDocuments, + onDidCloseTextDocument, + onDidOpenTextDocument, + onDidSaveTextDocument, +} from '../../common/vscodeApis/workspaceApis'; +import { traceVerbose } from '../../logging'; +import { getInstalledPackagesDiagnostics, INSTALL_CHECKER_SOURCE } from './common/installCheckUtils'; + +export const DEPS_NOT_INSTALLED_KEY = 'pythonDepsNotInstalled'; + +async function setContextForActiveEditor(diagnosticCollection: DiagnosticCollection): Promise { + const doc = getActiveTextEditor()?.document; + if (doc && (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml'))) { + const diagnostics = diagnosticCollection.get(doc.uri); + if (diagnostics && diagnostics.length > 0) { + traceVerbose(`Setting context for python dependencies not installed: ${doc.uri.fsPath}`); + await executeCommand('setContext', DEPS_NOT_INSTALLED_KEY, true); + return; + } + } + + // undefined here in the logs means no file was selected + traceVerbose(`Clearing context for python dependencies not installed: ${doc?.uri.fsPath}`); + await executeCommand('setContext', DEPS_NOT_INSTALLED_KEY, false); +} + +export function registerInstalledPackagesDiagnosticsProvider( + disposables: IDisposableRegistry, + interpreterPathService: IInterpreterPathService, +): void { + const diagnosticCollection = createDiagnosticCollection(INSTALL_CHECKER_SOURCE); + const updateDiagnostics = (uri: Uri, diagnostics: Diagnostic[]) => { + if (diagnostics.length > 0) { + diagnosticCollection.set(uri, diagnostics); + } else if (diagnosticCollection.has(uri)) { + diagnosticCollection.delete(uri); + } + }; + + disposables.push(diagnosticCollection); + disposables.push( + onDidOpenTextDocument(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterPathService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }), + onDidSaveTextDocument(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterPathService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }), + onDidCloseTextDocument((e: TextDocument) => { + updateDiagnostics(e.uri, []); + }), + onDidChangeDiagnostics(async () => { + await setContextForActiveEditor(diagnosticCollection); + }), + onDidChangeActiveTextEditor(async () => { + await setContextForActiveEditor(diagnosticCollection); + }), + interpreterPathService.onDidChange(() => { + getOpenTextDocuments().forEach(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterPathService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }); + }), + ); + + getOpenTextDocuments().forEach(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterPathService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }); +} diff --git a/src/client/pythonEnvironments/creation/pyProjectTomlContext.ts b/src/client/pythonEnvironments/creation/pyProjectTomlContext.ts new file mode 100644 index 000000000000..5925b7641f45 --- /dev/null +++ b/src/client/pythonEnvironments/creation/pyProjectTomlContext.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TextDocument } from 'vscode'; +import { IDisposableRegistry } from '../../common/types'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { + onDidOpenTextDocument, + onDidSaveTextDocument, + getOpenTextDocuments, +} from '../../common/vscodeApis/workspaceApis'; +import { isPipInstallableToml } from './provider/venvUtils'; + +async function setPyProjectTomlContextKey(doc: TextDocument): Promise { + if (isPipInstallableToml(doc.getText())) { + await executeCommand('setContext', 'pipInstallableToml', true); + } else { + await executeCommand('setContext', 'pipInstallableToml', false); + } +} + +export function registerPyProjectTomlFeatures(disposables: IDisposableRegistry): void { + disposables.push( + onDidOpenTextDocument(async (doc: TextDocument) => { + if (doc.fileName.endsWith('pyproject.toml')) { + await setPyProjectTomlContextKey(doc); + } + }), + onDidSaveTextDocument(async (doc: TextDocument) => { + if (doc.fileName.endsWith('pyproject.toml')) { + await setPyProjectTomlContextKey(doc); + } + }), + ); + + const docs = getOpenTextDocuments().filter( + (doc) => doc.fileName.endsWith('pyproject.toml') && isPipInstallableToml(doc.getText()), + ); + if (docs.length > 0) { + executeCommand('setContext', 'pipInstallableToml', true); + } else { + executeCommand('setContext', 'pipInstallableToml', false); + } +} diff --git a/src/client/pythonEnvironments/creation/registrations.ts b/src/client/pythonEnvironments/creation/registrations.ts new file mode 100644 index 000000000000..eeb04036bc1b --- /dev/null +++ b/src/client/pythonEnvironments/creation/registrations.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IDisposableRegistry, IInterpreterPathService, IPathUtils } from '../../common/types'; +import { IInterpreterQuickPick } from '../../interpreter/configuration/types'; +import { registerCreateEnvironmentFeatures } from './createEnvApi'; +import { registerCreateEnvironmentButtonFeatures } from './createEnvButtonContext'; +import { registerInstalledPackagesDiagnosticsProvider } from './installedPackagesDiagnostic'; +import { registerPyProjectTomlFeatures } from './pyProjectTomlContext'; + +export function registerAllCreateEnvironmentFeatures( + disposables: IDisposableRegistry, + interpreterQuickPick: IInterpreterQuickPick, + interpreterPathService: IInterpreterPathService, + pathUtils: IPathUtils, +): void { + registerCreateEnvironmentFeatures(disposables, interpreterQuickPick, interpreterPathService, pathUtils); + registerCreateEnvironmentButtonFeatures(disposables); + registerPyProjectTomlFeatures(disposables); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService); +} diff --git a/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts b/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts new file mode 100644 index 000000000000..de8e263fc3fe --- /dev/null +++ b/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { Diagnostic, TextDocument, Range, Uri } from 'vscode'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { getInstalledPackagesDiagnostics } from '../../../../client/pythonEnvironments/creation/common/installCheckUtils'; +import { IInterpreterPathService } from '../../../../client/common/types'; + +chaiUse(chaiAsPromised); + +function getSomeRequirementFile(): typemoq.IMock { + const someFilePath = 'requirements.txt'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.languageId).returns(() => 'pip-requirements'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'flake8-csv'); + return someFile; +} + +const MISSING_PACKAGES_STR = + '[{"line": 8, "character": 34, "endLine": 8, "endCharacter": 44, "package": "flake8-csv", "code": "not-installed", "severity": 3}]'; +const MISSING_PACKAGES: Diagnostic[] = [ + { + range: new Range(8, 34, 8, 44), + message: 'Package `flake8-csv` is not installed in the selected environment.', + source: 'Python-InstalledPackagesChecker', + code: { value: 'not-installed', target: Uri.parse(`https://pypi.org/p/flake8-csv`) }, + severity: 3, + relatedInformation: [], + }, +]; + +suite('Install check diagnostics tests', () => { + let plainExecStub: sinon.SinonStub; + let interpreterPathService: typemoq.IMock; + + setup(() => { + plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); + interpreterPathService = typemoq.Mock.ofType(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Test parse diagnostics', async () => { + plainExecStub.resolves({ stdout: MISSING_PACKAGES_STR, stderr: '' }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterPathService.object, someFile.object); + + assert.deepStrictEqual(result, MISSING_PACKAGES); + }); + + test('Test parse empty diagnostics', async () => { + plainExecStub.resolves({ stdout: '', stderr: '' }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterPathService.object, someFile.object); + + assert.deepStrictEqual(result, []); + }); +}); diff --git a/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts index 31842420dd59..eec2d066aadb 100644 --- a/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts +++ b/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts @@ -1,16 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -/* eslint-disable @typescript-eslint/no-explicit-any */ import * as chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; import { assert, use as chaiUse } from 'chai'; -import { TextDocument, TextDocumentChangeEvent, WorkspaceConfiguration } from 'vscode'; +import { WorkspaceConfiguration } from 'vscode'; import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; import { IDisposableRegistry } from '../../../client/common/types'; -import { registerCreateEnvButtonFeatures } from '../../../client/pythonEnvironments/creation/createEnvButtonContext'; +import { registerCreateEnvironmentButtonFeatures } from '../../../client/pythonEnvironments/creation/createEnvButtonContext'; chaiUse(chaiAsPromised); @@ -20,57 +19,17 @@ class FakeDisposable { } } -function getInstallableToml(): typemoq.IMock { - const pyprojectTomlPath = 'pyproject.toml'; - const pyprojectToml = typemoq.Mock.ofType(); - pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); - pyprojectToml - .setup((p) => p.getText(typemoq.It.isAny())) - .returns( - () => - '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', - ); - return pyprojectToml; -} - -function getNonInstallableToml(): typemoq.IMock { - const pyprojectTomlPath = 'pyproject.toml'; - const pyprojectToml = typemoq.Mock.ofType(); - pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); - pyprojectToml - .setup((p) => p.getText(typemoq.It.isAny())) - .returns(() => '[project]\nname = "spam"\nversion = "2020.0.0"\n'); - return pyprojectToml; -} - -function getSomeFile(): typemoq.IMock { - const someFilePath = 'something.py'; - const someFile = typemoq.Mock.ofType(); - someFile.setup((p) => p.fileName).returns(() => someFilePath); - someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); - return someFile; -} - -suite('PyProject.toml Create Env Features', () => { +suite('Create Env content button settings tests', () => { let executeCommandStub: sinon.SinonStub; const disposables: IDisposableRegistry = []; - let getOpenTextDocumentsStub: sinon.SinonStub; - let onDidOpenTextDocumentStub: sinon.SinonStub; - let onDidChangeTextDocumentStub: sinon.SinonStub; let onDidChangeConfigurationStub: sinon.SinonStub; let getConfigurationStub: sinon.SinonStub; let configMock: typemoq.IMock; setup(() => { executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); - getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); - onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); - onDidChangeTextDocumentStub = sinon.stub(workspaceApis, 'onDidChangeTextDocument'); getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); onDidChangeConfigurationStub = sinon.stub(workspaceApis, 'onDidChangeConfiguration'); - - onDidOpenTextDocumentStub.returns(new FakeDisposable()); - onDidChangeTextDocumentStub.returns(new FakeDisposable()); onDidChangeConfigurationStub.returns(new FakeDisposable()); configMock = typemoq.Mock.ofType(); @@ -84,29 +43,23 @@ suite('PyProject.toml Create Env Features', () => { }); test('python.createEnvironment.contentButton setting is set to "show", no files open', async () => { - getOpenTextDocumentsStub.returns([]); - - registerCreateEnvButtonFeatures(disposables); + registerCreateEnvironmentButtonFeatures(disposables); assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); }); test('python.createEnvironment.contentButton setting is set to "hide", no files open', async () => { configMock.reset(); configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); - getOpenTextDocumentsStub.returns([]); - registerCreateEnvButtonFeatures(disposables); + registerCreateEnvironmentButtonFeatures(disposables); assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); }); test('python.createEnvironment.contentButton setting changed from "hide" to "show"', async () => { configMock.reset(); configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); - getOpenTextDocumentsStub.returns([]); let handler: () => void = () => { /* do nothing */ @@ -116,9 +69,8 @@ suite('PyProject.toml Create Env Features', () => { return new FakeDisposable(); }); - registerCreateEnvButtonFeatures(disposables); + registerCreateEnvironmentButtonFeatures(disposables); assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); executeCommandStub.reset(); configMock.reset(); @@ -131,7 +83,6 @@ suite('PyProject.toml Create Env Features', () => { test('python.createEnvironment.contentButton setting changed from "show" to "hide"', async () => { configMock.reset(); configMock.setup((c) => c.get(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); - getOpenTextDocumentsStub.returns([]); let handler: () => void = () => { /* do nothing */ @@ -141,9 +92,8 @@ suite('PyProject.toml Create Env Features', () => { return new FakeDisposable(); }); - registerCreateEnvButtonFeatures(disposables); + registerCreateEnvironmentButtonFeatures(disposables); assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); executeCommandStub.reset(); configMock.reset(); @@ -152,195 +102,4 @@ suite('PyProject.toml Create Env Features', () => { assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); }); - - test('Installable pyproject.toml is already open in the editor on extension activate', async () => { - const pyprojectToml = getInstallableToml(); - getOpenTextDocumentsStub.returns([pyprojectToml.object]); - - registerCreateEnvButtonFeatures(disposables); - - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); - }); - - test('Non installable pyproject.toml is already open in the editor on extension activate', async () => { - const pyprojectToml = getNonInstallableToml(); - getOpenTextDocumentsStub.returns([pyprojectToml.object]); - - registerCreateEnvButtonFeatures(disposables); - - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - }); - - test('Some random file open in the editor on extension activate', async () => { - const someFile = getSomeFile(); - getOpenTextDocumentsStub.returns([someFile.object]); - - registerCreateEnvButtonFeatures(disposables); - - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - }); - - test('Installable pyproject.toml is opened in the editor', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (doc: TextDocument) => void = () => { - /* do nothing */ - }; - onDidOpenTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getInstallableToml(); - - registerCreateEnvButtonFeatures(disposables); - assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', true)); - - handler(pyprojectToml.object); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); - }); - - test('Non Installable pyproject.toml is opened in the editor', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (doc: TextDocument) => void = () => { - /* do nothing */ - }; - onDidOpenTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getNonInstallableToml(); - - registerCreateEnvButtonFeatures(disposables); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - executeCommandStub.reset(); - - handler(pyprojectToml.object); - - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - }); - - test('Some random file is opened in the editor', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (doc: TextDocument) => void = () => { - /* do nothing */ - }; - onDidOpenTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const someFile = getSomeFile(); - - registerCreateEnvButtonFeatures(disposables); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - executeCommandStub.reset(); - - handler(someFile.object); - - assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', false)); - }); - - test('Installable pyproject.toml is changed', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (d: TextDocumentChangeEvent) => void = () => { - /* do nothing */ - }; - onDidChangeTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getInstallableToml(); - - registerCreateEnvButtonFeatures(disposables); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - - handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined }); - - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); - }); - - test('Non Installable pyproject.toml is changed', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (d: TextDocumentChangeEvent) => void = () => { - /* do nothing */ - }; - onDidChangeTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const pyprojectToml = getNonInstallableToml(); - - registerCreateEnvButtonFeatures(disposables); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - executeCommandStub.reset(); - - handler({ contentChanges: [], document: pyprojectToml.object, reason: undefined }); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); - }); - - test('Non Installable pyproject.toml is changed to Installable', async () => { - getOpenTextDocumentsStub.returns([]); - - let openHandler: (doc: TextDocument) => void = () => { - /* do nothing */ - }; - onDidOpenTextDocumentStub.callsFake((callback) => { - openHandler = callback; - return new FakeDisposable(); - }); - - let changeHandler: (d: TextDocumentChangeEvent) => void = () => { - /* do nothing */ - }; - onDidChangeTextDocumentStub.callsFake((callback) => { - changeHandler = callback; - return new FakeDisposable(); - }); - - const nonInatallablePyprojectToml = getNonInstallableToml(); - const installablePyprojectToml = getInstallableToml(); - - registerCreateEnvButtonFeatures(disposables); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - executeCommandStub.reset(); - - openHandler(nonInatallablePyprojectToml.object); - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); - executeCommandStub.reset(); - - changeHandler({ contentChanges: [], document: installablePyprojectToml.object, reason: undefined }); - - assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); - }); - - test('Some random file is changed', async () => { - getOpenTextDocumentsStub.returns([]); - - let handler: (d: TextDocumentChangeEvent) => void = () => { - /* do nothing */ - }; - onDidChangeTextDocumentStub.callsFake((callback) => { - handler = callback; - return new FakeDisposable(); - }); - - const someFile = getSomeFile(); - - registerCreateEnvButtonFeatures(disposables); - assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); - executeCommandStub.reset(); - - handler({ contentChanges: [], document: someFile.object, reason: undefined }); - - assert.ok(executeCommandStub.notCalled); - }); }); diff --git a/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts b/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts new file mode 100644 index 000000000000..10fe06bba442 --- /dev/null +++ b/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts @@ -0,0 +1,333 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { Diagnostic, DiagnosticCollection, TextEditor, Range, Uri, TextDocument } from 'vscode'; +import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as languageApis from '../../../client/common/vscodeApis/languageApis'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { IDisposableRegistry, IInterpreterPathService } from '../../../client/common/types'; +import * as installUtils from '../../../client/pythonEnvironments/creation/common/installCheckUtils'; +import { + DEPS_NOT_INSTALLED_KEY, + registerInstalledPackagesDiagnosticsProvider, +} from '../../../client/pythonEnvironments/creation/installedPackagesDiagnostic'; + +chaiUse(chaiAsPromised); + +class FakeDisposable { + public dispose() { + // Do nothing + } +} + +const MISSING_PACKAGES: Diagnostic[] = [ + { + range: new Range(8, 34, 8, 44), + message: 'Package `flake8-csv` is not installed in the selected environment.', + source: 'Python-InstalledPackagesChecker', + code: { value: 'not-installed', target: Uri.parse(`https://pypi.org/p/flake8-csv`) }, + severity: 3, + relatedInformation: [], + }, +]; + +function getSomeFile(): typemoq.IMock { + const someFilePath = 'something.py'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); + return someFile; +} + +function getSomeRequirementFile(): typemoq.IMock { + const someFilePath = 'requirements.txt'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.languageId).returns(() => 'pip-requirements'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'flake8-csv'); + return someFile; +} + +function getPyProjectTomlFile(): typemoq.IMock { + const someFilePath = 'pyproject.toml'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.languageId).returns(() => 'toml'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[project]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.7"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + ); + return someFile; +} + +function getSomeTomlFile(): typemoq.IMock { + const someFilePath = 'something.toml'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.languageId).returns(() => 'toml'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[something]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.7"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + ); + return someFile; +} + +suite('Create Env content button settings tests', () => { + let executeCommandStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let getOpenTextDocumentsStub: sinon.SinonStub; + let onDidOpenTextDocumentStub: sinon.SinonStub; + let onDidSaveTextDocumentStub: sinon.SinonStub; + let onDidCloseTextDocumentStub: sinon.SinonStub; + let onDidChangeDiagnosticsStub: sinon.SinonStub; + let onDidChangeActiveTextEditorStub: sinon.SinonStub; + let createDiagnosticCollectionStub: sinon.SinonStub; + let diagnosticCollection: typemoq.IMock; + let getActiveTextEditorStub: sinon.SinonStub; + let textEditor: typemoq.IMock; + let getInstalledPackagesDiagnosticsStub: sinon.SinonStub; + let interpreterPathService: typemoq.IMock; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + + getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); + getOpenTextDocumentsStub.returns([]); + + onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); + onDidSaveTextDocumentStub = sinon.stub(workspaceApis, 'onDidSaveTextDocument'); + onDidCloseTextDocumentStub = sinon.stub(workspaceApis, 'onDidCloseTextDocument'); + onDidOpenTextDocumentStub.returns(new FakeDisposable()); + onDidSaveTextDocumentStub.returns(new FakeDisposable()); + onDidCloseTextDocumentStub.returns(new FakeDisposable()); + + onDidChangeDiagnosticsStub = sinon.stub(languageApis, 'onDidChangeDiagnostics'); + onDidChangeDiagnosticsStub.returns(new FakeDisposable()); + createDiagnosticCollectionStub = sinon.stub(languageApis, 'createDiagnosticCollection'); + diagnosticCollection = typemoq.Mock.ofType(); + diagnosticCollection.setup((d) => d.set(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => undefined); + diagnosticCollection.setup((d) => d.clear()).returns(() => undefined); + diagnosticCollection.setup((d) => d.delete(typemoq.It.isAny())).returns(() => undefined); + diagnosticCollection.setup((d) => d.has(typemoq.It.isAny())).returns(() => false); + createDiagnosticCollectionStub.returns(diagnosticCollection.object); + + onDidChangeActiveTextEditorStub = sinon.stub(windowApis, 'onDidChangeActiveTextEditor'); + onDidChangeActiveTextEditorStub.returns(new FakeDisposable()); + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + textEditor = typemoq.Mock.ofType(); + getActiveTextEditorStub.returns(textEditor.object); + + getInstalledPackagesDiagnosticsStub = sinon.stub(installUtils, 'getInstalledPackagesDiagnostics'); + interpreterPathService = typemoq.Mock.ofType(); + interpreterPathService + .setup((i) => i.onDidChange(typemoq.It.isAny(), undefined, undefined)) + .returns(() => new FakeDisposable()); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('Ensure nothing is run if there are no open documents', () => { + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + assert.ok(executeCommandStub.notCalled); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test('Should not run packages check if opened files are not dep files', () => { + const someFile = getSomeFile(); + const someTomlFile = getSomeTomlFile(); + getOpenTextDocumentsStub.returns([someFile.object, someTomlFile.object]); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + assert.ok(executeCommandStub.notCalled); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test('Should run packages check if opened files are dep files', () => { + const reqFile = getSomeRequirementFile(); + const tomlFile = getPyProjectTomlFile(); + getOpenTextDocumentsStub.returns([reqFile.object, tomlFile.object]); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + assert.ok(getInstalledPackagesDiagnosticsStub.calledTwice); + }); + + [getSomeRequirementFile().object, getPyProjectTomlFile().object].forEach((file) => { + test(`Should run packages check on open of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.calledOnce); + }); + + test(`Should run packages check on save of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.calledOnce); + }); + + test(`Should run packages check on close of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidCloseTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + + diagnosticCollection.reset(); + diagnosticCollection.setup((d) => d.delete(typemoq.It.isAny())).verifiable(typemoq.Times.once()); + diagnosticCollection + .setup((d) => d.has(typemoq.It.isAny())) + .returns(() => true) + .verifiable(typemoq.Times.once()); + + handler(file); + diagnosticCollection.verifyAll(); + }); + + test(`Should trigger a context update on active editor switch to dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeActiveTextEditorStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => MISSING_PACKAGES); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, true)); + }); + + test(`Should trigger a context update to true on diagnostic change to dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeDiagnosticsStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => MISSING_PACKAGES); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, true)); + }); + }); + + [getSomeFile().object, getSomeTomlFile().object].forEach((file) => { + test(`Should not run packages check on open of a non dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test(`Should not run packages check on save of a non dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test(`Should trigger a context update on active editor switch to non-dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeActiveTextEditorStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => []); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, false)); + }); + + test(`Should trigger a context update to false on diagnostic change to non-dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeDiagnosticsStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterPathService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => []); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, false)); + }); + }); +}); diff --git a/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts b/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts new file mode 100644 index 000000000000..7106ee64162f --- /dev/null +++ b/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { TextDocument } from 'vscode'; +import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { registerPyProjectTomlFeatures } from '../../../client/pythonEnvironments/creation/pyProjectTomlContext'; + +chaiUse(chaiAsPromised); + +class FakeDisposable { + public dispose() { + // Do nothing + } +} + +function getInstallableToml(): typemoq.IMock { + const pyprojectTomlPath = 'pyproject.toml'; + const pyprojectToml = typemoq.Mock.ofType(); + pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); + pyprojectToml + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + ); + return pyprojectToml; +} + +function getNonInstallableToml(): typemoq.IMock { + const pyprojectTomlPath = 'pyproject.toml'; + const pyprojectToml = typemoq.Mock.ofType(); + pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); + pyprojectToml + .setup((p) => p.getText(typemoq.It.isAny())) + .returns(() => '[project]\nname = "spam"\nversion = "2020.0.0"\n'); + return pyprojectToml; +} + +function getSomeFile(): typemoq.IMock { + const someFilePath = 'something.py'; + const someFile = typemoq.Mock.ofType(); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); + return someFile; +} + +suite('PyProject.toml Create Env Features', () => { + let executeCommandStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let getOpenTextDocumentsStub: sinon.SinonStub; + let onDidOpenTextDocumentStub: sinon.SinonStub; + let onDidSaveTextDocumentStub: sinon.SinonStub; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); + onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); + onDidSaveTextDocumentStub = sinon.stub(workspaceApis, 'onDidSaveTextDocument'); + + onDidOpenTextDocumentStub.returns(new FakeDisposable()); + onDidSaveTextDocumentStub.returns(new FakeDisposable()); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('Installable pyproject.toml is already open in the editor on extension activate', async () => { + const pyprojectToml = getInstallableToml(); + getOpenTextDocumentsStub.returns([pyprojectToml.object]); + + registerPyProjectTomlFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non installable pyproject.toml is already open in the editor on extension activate', async () => { + const pyprojectToml = getNonInstallableToml(); + getOpenTextDocumentsStub.returns([pyprojectToml.object]); + + registerPyProjectTomlFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Some random file open in the editor on extension activate', async () => { + const someFile = getSomeFile(); + getOpenTextDocumentsStub.returns([someFile.object]); + + registerPyProjectTomlFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Installable pyproject.toml is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', true)); + + handler(pyprojectToml.object); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non Installable pyproject.toml is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getNonInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Some random file is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const someFile = getSomeFile(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(someFile.object); + + assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', false)); + }); + + test('Installable pyproject.toml is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non Installable pyproject.toml is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getNonInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Non Installable pyproject.toml is changed to Installable', async () => { + getOpenTextDocumentsStub.returns([]); + + let openHandler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + openHandler = callback; + return new FakeDisposable(); + }); + + let changeHandler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + changeHandler = callback; + return new FakeDisposable(); + }); + + const nonInatallablePyprojectToml = getNonInstallableToml(); + const installablePyprojectToml = getInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + openHandler(nonInatallablePyprojectToml.object); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + changeHandler(installablePyprojectToml.object); + + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Some random file is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const someFile = getSomeFile(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(someFile.object); + + assert.ok(executeCommandStub.notCalled); + }); +}); From c9a7268a7e6ae6f968ed595d4aaa4e0b187b7407 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 24 May 2023 10:57:25 -0700 Subject: [PATCH 0011/1136] Revert "Remove hack to check the vscode version" (#21294) Reverts microsoft/vscode-python#21180 For https://github.com/microsoft/vscode-python/issues/20769 --- package-lock.json | 2 +- package.json | 2 +- src/test/debuggerTest.ts | 4 ++-- src/test/multiRootTest.ts | 4 ++-- src/test/standardTest.ts | 4 ++-- src/test/utils/vscode.ts | 12 ++++++++++-- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index f00cbb0a912e..bfbd316f269d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.78.0" + "vscode": "^1.78.0-20230421" } }, "node_modules/@azure/abort-controller": { diff --git a/package.json b/package.json index 8b364b5d60e6..a92875a230d4 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.78.0" + "vscode": "^1.78.0-20230421" }, "keywords": [ "python", diff --git a/src/test/debuggerTest.ts b/src/test/debuggerTest.ts index 36a0060b9303..949f14caee3d 100644 --- a/src/test/debuggerTest.ts +++ b/src/test/debuggerTest.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { runTests } from '@vscode/test-electron'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; -import { getVersion } from './utils/vscode'; +import { getChannel } from './utils/vscode'; const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); process.env.IS_CI_SERVER_TEST_DEBUGGER = '1'; @@ -17,7 +17,7 @@ function start() { extensionDevelopmentPath: EXTENSION_ROOT_DIR_FOR_TESTS, extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), launchArgs: [workspacePath], - version: getVersion(), + version: getChannel(), extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, }).catch((ex) => { console.error('End Debugger tests (with errors)', ex); diff --git a/src/test/multiRootTest.ts b/src/test/multiRootTest.ts index 9a6d7cd1b904..c8c63b6dabe5 100644 --- a/src/test/multiRootTest.ts +++ b/src/test/multiRootTest.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import { runTests } from '@vscode/test-electron'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; import { initializeLogger } from './testLogger'; -import { getVersion } from './utils/vscode'; +import { getChannel } from './utils/vscode'; const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; @@ -17,7 +17,7 @@ function start() { extensionDevelopmentPath: EXTENSION_ROOT_DIR_FOR_TESTS, extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), launchArgs: [workspacePath], - version: getVersion(), + version: getChannel(), extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, }).catch((ex) => { console.error('End Multiroot tests (with errors)', ex); diff --git a/src/test/standardTest.ts b/src/test/standardTest.ts index c8416ebf1d7d..0562d1adf431 100644 --- a/src/test/standardTest.ts +++ b/src/test/standardTest.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import { downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath, runTests } from '@vscode/test-electron'; import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../client/common/constants'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; -import { getVersion } from './utils/vscode'; +import { getChannel } from './utils/vscode'; // If running smoke tests, we don't have access to this. if (process.env.TEST_FILES_SUFFIX !== 'smoke.test') { @@ -76,7 +76,7 @@ async function installPylanceExtension(vscodeExecutablePath: string) { async function start() { console.log('*'.repeat(100)); console.log('Start Standard tests'); - const channel = getVersion(); + const channel = getChannel(); console.log(`Using ${channel} build of VS Code.`); const vscodeExecutablePath = await downloadAndUnzipVSCode(channel); const baseLaunchArgs = diff --git a/src/test/utils/vscode.ts b/src/test/utils/vscode.ts index 44f745dbaf90..a10ceb2e8881 100644 --- a/src/test/utils/vscode.ts +++ b/src/test/utils/vscode.ts @@ -2,14 +2,22 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -export function getVersion(): string { +const insidersVersion = /^\^(\d+\.\d+\.\d+)-(insider|\d{8})$/; + +export function getChannel(): string { if (process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL) { return process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL; } const packageJsonPath = path.join(EXTENSION_ROOT_DIR, 'package.json'); if (fs.pathExistsSync(packageJsonPath)) { const packageJson = fs.readJSONSync(packageJsonPath); - return packageJson.engines.vscode.replace('^', ''); + const engineVersion = packageJson.engines.vscode; + if (insidersVersion.test(engineVersion)) { + // Can't pass in the version number for an insiders build; + // https://github.com/microsoft/vscode-test/issues/176 + return 'insiders'; + } + return engineVersion.replace('^', ''); } return 'stable'; } From 4b4e5b7f5ce637cdc626a5d473f8723cb72dccf7 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Wed, 24 May 2023 13:01:41 -0700 Subject: [PATCH 0012/1136] Update pyright version (#21296) Fix error in tests, updating pyright version --- .github/workflows/build.yml | 1 + .github/workflows/pr-check.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 805077ffdb46..f418a802ff8e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -103,6 +103,7 @@ jobs: - name: Run Pyright uses: jakebailey/pyright-action@v1 with: + version: 1.1.308 working-directory: 'pythonFiles' ### Non-smoke tests diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 2ac560af995d..84903463e204 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -77,6 +77,7 @@ jobs: - name: Run Pyright uses: jakebailey/pyright-action@v1 with: + version: 1.1.308 working-directory: 'pythonFiles' ### Non-smoke tests From f2f5fe26c41d7fff79b56b5a0490cd1c3b49f451 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Wed, 24 May 2023 13:31:26 -0700 Subject: [PATCH 0013/1136] Check config type in the ChildProcessAttachEvents (#21272) --- .../hooks/childProcessAttachHandler.ts | 3 ++- .../childProcessAttachHandler.unit.test.ts | 21 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts index 6851e54a8723..23602ffce086 100644 --- a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts +++ b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts @@ -9,6 +9,7 @@ import { swallowExceptions } from '../../../common/utils/decorators'; import { AttachRequestArguments } from '../../types'; import { DebuggerEvents } from './constants'; import { IChildProcessAttachService, IDebugSessionEventHandlers } from './types'; +import { DebuggerTypeName } from '../../constants'; /** * This class is responsible for automatically attaching the debugger to any @@ -25,7 +26,7 @@ export class ChildProcessAttachEventHandler implements IDebugSessionEventHandler @swallowExceptions('Handle child process launch') public async handleCustomEvent(event: DebugSessionCustomEvent): Promise { - if (!event) { + if (!event || event.session.configuration.type !== DebuggerTypeName) { return; } diff --git a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts index ee9a59c8e6aa..b1053def2eba 100644 --- a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts +++ b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts @@ -9,6 +9,7 @@ import { ChildProcessAttachEventHandler } from '../../../../client/debugger/exte import { ChildProcessAttachService } from '../../../../client/debugger/extension/hooks/childProcessAttachService'; import { DebuggerEvents } from '../../../../client/debugger/extension/hooks/constants'; import { AttachRequestArguments } from '../../../../client/debugger/types'; +import { DebuggerTypeName } from '../../../../client/debugger/constants'; suite('Debug - Child Process', () => { test('Do not attach if the event is undefined', async () => { @@ -21,7 +22,15 @@ suite('Debug - Child Process', () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); const body: any = {}; - const session: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; + await handler.handleCustomEvent({ event: 'abc', body, session }); + verify(attachService.attach(body, session)).never(); + }); + test('Do not attach to child process if debugger type is different', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + const body: any = {}; + const session: any = { configuration: { type: 'other-type' } }; await handler.handleCustomEvent({ event: 'abc', body, session }); verify(attachService.attach(body, session)).never(); }); @@ -29,7 +38,7 @@ suite('Debug - Child Process', () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); const body: any = {}; - const session: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; await handler.handleCustomEvent({ event: DebuggerEvents.PtvsdAttachToSubprocess, body, session }); verify(attachService.attach(body, session)).never(); }); @@ -37,7 +46,7 @@ suite('Debug - Child Process', () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); const body: any = {}; - const session: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); verify(attachService.attach(body, session)).never(); }); @@ -51,9 +60,11 @@ suite('Debug - Child Process', () => { port: 1234, subProcessId: 2, }; - const session: any = {}; + const session: any = { + configuration: { type: DebuggerTypeName }, + }; when(attachService.attach(body, session)).thenThrow(new Error('Kaboom')); - await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session: {} as any }); + await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); verify(attachService.attach(body, anything())).once(); const [, secondArg] = capture(attachService.attach).last(); expect(secondArg).to.deep.equal(session); From e2a9cecf9cec0f3a42d2723927a83d486c9b5bd8 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 24 May 2023 16:42:24 -0700 Subject: [PATCH 0014/1136] allow large scale testing (#21269) allows new testing rewrite to handle 500+ tests and load and run these tests. High limit tested was 10,000 tests. --- .vscode/launch.json | 3 +- pythonFiles/unittestadapter/discovery.py | 8 +- pythonFiles/unittestadapter/utils.py | 9 +- .../vscode_pytest/run_pytest_script.py | 90 +++++++++++++++++++ src/client/common/process/types.ts | 1 + .../testing/testController/common/server.ts | 32 ++++--- .../testing/testController/common/utils.ts | 37 ++++++++ .../pytest/pytestExecutionAdapter.ts | 54 +++++++++-- .../testController/workspaceTestAdapter.ts | 5 +- 9 files changed, 217 insertions(+), 22 deletions(-) create mode 100644 pythonFiles/vscode_pytest/run_pytest_script.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 1ca0db3dc858..82981a93305d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,8 +22,7 @@ // Enable this to log telemetry to the output during debugging "XVSC_PYTHON_LOG_TELEMETRY": "1", // Enable this to log debugger output. Directory must exist ahead of time - "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output_Ex", - "ENABLE_PYTHON_TESTING_REWRITE": "1" + "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output_Ex" } }, { diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index dc0a139ed5a2..bcc2fd967f78 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -8,7 +8,13 @@ import sys import traceback import unittest -from typing import List, Literal, Optional, Tuple, Union +from typing import List, Optional, Tuple, Union + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) + +from typing_extensions import Literal # Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. PYTHON_FILES = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff --git a/pythonFiles/unittestadapter/utils.py b/pythonFiles/unittestadapter/utils.py index 568ff30ee92d..9c8b896a8d6e 100644 --- a/pythonFiles/unittestadapter/utils.py +++ b/pythonFiles/unittestadapter/utils.py @@ -6,8 +6,15 @@ import inspect import os import pathlib +import sys import unittest -from typing import List, Tuple, TypedDict, Union +from typing import List, Tuple, Union + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) + +from typing_extensions import TypedDict # Types diff --git a/pythonFiles/vscode_pytest/run_pytest_script.py b/pythonFiles/vscode_pytest/run_pytest_script.py new file mode 100644 index 000000000000..f6d6bdcafd5f --- /dev/null +++ b/pythonFiles/vscode_pytest/run_pytest_script.py @@ -0,0 +1,90 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +import io +import json +import os +import pathlib +import socket +import sys +from typing import List + +import pytest + +CONTENT_LENGTH: str = "Content-Length:" + + +def process_rpc_json(data: str) -> List[str]: + """Process the JSON data which comes from the server which runs the pytest discovery.""" + str_stream: io.StringIO = io.StringIO(data) + + length: int = 0 + + while True: + line: str = str_stream.readline() + if CONTENT_LENGTH.lower() in line.lower(): + length = int(line[len(CONTENT_LENGTH) :]) + break + + if not line or line.isspace(): + raise ValueError("Header does not contain Content-Length") + + while True: + line: str = str_stream.readline() + if not line or line.isspace(): + break + + raw_json: str = str_stream.read(length) + return json.loads(raw_json) + + +# This script handles running pytest via pytest.main(). It is called via run in the +# pytest execution adapter and gets the test_ids to run via stdin and the rest of the +# args through sys.argv. It then runs pytest.main() with the args and test_ids. + +if __name__ == "__main__": + # Add the root directory to the path so that we can import the plugin. + directory_path = pathlib.Path(__file__).parent.parent + sys.path.append(os.fspath(directory_path)) + # Get the rest of the args to run with pytest. + args = sys.argv[1:] + run_test_ids_port = os.environ.get("RUN_TEST_IDS_PORT") + run_test_ids_port_int = ( + int(run_test_ids_port) if run_test_ids_port is not None else 0 + ) + test_ids_from_buffer = [] + try: + client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client_socket.connect(("localhost", run_test_ids_port_int)) + print(f"CLIENT: Server listening on port {run_test_ids_port_int}...") + buffer = b"" + + while True: + # Receive the data from the client + data = client_socket.recv(1024 * 1024) + if not data: + break + + # Append the received data to the buffer + buffer += data + + try: + # Try to parse the buffer as JSON + test_ids_from_buffer = process_rpc_json(buffer.decode("utf-8")) + # Clear the buffer as complete JSON object is received + buffer = b"" + + # Process the JSON data + print(f"Received JSON data: {test_ids_from_buffer}") + break + except json.JSONDecodeError: + # JSON decoding error, the complete JSON object is not yet received + continue + except socket.error as e: + print(f"Error: Could not connect to runTestIdsPort: {e}") + print("Error: Could not connect to runTestIdsPort") + try: + if test_ids_from_buffer: + arg_array = ["-p", "vscode_pytest"] + args + test_ids_from_buffer + pytest.main(arg_array) + except json.JSONDecodeError: + print("Error: Could not parse test ids from stdin") diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index 8298957285e8..62e787b694b5 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -25,6 +25,7 @@ export type SpawnOptions = ChildProcessSpawnOptions & { throwOnStdErr?: boolean; extraVariables?: NodeJS.ProcessEnv; outputChannel?: OutputChannel; + stdinStr?: string; }; export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean }; diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 6849f0f8969a..a00623aa33c7 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -26,23 +26,35 @@ export class PythonTestServer implements ITestServer, Disposable { constructor(private executionFactory: IPythonExecutionFactory, private debugLauncher: ITestDebugLauncher) { this.server = net.createServer((socket: net.Socket) => { + let buffer: Buffer = Buffer.alloc(0); // Buffer to accumulate received data socket.on('data', (data: Buffer) => { try { let rawData: string = data.toString(); - - while (rawData.length > 0) { - const rpcHeaders = jsonRPCHeaders(rawData); + buffer = Buffer.concat([buffer, data]); + while (buffer.length > 0) { + const rpcHeaders = jsonRPCHeaders(buffer.toString()); const uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); + const totalContentLength = rpcHeaders.headers.get('Content-Length'); + if (!uuid) { + traceLog('On data received: Error occurred because payload UUID is undefined'); + this._onDataReceived.fire({ uuid: '', data: '' }); + return; + } + if (!this.uuids.includes(uuid)) { + traceLog('On data received: Error occurred because the payload UUID is not recognized'); + this._onDataReceived.fire({ uuid: '', data: '' }); + return; + } rawData = rpcHeaders.remainingRawData; - if (uuid && this.uuids.includes(uuid)) { - const rpcContent = jsonRPCContent(rpcHeaders.headers, rawData); - rawData = rpcContent.remainingRawData; - this._onDataReceived.fire({ uuid, data: rpcContent.extractedJSON }); + const rpcContent = jsonRPCContent(rpcHeaders.headers, rawData); + const extractedData = rpcContent.extractedJSON; + if (extractedData.length === Number(totalContentLength)) { + // do not send until we have the full content + this._onDataReceived.fire({ uuid, data: extractedData }); this.uuids = this.uuids.filter((u) => u !== uuid); + buffer = Buffer.alloc(0); } else { - traceLog(`Error processing test server request: uuid not found`); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; + break; } } } catch (ex) { diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index e0bad383d695..88e3450d35dc 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -1,5 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as net from 'net'; +import { traceLog } from '../../../logging'; export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); @@ -50,3 +52,38 @@ export function jsonRPCContent(headers: Map, rawData: string): I remainingRawData, }; } +export const startServer = (testIds: string): Promise => + new Promise((resolve, reject) => { + const server = net.createServer((socket: net.Socket) => { + // Convert the test_ids array to JSON + const testData = JSON.stringify(testIds); + + // Create the headers + const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; + + // Create the payload by concatenating the headers and the test data + const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; + + // Send the payload to the socket + socket.write(payload); + + // Handle socket events + socket.on('data', (data) => { + traceLog('Received data:', data.toString()); + }); + + socket.on('end', () => { + traceLog('Client disconnected'); + }); + }); + + server.listen(0, () => { + const { port } = server.address() as net.AddressInfo; + traceLog(`Server listening on port ${port}`); + resolve(port); + }); + + server.on('error', (error: Error) => { + reject(error); + }); + }); diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 623fd1ff3a8c..4c6dcd9cdbee 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -3,9 +3,10 @@ import { Uri } from 'vscode'; import * as path from 'path'; +import * as net from 'net'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; -import { traceVerbose } from '../../../logging'; +import { traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, @@ -90,6 +91,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { TEST_PORT: this.testServer.getPort().toString(), }, outputChannel: this.outputChannel, + stdinStr: testIds.toString(), }; // Create the Python environment in which to execute the command. @@ -114,7 +116,48 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { testArgs.push('--capture', 'no'); } - const pluginArgs = ['-p', 'vscode_pytest', '-v'].concat(testArgs).concat(testIds); + const pluginArgs = ['-p', 'vscode_pytest'].concat(testArgs).concat(testIds); + const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); + const runArgs = [scriptPath, ...testArgs]; + + const testData = JSON.stringify(testIds); + const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; + const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; + + const startServer = (): Promise => + new Promise((resolve, reject) => { + const server = net.createServer((socket: net.Socket) => { + socket.on('end', () => { + traceLog('Client disconnected'); + }); + }); + + server.listen(0, () => { + const { port } = server.address() as net.AddressInfo; + traceLog(`Server listening on port ${port}`); + resolve(port); + }); + + server.on('error', (error: Error) => { + reject(error); + }); + server.on('connection', (socket: net.Socket) => { + socket.write(payload); + traceLog('payload sent', payload); + }); + }); + + // Start the server and wait until it is listening + await startServer() + .then((assignedPort) => { + traceLog(`Server started and listening on port ${assignedPort}`); + if (spawnOptions.extraVariables) + spawnOptions.extraVariables.RUN_TEST_IDS_PORT = assignedPort.toString(); + }) + .catch((error) => { + console.error('Error starting server:', error); + }); + if (debugBool) { const pytestPort = this.testServer.getPort().toString(); const pytestUUID = uuid.toString(); @@ -129,9 +172,10 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { console.debug(`Running debug test with arguments: ${pluginArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions); } else { - const runArgs = ['-m', 'pytest'].concat(pluginArgs); - console.debug(`Running test with arguments: ${runArgs.join(' ')}\r\n`); - execService?.exec(runArgs, spawnOptions); + await execService?.exec(runArgs, spawnOptions).catch((ex) => { + console.debug(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); + return Promise.reject(ex); + }); } } catch (ex) { console.debug(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index b22fee69d295..0b19d4d87d6f 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -348,12 +348,11 @@ export class WorkspaceTestAdapter { const testingErrorConst = this.testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; const { errors } = rawTestData; - traceError(testingErrorConst, '\r\n', errors!.join('\r\n\r\n')); - + traceError(testingErrorConst, '\r\n', errors?.join('\r\n\r\n')); let errorNode = testController.items.get(`DiscoveryError:${workspacePath}`); const message = util.format( `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, - errors!.join('\r\n\r\n'), + errors?.join('\r\n\r\n'), ); if (errorNode === undefined) { From b916981ed6a9a6a8267477637843e6fb4b66d7f3 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 24 May 2023 16:42:44 -0700 Subject: [PATCH 0015/1136] remove unneeded multiroot code (#21295) removed extra steps to wrap data since this creates duplicate folders in the controller and only keeps the most recent instead of all the roots from different workspaces. --- .../testing/testController/controller.ts | 5 +-- .../testController/workspaceTestAdapter.ts | 43 +------------------ 2 files changed, 2 insertions(+), 46 deletions(-) diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 58eaa9c890d6..20fcfa49bb69 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -254,8 +254,6 @@ export class PythonTestController implements ITestController, IExtensionSingleAc testAdapter.discoverTests( this.testController, this.refreshCancellation.token, - this.testAdapters.size > 1, - this.workspaceService.workspaceFile?.fsPath, this.pythonExecFactory, ); } else { @@ -274,8 +272,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc testAdapter.discoverTests( this.testController, this.refreshCancellation.token, - this.testAdapters.size > 1, - this.workspaceService.workspaceFile?.fsPath, + this.pythonExecFactory, ); } else { // else use OLD test discovery mechanism diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index 0b19d4d87d6f..ceb6a0b60afb 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -29,13 +29,7 @@ import { getTestCaseNodes, RunTestTag, } from './common/testItemUtilities'; -import { - DiscoveredTestItem, - DiscoveredTestNode, - DiscoveredTestType, - ITestDiscoveryAdapter, - ITestExecutionAdapter, -} from './common/types'; +import { DiscoveredTestItem, DiscoveredTestNode, ITestDiscoveryAdapter, ITestExecutionAdapter } from './common/types'; import { fixLogLines } from './common/utils'; import { IPythonExecutionFactory } from '../../common/process/types'; import { ITestDebugLauncher } from '../common/types'; @@ -288,8 +282,6 @@ export class WorkspaceTestAdapter { public async discoverTests( testController: TestController, token?: CancellationToken, - isMultiroot?: boolean, - workspaceFilePath?: string, executionFactory?: IPythonExecutionFactory, ): Promise { sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: this.testProvider }); @@ -366,39 +358,6 @@ export class WorkspaceTestAdapter { // then parse and insert test data. testController.items.delete(`DiscoveryError:${workspacePath}`); - // Wrap the data under a root node named after the test provider. - const wrappedTests = rawTestData.tests; - - // If we are in a multiroot workspace scenario, wrap the current folder's test result in a tree under the overall root + the current folder name. - let rootPath = workspacePath; - let childrenRootPath = rootPath; - let childrenRootName = path.basename(rootPath); - - if (isMultiroot) { - rootPath = workspaceFilePath!; - childrenRootPath = workspacePath; - childrenRootName = path.basename(workspacePath); - } - - const children = [ - { - path: childrenRootPath, - name: childrenRootName, - type_: 'folder' as DiscoveredTestType, - id_: childrenRootPath, - children: wrappedTests ? [wrappedTests] : [], - }, - ]; - - // Update the raw test data with the wrapped data. - rawTestData.tests = { - path: rootPath, - name: this.testProvider, - type_: 'folder', - id_: rootPath, - children, - }; - if (rawTestData.tests) { // If the test root for this folder exists: Workspace refresh, update its children. // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. From 4109228af1b3d151b01aeb6f11a7b83491599682 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 25 May 2023 10:08:31 -0700 Subject: [PATCH 0016/1136] fix debugging with new pytest run script (#21299) fix debugging for run_pytest_script.py setup --- .../common/process/internal/scripts/index.ts | 7 +++++ src/client/testing/common/debugLauncher.ts | 13 +++++----- src/client/testing/common/types.ts | 1 + .../pytest/pytestExecutionAdapter.ts | 26 +++++++++++-------- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/client/common/process/internal/scripts/index.ts b/src/client/common/process/internal/scripts/index.ts index 7240d7be67f8..f719844ef80b 100644 --- a/src/client/common/process/internal/scripts/index.ts +++ b/src/client/common/process/internal/scripts/index.ts @@ -110,6 +110,13 @@ export function testlauncher(testArgs: string[]): string[] { return [script, ...testArgs]; } +// run_pytest_script.py +export function pytestlauncher(testArgs: string[]): string[] { + const script = path.join(SCRIPTS_DIR, 'vscode_pytest', 'run_pytest_script.py'); + // There is no output to parse, so we do not return a function. + return [script, ...testArgs]; +} + // visualstudio_py_testlauncher.py // eslint-disable-next-line camelcase diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 5b39bd97a740..af233132768c 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -178,12 +178,7 @@ export class DebugLauncher implements ITestDebugLauncher { const args = script(testArgs); const [program] = args; configArgs.program = program; - // if the test provider is pytest, then use the pytest module instead of using a program - const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; - if (options.testProvider === 'pytest' && rewriteTestingEnabled) { - configArgs.module = 'pytest'; - configArgs.program = undefined; - } + configArgs.args = args.slice(1); // We leave configArgs.request as "test" so it will be sent in telemetry. @@ -204,12 +199,16 @@ export class DebugLauncher implements ITestDebugLauncher { throw Error(`Invalid debug config "${debugConfig.name}"`); } launchArgs.request = 'launch'; + + // If we are in the pytest rewrite then we must send additional environment variables. + const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; if (options.testProvider === 'pytest' && rewriteTestingEnabled) { if (options.pytestPort && options.pytestUUID) { launchArgs.env = { ...launchArgs.env, TEST_PORT: options.pytestPort, TEST_UUID: options.pytestUUID, + RUN_TEST_IDS_PORT: options.pytestRunTestIdsPort, }; } else { throw Error( @@ -238,7 +237,7 @@ export class DebugLauncher implements ITestDebugLauncher { } case 'pytest': { if (rewriteTestingEnabled) { - return (testArgs: string[]) => testArgs; + return internalScripts.pytestlauncher; // this is the new way to run pytest execution, debugger } return internalScripts.testlauncher; // old way pytest execution, debugger } diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts index a5d263058525..443a2c2ef20e 100644 --- a/src/client/testing/common/types.ts +++ b/src/client/testing/common/types.ts @@ -27,6 +27,7 @@ export type LaunchOptions = { outChannel?: OutputChannel; pytestPort?: string; pytestUUID?: string; + pytestRunTestIdsPort?: string; }; export type ParserOptions = TestDiscoveryOptions; diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 4c6dcd9cdbee..39b7f8c787b9 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import * as net from 'net'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; -import { traceLog, traceVerbose } from '../../../logging'; +import { traceError, traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, @@ -112,18 +112,16 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testArgs.splice(0, 0, '--rootdir', uri.fsPath); } - // why is this needed? if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { testArgs.push('--capture', 'no'); } - const pluginArgs = ['-p', 'vscode_pytest'].concat(testArgs).concat(testIds); - const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); - const runArgs = [scriptPath, ...testArgs]; + // create payload with testIds to send to run pytest script const testData = JSON.stringify(testIds); const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; + let pytestRunTestIdsPort: string | undefined; const startServer = (): Promise => new Promise((resolve, reject) => { const server = net.createServer((socket: net.Socket) => { @@ -151,11 +149,12 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { await startServer() .then((assignedPort) => { traceLog(`Server started and listening on port ${assignedPort}`); + pytestRunTestIdsPort = assignedPort.toString(); if (spawnOptions.extraVariables) - spawnOptions.extraVariables.RUN_TEST_IDS_PORT = assignedPort.toString(); + spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort; }) .catch((error) => { - console.error('Error starting server:', error); + traceError('Error starting server:', error); }); if (debugBool) { @@ -163,22 +162,27 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const pytestUUID = uuid.toString(); const launchOptions: LaunchOptions = { cwd: uri.fsPath, - args: pluginArgs, + args: testArgs, token: spawnOptions.token, testProvider: PYTEST_PROVIDER, pytestPort, pytestUUID, + pytestRunTestIdsPort, }; - console.debug(`Running debug test with arguments: ${pluginArgs.join(' ')}\r\n`); + traceVerbose(`Running debug test with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions); } else { + // combine path to run script with run args + const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); + const runArgs = [scriptPath, ...testArgs]; + await execService?.exec(runArgs, spawnOptions).catch((ex) => { - console.debug(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); + traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); return Promise.reject(ex); }); } } catch (ex) { - console.debug(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); + traceVerbose(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); return Promise.reject(ex); } From 72f7ef8113536d35fa000c8a75f7aae598575990 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 25 May 2023 13:41:55 -0700 Subject: [PATCH 0017/1136] Set up testing rewrite experiment (#21258) is the beginning of this issue: https://github.com/microsoft/vscode-python/issues/21150, in that it will start the process of implementing the setting in the extension --- src/client/common/experiments/groups.ts | 4 +++ src/client/testing/common/debugLauncher.ts | 17 +++++----- .../testing/testController/common/utils.ts | 10 ++++++ .../testing/testController/controller.ts | 21 ++++++------ .../testController/pytest/pytestController.ts | 31 +++++++++-------- .../unittest/unittestController.ts | 33 +++++++++++-------- .../testing/common/debugLauncher.unit.test.ts | 29 +++++++++------- 7 files changed, 88 insertions(+), 57 deletions(-) diff --git a/src/client/common/experiments/groups.ts b/src/client/common/experiments/groups.ts index 5884aafd122d..1ee06469095c 100644 --- a/src/client/common/experiments/groups.ts +++ b/src/client/common/experiments/groups.ts @@ -14,3 +14,7 @@ export enum TerminalEnvVarActivation { export enum ShowFormatterExtensionPrompt { experiment = 'pythonPromptNewFormatterExt', } +// Experiment to enable the new testing rewrite. +export enum EnableTestAdapterRewrite { + experiment = 'pythonTestAdapter', +} diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index af233132768c..0c13671b6e0d 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -16,6 +16,7 @@ import { getConfigurationsForWorkspace } from '../../debugger/extension/configur import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis/workspaceApis'; import { showErrorMessage } from '../../common/vscodeApis/windowApis'; import { createDeferred } from '../../common/utils/async'; +import { pythonTestAdapterRewriteEnabled } from '../testController/common/utils'; @injectable() export class DebugLauncher implements ITestDebugLauncher { @@ -87,6 +88,7 @@ export class DebugLauncher implements ITestDebugLauncher { path: path.join(EXTENSION_ROOT_DIR, 'pythonFiles'), include: false, }); + DebugLauncher.applyDefaults(debugConfig!, workspaceFolder, configSettings); return this.convertConfigToArgs(debugConfig!, workspaceFolder, options); @@ -171,10 +173,11 @@ export class DebugLauncher implements ITestDebugLauncher { workspaceFolder: WorkspaceFolder, options: LaunchOptions, ): Promise { + const pythonTestAdapterRewriteExperiment = pythonTestAdapterRewriteEnabled(this.serviceContainer); const configArgs = debugConfig as LaunchRequestArguments; const testArgs = options.testProvider === 'unittest' ? options.args.filter((item) => item !== '--debug') : options.args; - const script = DebugLauncher.getTestLauncherScript(options.testProvider); + const script = DebugLauncher.getTestLauncherScript(options.testProvider, pythonTestAdapterRewriteExperiment); const args = script(testArgs); const [program] = args; configArgs.program = program; @@ -199,10 +202,7 @@ export class DebugLauncher implements ITestDebugLauncher { throw Error(`Invalid debug config "${debugConfig.name}"`); } launchArgs.request = 'launch'; - - // If we are in the pytest rewrite then we must send additional environment variables. - const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; - if (options.testProvider === 'pytest' && rewriteTestingEnabled) { + if (options.testProvider === 'pytest' && pythonTestAdapterRewriteExperiment) { if (options.pytestPort && options.pytestUUID) { launchArgs.env = { ...launchArgs.env, @@ -226,17 +226,16 @@ export class DebugLauncher implements ITestDebugLauncher { return launchArgs; } - private static getTestLauncherScript(testProvider: TestProvider) { - const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; + private static getTestLauncherScript(testProvider: TestProvider, pythonTestAdapterRewriteExperiment?: boolean) { switch (testProvider) { case 'unittest': { - if (rewriteTestingEnabled) { + if (pythonTestAdapterRewriteExperiment) { return internalScripts.execution_py_testlauncher; // this is the new way to run unittest execution, debugger } return internalScripts.visualstudio_py_testlauncher; // old way unittest execution, debugger } case 'pytest': { - if (rewriteTestingEnabled) { + if (pythonTestAdapterRewriteExperiment) { return internalScripts.pytestlauncher; // this is the new way to run pytest execution, debugger } return internalScripts.testlauncher; // old way pytest execution, debugger diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 88e3450d35dc..1bf31e80e11a 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -3,6 +3,10 @@ import * as net from 'net'; import { traceLog } from '../../../logging'; +import { EnableTestAdapterRewrite } from '../../../common/experiments/groups'; +import { IExperimentService } from '../../../common/types'; +import { IServiceContainer } from '../../../ioc/types'; + export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); return `${lines.join('\r\n')}\r\n`; @@ -52,6 +56,12 @@ export function jsonRPCContent(headers: Map, rawData: string): I remainingRawData, }; } + +export function pythonTestAdapterRewriteEnabled(serviceContainer: IServiceContainer): boolean { + const experiment = serviceContainer.get(IExperimentService); + return experiment.inExperimentSync(EnableTestAdapterRewrite.experiment); +} + export const startServer = (testIds: string): Promise => new Promise((resolve, reject) => { const server = net.createServer((socket: net.Socket) => { diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 20fcfa49bb69..6c6b9f409e5f 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -44,6 +44,8 @@ import { PytestTestDiscoveryAdapter } from './pytest/pytestDiscoveryAdapter'; import { PytestTestExecutionAdapter } from './pytest/pytestExecutionAdapter'; import { WorkspaceTestAdapter } from './workspaceTestAdapter'; import { ITestDebugLauncher } from '../common/types'; +import { pythonTestAdapterRewriteEnabled } from './common/utils'; +import { IServiceContainer } from '../../ioc/types'; // Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER]; @@ -93,6 +95,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, @inject(ITestOutputChannel) private readonly testOutputChannel: ITestOutputChannel, + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, ) { this.refreshCancellation = new CancellationTokenSource(); @@ -241,12 +244,11 @@ export class PythonTestController implements ITestController, IExtensionSingleAc if (uri) { const settings = this.configSettings.getSettings(uri); traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`); - const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; if (settings.testing.pytestEnabled) { // Ensure we send test telemetry if it gets disabled again this.sendTestDisabledTelemetry = true; - if (rewriteTestingEnabled) { - // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism + // ** experiment to roll out NEW test discovery mechanism + if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { const workspace = this.workspaceService.getWorkspaceFolder(uri); traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); const testAdapter = @@ -263,8 +265,8 @@ export class PythonTestController implements ITestController, IExtensionSingleAc } else if (settings.testing.unittestEnabled) { // ** Ensure we send test telemetry if it gets disabled again this.sendTestDisabledTelemetry = true; - if (rewriteTestingEnabled) { - // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism + // ** experiment to roll out NEW test discovery mechanism + if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { const workspace = this.workspaceService.getWorkspaceFolder(uri); traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); const testAdapter = @@ -388,14 +390,13 @@ export class PythonTestController implements ITestController, IExtensionSingleAc const settings = this.configSettings.getSettings(workspace.uri); if (testItems.length > 0) { - const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; if (settings.testing.pytestEnabled) { sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { tool: 'pytest', debugging: request.profile?.kind === TestRunProfileKind.Debug, }); - // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism - if (rewriteTestingEnabled) { + // ** experiment to roll out NEW test discovery mechanism + if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { const testAdapter = this.testAdapters.get(workspace.uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); @@ -425,8 +426,8 @@ export class PythonTestController implements ITestController, IExtensionSingleAc tool: 'unittest', debugging: request.profile?.kind === TestRunProfileKind.Debug, }); - // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism - if (rewriteTestingEnabled) { + // ** experiment to roll out NEW test discovery mechanism + if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { const testAdapter = this.testAdapters.get(workspace.uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); diff --git a/src/client/testing/testController/pytest/pytestController.ts b/src/client/testing/testController/pytest/pytestController.ts index 793170231210..997e3e29b7ec 100644 --- a/src/client/testing/testController/pytest/pytestController.ts +++ b/src/client/testing/testController/pytest/pytestController.ts @@ -285,19 +285,24 @@ export class PytestController implements ITestFrameworkController { public runTests(testRun: ITestRun, workspace: WorkspaceFolder, token: CancellationToken): Promise { const settings = this.configService.getSettings(workspace.uri); - return this.runner.runTests( - testRun, - { - workspaceFolder: workspace.uri, - cwd: - settings.testing.cwd && settings.testing.cwd.length > 0 - ? settings.testing.cwd - : workspace.uri.fsPath, - token, - args: settings.testing.pytestArgs, - }, - this.idToRawData, - ); + try { + return this.runner.runTests( + testRun, + { + workspaceFolder: workspace.uri, + cwd: + settings.testing.cwd && settings.testing.cwd.length > 0 + ? settings.testing.cwd + : workspace.uri.fsPath, + token, + args: settings.testing.pytestArgs, + }, + this.idToRawData, + ); + } catch (ex) { + sendTelemetryEvent(EventName.UNITTEST_RUN_ALL_FAILED, undefined); + throw new Error(`Failed to run tests: ${ex}`); + } } } diff --git a/src/client/testing/testController/unittest/unittestController.ts b/src/client/testing/testController/unittest/unittestController.ts index ee79103c4e3e..a795620f3ca0 100644 --- a/src/client/testing/testController/unittest/unittestController.ts +++ b/src/client/testing/testController/unittest/unittestController.ts @@ -251,20 +251,25 @@ export class UnittestController implements ITestFrameworkController { testController?: TestController, ): Promise { const settings = this.configService.getSettings(workspace.uri); - return this.runner.runTests( - testRun, - { - workspaceFolder: workspace.uri, - cwd: - settings.testing.cwd && settings.testing.cwd.length > 0 - ? settings.testing.cwd - : workspace.uri.fsPath, - token, - args: settings.testing.unittestArgs, - }, - this.idToRawData, - testController, - ); + try { + return this.runner.runTests( + testRun, + { + workspaceFolder: workspace.uri, + cwd: + settings.testing.cwd && settings.testing.cwd.length > 0 + ? settings.testing.cwd + : workspace.uri.fsPath, + token, + args: settings.testing.unittestArgs, + }, + this.idToRawData, + testController, + ); + } catch (ex) { + sendTelemetryEvent(EventName.UNITTEST_RUN_ALL_FAILED, undefined); + throw new Error(`Failed to run tests: ${ex}`); + } } } diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index b8b7d5c55130..41b95c66040e 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -29,6 +29,7 @@ import { ITestingSettings } from '../../../client/testing/configuration/types'; import { TestProvider } from '../../../client/testing/types'; import { isOs, OSType } from '../../common'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import * as util from '../../../client/testing/testController/common/utils'; use(chaiAsPromised); @@ -45,6 +46,7 @@ suite('Unit Tests - Debug Launcher', () => { let getWorkspaceFoldersStub: sinon.SinonStub; let pathExistsStub: sinon.SinonStub; let readFileStub: sinon.SinonStub; + let pythonTestAdapterRewriteEnabledStub: sinon.SinonStub; const envVars = { FOO: 'BAR' }; setup(async () => { @@ -65,6 +67,8 @@ suite('Unit Tests - Debug Launcher', () => { getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); pathExistsStub = sinon.stub(fs, 'pathExists'); readFileStub = sinon.stub(fs, 'readFile'); + pythonTestAdapterRewriteEnabledStub = sinon.stub(util, 'pythonTestAdapterRewriteEnabled'); + pythonTestAdapterRewriteEnabledStub.returns(false); const appShell = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); @@ -143,19 +147,22 @@ suite('Unit Tests - Debug Launcher', () => { uri: Uri.file(folderPath), }; } - function getTestLauncherScript(testProvider: TestProvider) { - switch (testProvider) { - case 'unittest': { - return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'visualstudio_py_testlauncher.py'); - } - case 'pytest': { - return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'testlauncher.py'); - } - default: { - throw new Error(`Unknown test provider '${testProvider}'`); + function getTestLauncherScript(testProvider: TestProvider, pythonTestAdapterRewriteExperiment?: boolean) { + if (!pythonTestAdapterRewriteExperiment) { + switch (testProvider) { + case 'unittest': { + return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'visualstudio_py_testlauncher.py'); + } + case 'pytest': { + return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'testlauncher.py'); + } + default: { + throw new Error(`Unknown test provider '${testProvider}'`); + } } } } + function getDefaultDebugConfig(): DebugConfiguration { return { name: 'Debug Unit Test', @@ -178,7 +185,7 @@ suite('Unit Tests - Debug Launcher', () => { expected?: DebugConfiguration, debugConfigs?: string | DebugConfiguration[], ) { - const testLaunchScript = getTestLauncherScript(testProvider); + const testLaunchScript = getTestLauncherScript(testProvider, false); const workspaceFolders = [createWorkspaceFolder(options.cwd), createWorkspaceFolder('five/six/seven')]; getWorkspaceFoldersStub.returns(workspaceFolders); From c2134917896b009ac678dac9c712aba813b55e55 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 26 May 2023 09:26:55 -0700 Subject: [PATCH 0018/1136] Apply environment variables after shell initialization scripts are run in `pythonTerminalEnvVarActivation` experiment (#21290) For #11039 https://github.com/microsoft/vscode-python/issues/20822 Closes https://github.com/microsoft/vscode-python/issues/21297 Update proposed APIs to be used in Terminal activation experiment. --- package-lock.json | 2 +- package.json | 5 +- .../terminalEnvVarCollectionService.ts | 38 +++++++---- ...rminalEnvVarCollectionService.unit.test.ts | 66 ++++++++++--------- .../vscode.proposed.envCollectionOptions.d.ts | 56 ++++++++++++++++ ...scode.proposed.envCollectionWorkspace.d.ts | 44 ++++++------- 6 files changed, 141 insertions(+), 70 deletions(-) create mode 100644 types/src/vscode-dts/vscode.proposed.envCollectionOptions.d.ts diff --git a/package-lock.json b/package-lock.json index bfbd316f269d..a6bdd1be4c1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.78.0-20230421" + "vscode": "^1.79.0-20230525" } }, "node_modules/@azure/abort-controller": { diff --git a/package.json b/package.json index a92875a230d4..7918881bd115 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "testObserver", "quickPickItemTooltip", "envCollectionWorkspace", - "saveEditor" + "saveEditor", + "envCollectionOptions" ], "author": { "name": "Microsoft Corporation" @@ -45,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.78.0-20230421" + "vscode": "^1.79.0-20230526" }, "keywords": [ "python", diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 26852303d099..dbf77e72379a 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -3,7 +3,14 @@ import * as path from 'path'; import { inject, injectable } from 'inversify'; -import { ProgressOptions, ProgressLocation, MarkdownString, WorkspaceFolder } from 'vscode'; +import { + ProgressOptions, + ProgressLocation, + MarkdownString, + WorkspaceFolder, + EnvironmentVariableCollection, + EnvironmentVariableScope, +} from 'vscode'; import { pathExists } from 'fs-extra'; import { IExtensionActivationService } from '../../activation/types'; import { IApplicationShell, IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; @@ -108,6 +115,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ undefined, shell, ); + const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder); if (!env) { const shellType = identifyShellFromShellPath(shell); const defaultShell = defaultShells[this.platform.osType]; @@ -117,7 +125,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ await this._applyCollection(resource, defaultShell?.shell); return; } - this.context.environmentVariableCollection.clear({ workspaceFolder }); + envVarCollection.clear(); this.previousEnvVars = _normCaseKeys(process.env); return; } @@ -129,10 +137,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (prevValue !== value) { if (value !== undefined) { traceVerbose(`Setting environment variable ${key} in collection to ${value}`); - this.context.environmentVariableCollection.replace(key, value, { workspaceFolder }); + envVarCollection.replace(key, value, { applyAtShellIntegration: true }); } else { traceVerbose(`Clearing environment variable ${key} from collection`); - this.context.environmentVariableCollection.delete(key, { workspaceFolder }); + envVarCollection.delete(key); } } }); @@ -140,14 +148,21 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ // If the previous env var is not in the current env, clear it from collection. if (!(key in env)) { traceVerbose(`Clearing environment variable ${key} from collection`); - this.context.environmentVariableCollection.delete(key, { workspaceFolder }); + envVarCollection.delete(key); } }); const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); - this.context.environmentVariableCollection.setDescription(description, { - workspaceFolder, - }); + envVarCollection.description = description; + } + + private getEnvironmentVariableCollection(workspaceFolder?: WorkspaceFolder) { + const envVarCollection = this.context.environmentVariableCollection as EnvironmentVariableCollection & { + getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; + }; + return workspaceFolder + ? envVarCollection.getScopedEnvironmentVariableCollection({ workspaceFolder }) + : envVarCollection; } private async handleMicroVenv(resource: Resource) { @@ -156,12 +171,11 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (interpreter?.envType === EnvironmentType.Venv) { const activatePath = path.join(path.dirname(interpreter.path), 'activate'); if (!(await pathExists(activatePath))) { - this.context.environmentVariableCollection.replace( + const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder); + envVarCollection.replace( 'PATH', `${path.dirname(interpreter.path)}${path.delimiter}${process.env.Path}`, - { - workspaceFolder, - }, + { applyAtShellIntegration: true }, ); return; } diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index feecf63f5577..cb0b6b02f288 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -7,7 +7,13 @@ import * as sinon from 'sinon'; import { assert, expect } from 'chai'; import { cloneDeep } from 'lodash'; import { mock, instance, when, anything, verify, reset } from 'ts-mockito'; -import { EnvironmentVariableCollection, ProgressLocation, Uri, WorkspaceFolder } from 'vscode'; +import { + EnvironmentVariableCollection, + EnvironmentVariableScope, + ProgressLocation, + Uri, + WorkspaceFolder, +} from 'vscode'; import { IApplicationShell, IApplicationEnvironment, @@ -39,7 +45,10 @@ suite('Terminal Environment Variable Collection Service', () => { let context: IExtensionContext; let shell: IApplicationShell; let experimentService: IExperimentService; - let collection: EnvironmentVariableCollection; + let collection: EnvironmentVariableCollection & { + getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; + }; + let scopedCollection: EnvironmentVariableCollection; let applicationEnvironment: IApplicationEnvironment; let environmentActivationService: IEnvironmentActivationService; let workspaceService: IWorkspaceService; @@ -62,7 +71,13 @@ suite('Terminal Environment Variable Collection Service', () => { interpreterService = mock(); context = mock(); shell = mock(); - collection = mock(); + collection = mock< + EnvironmentVariableCollection & { + getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; + } + >(); + scopedCollection = mock(); + when(collection.getScopedEnvironmentVariableCollection(anything())).thenReturn(instance(scopedCollection)); when(context.environmentVariableCollection).thenReturn(instance(collection)); experimentService = mock(); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); @@ -166,12 +181,12 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(envVars); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.delete('PATH', anything())).once(); + verify(collection.delete('PATH')).once(); }); test('Verify envs are not applied if env activation is disabled', async () => { @@ -187,7 +202,7 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(envVars); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); reset(configService); when(configService.getSettings(anything())).thenReturn(({ terminal: { activateEnvironment: false }, @@ -197,10 +212,10 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).never(); - verify(collection.delete('PATH', anything())).never(); + verify(collection.delete('PATH')).never(); }); - test('Verify correct scope is used when applying envs and setting description', async () => { + test('Verify correct options are used when applying envs and setting description', async () => { const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; delete envVars.PATH; const resource = Uri.file('a'); @@ -214,25 +229,16 @@ suite('Terminal Environment Variable Collection Service', () => { environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, customShell), ).thenResolve(envVars); - when(collection.replace(anything(), anything(), anything())).thenCall((_e, _v, scope) => { - assert.deepEqual(scope, { workspaceFolder }); - return Promise.resolve(); - }); - when(collection.delete(anything(), anything())).thenCall((_e, scope) => { - assert.deepEqual(scope, { workspaceFolder }); + when(scopedCollection.replace(anything(), anything(), anything())).thenCall((_e, _v, options) => { + assert.deepEqual(options, { applyAtShellIntegration: true }); return Promise.resolve(); }); - let description = ''; - when(collection.setDescription(anything(), anything())).thenCall((d, scope) => { - assert.deepEqual(scope, { workspaceFolder }); - description = d.value; - }); + when(scopedCollection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(resource, customShell); - verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.delete('PATH', anything())).once(); - expect(description).to.equal(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); + verify(scopedCollection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + verify(scopedCollection.delete('PATH')).once(); }); test('Only relative changes to previously applied variables are applied to the collection', async () => { @@ -251,7 +257,7 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(envVars); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); @@ -270,8 +276,8 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); - verify(collection.delete('CONDA_PREFIX', anything())).once(); - verify(collection.delete('RANDOM_VAR', anything())).once(); + verify(collection.delete('CONDA_PREFIX')).once(); + verify(collection.delete('RANDOM_VAR')).once(); }); test('If no activated variables are returned for custom shell, fallback to using default shell', async () => { @@ -294,12 +300,12 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(envVars); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.delete(anything(), anything())).never(); + verify(collection.delete(anything())).never(); }); test('If no activated variables are returned for default shell, clear collection', async () => { @@ -313,12 +319,10 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(undefined); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything(), anything())).thenResolve(); - when(collection.setDescription(anything(), anything())).thenReturn(); + when(collection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, defaultShell?.shell); - verify(collection.clear(anything())).once(); - verify(collection.setDescription(anything(), anything())).never(); + verify(collection.clear()).once(); }); }); diff --git a/types/src/vscode-dts/vscode.proposed.envCollectionOptions.d.ts b/types/src/vscode-dts/vscode.proposed.envCollectionOptions.d.ts new file mode 100644 index 000000000000..d25a92725a4d --- /dev/null +++ b/types/src/vscode-dts/vscode.proposed.envCollectionOptions.d.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/179476 + + /** + * Options applied to the mutator. + */ + export interface EnvironmentVariableMutatorOptions { + /** + * Apply to the environment just before the process is created. + * + * Defaults to true. + */ + applyAtProcessCreation?: boolean; + + /** + * Apply to the environment in the shell integration script. Note that this _will not_ apply + * the mutator if shell integration is disabled or not working for some reason. + * + * Defaults to false. + */ + applyAtShellIntegration?: boolean; + } + + /** + * A type of mutation and its value to be applied to an environment variable. + */ + export interface EnvironmentVariableMutator { + /** + * Options applied to the mutator. + */ + readonly options: EnvironmentVariableMutatorOptions; + } + + export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { + /** + * @param options Options applied to the mutator. + */ + replace(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + + /** + * @param options Options applied to the mutator. + */ + append(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + + /** + * @param options Options applied to the mutator. + */ + prepend(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + } +} diff --git a/types/vscode.proposed.envCollectionWorkspace.d.ts b/types/vscode.proposed.envCollectionWorkspace.d.ts index b1176a9d46c2..d778e53e5086 100644 --- a/types/vscode.proposed.envCollectionWorkspace.d.ts +++ b/types/vscode.proposed.envCollectionWorkspace.d.ts @@ -5,34 +5,30 @@ declare module 'vscode' { - // https://github.com/microsoft/vscode/issues/171173 + // https://github.com/microsoft/vscode/issues/182069 - export interface EnvironmentVariableMutator { - readonly type: EnvironmentVariableMutatorType; - readonly value: string; - readonly scope: EnvironmentVariableScope | undefined; - } - - export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { - /** - * Sets a description for the environment variable collection, this will be used to describe the changes in the UI. - * @param description A description for the environment variable collection. - * @param scope Specific scope to which this description applies to. - */ - setDescription(description: string | MarkdownString | undefined, scope?: EnvironmentVariableScope): void; - replace(variable: string, value: string, scope?: EnvironmentVariableScope): void; - append(variable: string, value: string, scope?: EnvironmentVariableScope): void; - prepend(variable: string, value: string, scope?: EnvironmentVariableScope): void; - get(variable: string, scope?: EnvironmentVariableScope): EnvironmentVariableMutator | undefined; - delete(variable: string, scope?: EnvironmentVariableScope): void; - clear(scope?: EnvironmentVariableScope): void; - - } + // export interface ExtensionContext { + // /** + // * Gets the extension's environment variable collection for this workspace, enabling changes + // * to be applied to terminal environment variables. + // * + // * @param scope The scope to which the environment variable collection applies to. + // */ + // readonly environmentVariableCollection: EnvironmentVariableCollection & { getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection }; + // } export type EnvironmentVariableScope = { /** - * The workspace folder to which this collection applies to. If unspecified, collection applies to all workspace folders. - */ + * Any specific workspace folder to get collection for. If unspecified, collection applicable to all workspace folders is returned. + */ workspaceFolder?: WorkspaceFolder; }; + + export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { + /** + * A description for the environment variable collection, this will be used to describe the + * changes in the UI. + */ + description: string | MarkdownString | undefined; + } } From f148139f65e455fcc911cd5a3fdaba15b4bdb776 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 26 May 2023 11:43:46 -0700 Subject: [PATCH 0019/1136] allow pytest tests to handle multiple payloads (#21301) As part of the switch to allow for dynamic run- the pytest discovery and execution tests are now switched to be take lists of dicts where the dicts are the payloads. --- pythonFiles/tests/pytestadapter/helpers.py | 29 +++++++---- .../tests/pytestadapter/test_discovery.py | 44 +++++++++------- .../tests/pytestadapter/test_execution.py | 52 +++++++++++-------- 3 files changed, 73 insertions(+), 52 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index e84c46037c2f..013e4bb31fca 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -1,18 +1,15 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - -import contextlib import io import json import os import pathlib -import random import socket import subprocess import sys import threading import uuid -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Tuple TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" from typing_extensions import TypedDict @@ -70,31 +67,29 @@ def _new_sock() -> socket.socket: ) -def process_rpc_json(data: str) -> Dict[str, Any]: +def process_rpc_message(data: str) -> Tuple[Dict[str, Any], str]: """Process the JSON data which comes from the server which runs the pytest discovery.""" str_stream: io.StringIO = io.StringIO(data) length: int = 0 - while True: line: str = str_stream.readline() if CONTENT_LENGTH.lower() in line.lower(): length = int(line[len(CONTENT_LENGTH) :]) break - if not line or line.isspace(): raise ValueError("Header does not contain Content-Length") - while True: line: str = str_stream.readline() if not line or line.isspace(): break raw_json: str = str_stream.read(length) - return json.loads(raw_json) + dict_json: Dict[str, Any] = json.loads(raw_json) + return dict_json, str_stream.read() -def runner(args: List[str]) -> Optional[Dict[str, Any]]: +def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: """Run the pytest discovery and return the JSON data from the server.""" process_args: List[str] = [ sys.executable, @@ -133,7 +128,19 @@ def runner(args: List[str]) -> Optional[Dict[str, Any]]: t1.join() t2.join() - return process_rpc_json(result[0]) if result else None + a = process_rpc_json(result[0]) + return a if result else None + + +def process_rpc_json(data: str) -> List[Dict[str, Any]]: + """Process the JSON data which comes from the server which runs the pytest discovery.""" + json_messages = [] + remaining = data + while remaining: + json_data, remaining = process_rpc_message(remaining) + json_messages.append(json_data) + + return json_messages def _listen_on_socket(listener: socket.socket, result: List[str]): diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index bb6e7255704e..ab7abb508153 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import os import shutil +from typing import Any, Dict, List, Optional import pytest @@ -28,12 +29,15 @@ def test_syntax_error(tmp_path): temp_dir.mkdir() p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) - actual = runner(["--collect-only", os.fspath(p)]) - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + actual_list: Optional[List[Dict[str, Any]]] = runner( + ["--collect-only", os.fspath(p)] + ) + assert actual_list + for actual in actual_list: + assert all(item in actual for item in ("status", "cwd", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["error"]) == 2 def test_parameterized_error_collect(): @@ -42,12 +46,15 @@ def test_parameterized_error_collect(): The json should still be returned but the errors list should be present. """ file_path_str = "error_parametrize_discovery.py" - actual = runner(["--collect-only", file_path_str]) - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + actual_list: Optional[List[Dict[str, Any]]] = runner( + ["--collect-only", file_path_str] + ) + assert actual_list + for actual in actual_list: + assert all(item in actual for item in ("status", "cwd", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["error"]) == 2 @pytest.mark.parametrize( @@ -98,14 +105,15 @@ def test_pytest_collect(file, expected_const): file -- a string with the file or folder to run pytest discovery on. expected_const -- the expected output from running pytest discovery on the file. """ - actual = runner( + actual_list: Optional[List[Dict[str, Any]]] = runner( [ "--collect-only", os.fspath(TEST_DATA_PATH / file), ] ) - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert actual["tests"] == expected_const + assert actual_list + for actual in actual_list: + assert all(item in actual for item in ("status", "cwd", "tests")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert actual["tests"] == expected_const diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index 8613deb96098..d54e4e758d35 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import os import shutil +from typing import Any, Dict, List, Optional import pytest from tests.pytestadapter import expected_execution_test_output @@ -13,7 +14,7 @@ def test_syntax_error_execution(tmp_path): """Test pytest execution on a file that has a syntax error. Copies the contents of a .txt file to a .py file in the temporary directory - to then run pytest exeuction on. + to then run pytest execution on. The json should still be returned but the errors list should be present. @@ -28,12 +29,15 @@ def test_syntax_error_execution(tmp_path): temp_dir.mkdir() p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) - actual = runner(["error_syntax_discover.py::test_function"]) - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 1 + actual_list: Optional[List[Dict[str, Any]]] = runner( + ["error_syntax_discover.py::test_function"] + ) + assert actual_list + for actual in actual_list: + assert all(item in actual for item in ("status", "cwd", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["error"]) == 1 def test_bad_id_error_execution(): @@ -41,12 +45,13 @@ def test_bad_id_error_execution(): The json should still be returned but the errors list should be present. """ - actual = runner(["not/a/real::test_id"]) - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 1 + actual_list: Optional[List[Dict[str, Any]]] = runner(["not/a/real::test_id"]) + assert actual_list + for actual in actual_list: + assert all(item in actual for item in ("status", "cwd", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["error"]) == 1 @pytest.mark.parametrize( @@ -153,13 +158,14 @@ def test_pytest_execution(test_ids, expected_const): expected_const -- a dictionary of the expected output from running pytest discovery on the files. """ args = test_ids - actual = runner(args) - assert actual - assert all(item in actual for item in ("status", "cwd", "result")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - result_data = actual["result"] - for key in result_data: - if result_data[key]["outcome"] == "failure": - result_data[key]["message"] = "ERROR MESSAGE" - assert result_data == expected_const + actual_list: Optional[List[Dict[str, Any]]] = runner(args) + assert actual_list + for actual in actual_list: + assert all(item in actual for item in ("status", "cwd", "result")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + result_data = actual["result"] + for key in result_data: + if result_data[key]["outcome"] == "failure": + result_data[key]["message"] = "ERROR MESSAGE" + assert result_data == expected_const From e9a8dd52341a1a0f2beee7a43d0ccf44793e6a1f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 31 May 2023 09:08:07 -0700 Subject: [PATCH 0020/1136] remove duplicates from test_ids array (#21347) this will partially remediate https://github.com/microsoft/vscode-python/issues/21339 in regards to the duplicate IDs being run. --- src/client/testing/testController/workspaceTestAdapter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index ceb6a0b60afb..0edac06c2b5a 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -83,7 +83,7 @@ export class WorkspaceTestAdapter { let rawTestExecData; const testCaseNodes: TestItem[] = []; - const testCaseIds: string[] = []; + const testCaseIdsSet = new Set(); try { // first fetch all the individual test Items that we necessarily want includes.forEach((t) => { @@ -95,10 +95,10 @@ export class WorkspaceTestAdapter { runInstance.started(node); // do the vscode ui test item start here before runtest const runId = this.vsIdToRunId.get(node.id); if (runId) { - testCaseIds.push(runId); + testCaseIdsSet.add(runId); } }); - + const testCaseIds = Array.from(testCaseIdsSet); // ** execution factory only defined for new rewrite way if (executionFactory !== undefined) { rawTestExecData = await this.executionAdapter.runTests( From d968b8c472208f4635f75af649b58a9be027400d Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 31 May 2023 15:14:14 -0700 Subject: [PATCH 0021/1136] Dont show command for button trigger in command pallet (#21350) Fixes https://github.com/microsoft/vscode-python/issues/21322 --- package.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7918881bd115..b39392c8fa51 100644 --- a/package.json +++ b/package.json @@ -1600,6 +1600,12 @@ "title": "%python.command.python.createEnvironment.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, + { + "category": "Python", + "command": "python.createEnvironment-button", + "title": "%python.command.python.createEnvironment.title%", + "when": "false" + }, { "category": "Python", "command": "python.createTerminal", @@ -1629,14 +1635,14 @@ "command": "python.execInTerminal-icon", "icon": "$(play)", "title": "%python.command.python.execInTerminalIcon.title%", - "when": "false && editorLangId == python" + "when": "false" }, { "category": "Python", "command": "python.execInDedicatedTerminal", "icon": "$(play)", "title": "%python.command.python.execInDedicatedTerminal.title%", - "when": "false && editorLangId == python" + "when": "false" }, { "category": "Python", From dbd0b73b9605eb96d0da295a8ae95462f88e9915 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 31 May 2023 15:36:44 -0700 Subject: [PATCH 0022/1136] adding extra log messages for rewrite debugging (#21352) These logs print errors and other bits of information which will be helpful for debugging workflows of users where we need to get information such as args or which step in the process they got to. --- .../testing/testController/common/server.ts | 20 ++++++++++++----- .../testing/testController/controller.ts | 21 +++++++----------- .../pytest/pytestDiscoveryAdapter.ts | 13 ++++++----- .../pytest/pytestExecutionAdapter.ts | 22 ++++++++++--------- .../unittest/testDiscoveryAdapter.ts | 2 ++ .../testController/workspaceTestAdapter.ts | 3 ++- 6 files changed, 46 insertions(+), 35 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index a00623aa33c7..62c14d451fc2 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -9,7 +9,7 @@ import { IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; @@ -36,12 +36,12 @@ export class PythonTestServer implements ITestServer, Disposable { const uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); const totalContentLength = rpcHeaders.headers.get('Content-Length'); if (!uuid) { - traceLog('On data received: Error occurred because payload UUID is undefined'); + traceError('On data received: Error occurred because payload UUID is undefined'); this._onDataReceived.fire({ uuid: '', data: '' }); return; } if (!this.uuids.includes(uuid)) { - traceLog('On data received: Error occurred because the payload UUID is not recognized'); + traceError('On data received: Error occurred because the payload UUID is not recognized'); this._onDataReceived.fire({ uuid: '', data: '' }); return; } @@ -50,6 +50,7 @@ export class PythonTestServer implements ITestServer, Disposable { const extractedData = rpcContent.extractedJSON; if (extractedData.length === Number(totalContentLength)) { // do not send until we have the full content + traceVerbose(`Received data from test server: ${extractedData}`); this._onDataReceived.fire({ uuid, data: extractedData }); this.uuids = this.uuids.filter((u) => u !== uuid); buffer = Buffer.alloc(0); @@ -58,7 +59,7 @@ export class PythonTestServer implements ITestServer, Disposable { } } } catch (ex) { - traceLog(`Error processing test server request: ${ex} observe`); + traceError(`Error processing test server request: ${ex} observe`); this._onDataReceived.fire({ uuid: '', data: '' }); } }); @@ -114,6 +115,8 @@ export class PythonTestServer implements ITestServer, Disposable { outputChannel: options.outChannel, }; + const isRun = !options.testIds; + // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, @@ -154,9 +157,16 @@ export class PythonTestServer implements ITestServer, Disposable { token: options.token, testProvider: UNITTEST_PROVIDER, }; - + traceInfo(`Running DEBUG unittest with arguments: ${args}\r\n`); await this.debugLauncher!.launchDebugger(launchOptions); } else { + if (isRun) { + // This means it is running the test + traceInfo(`Running unittests with arguments: ${args}\r\n`); + } else { + // This means it is running discovery + traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); + } await execService.exec(args, spawnOptions); } } catch (ex) { diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 6c6b9f409e5f..0d3487855380 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -24,7 +24,7 @@ import { IConfigurationService, IDisposableRegistry, ITestOutputChannel, Resourc import { DelayedTrigger, IDelayedTrigger } from '../../common/utils/delayTrigger'; import { noop } from '../../common/utils/misc'; import { IInterpreterService } from '../../interpreter/contracts'; -import { traceError, traceVerbose } from '../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; import { IEventNamePropertyMapping, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../common/constants'; @@ -243,14 +243,14 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.refreshingStartedEvent.fire(); if (uri) { const settings = this.configSettings.getSettings(uri); - traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`); + const workspace = this.workspaceService.getWorkspaceFolder(uri); + traceInfo(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); + // Ensure we send test telemetry if it gets disabled again + this.sendTestDisabledTelemetry = true; + // ** experiment to roll out NEW test discovery mechanism if (settings.testing.pytestEnabled) { - // Ensure we send test telemetry if it gets disabled again - this.sendTestDisabledTelemetry = true; - // ** experiment to roll out NEW test discovery mechanism if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { - const workspace = this.workspaceService.getWorkspaceFolder(uri); - traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); + traceInfo(`Running discovery for pytest using the new test adapter.`); const testAdapter = this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); testAdapter.discoverTests( @@ -263,12 +263,8 @@ export class PythonTestController implements ITestController, IExtensionSingleAc await this.pytest.refreshTestData(this.testController, uri, this.refreshCancellation.token); } } else if (settings.testing.unittestEnabled) { - // ** Ensure we send test telemetry if it gets disabled again - this.sendTestDisabledTelemetry = true; - // ** experiment to roll out NEW test discovery mechanism if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { - const workspace = this.workspaceService.getWorkspaceFolder(uri); - traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); + traceInfo(`Running discovery for unittest using the new test adapter.`); const testAdapter = this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); testAdapter.discoverTests( @@ -288,7 +284,6 @@ export class PythonTestController implements ITestController, IExtensionSingleAc // If we are here we may have to remove an existing node from the tree // This handles the case where user removes test settings. Which should remove the // tests for that particular case from the tree view - const workspace = this.workspaceService.getWorkspaceFolder(uri); if (workspace) { const toDelete: string[] = []; this.testController.items.forEach((i: TestItem) => { diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 792826f4c3a5..aeb920407cd2 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -10,7 +10,7 @@ import { import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceVerbose } from '../../../logging'; +import { traceError, traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, DiscoveredTestPayload, ITestDiscoveryAdapter, ITestServer } from '../common/types'; /** @@ -80,11 +80,12 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { resource: uri, }; const execService = await executionFactory.createActivatedEnvironment(creationOptions); - execService - .exec(['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), spawnOptions) - .catch((ex) => { - deferred.reject(ex as Error); - }); + const discoveryArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + traceLog(`Discovering pytest tests with arguments: ${discoveryArgs.join(' ')}`); + execService.exec(discoveryArgs, spawnOptions).catch((ex) => { + traceError(`Error occurred while discovering tests: ${ex}`); + deferred.reject(ex as Error); + }); return deferred.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 39b7f8c787b9..413d18497613 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import * as net from 'net'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; -import { traceError, traceLog, traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, @@ -21,7 +21,7 @@ import { EXTENSION_ROOT_DIR } from '../../../common/constants'; // eslint-disable-next-line @typescript-eslint/no-explicit-any // (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; /** - * Wrapper Class for pytest test execution. This is where we call `runTestCommand`? + * Wrapper Class for pytest test execution.. */ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { @@ -52,7 +52,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, ): Promise { - traceVerbose(uri, testIds, debugBool); if (executionFactory !== undefined) { // ** new version of run tests. return this.runTestsNew(uri, testIds, debugBool, executionFactory, debugLauncher); @@ -120,41 +119,43 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const testData = JSON.stringify(testIds); const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; + traceLog(`Running pytest execution for the following test ids: ${testIds}`); let pytestRunTestIdsPort: string | undefined; const startServer = (): Promise => new Promise((resolve, reject) => { const server = net.createServer((socket: net.Socket) => { socket.on('end', () => { - traceLog('Client disconnected'); + traceVerbose('Client disconnected for pytest test ids server'); }); }); server.listen(0, () => { const { port } = server.address() as net.AddressInfo; - traceLog(`Server listening on port ${port}`); + traceVerbose(`Server listening on port ${port} for pytest test ids server`); resolve(port); }); server.on('error', (error: Error) => { + traceError('Error starting server for pytest test ids server:', error); reject(error); }); server.on('connection', (socket: net.Socket) => { socket.write(payload); - traceLog('payload sent', payload); + traceVerbose('payload sent for pytest execution', payload); }); }); // Start the server and wait until it is listening await startServer() .then((assignedPort) => { - traceLog(`Server started and listening on port ${assignedPort}`); + traceVerbose(`Server started for pytest test ids server and listening on port ${assignedPort}`); pytestRunTestIdsPort = assignedPort.toString(); if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort; }) .catch((error) => { - traceError('Error starting server:', error); + traceError('Error starting server for pytest test ids server:', error); }); if (debugBool) { @@ -169,12 +170,13 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { pytestUUID, pytestRunTestIdsPort, }; - traceVerbose(`Running debug test with arguments: ${testArgs.join(' ')}\r\n`); + traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions); } else { // combine path to run script with run args const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); const runArgs = [scriptPath, ...testArgs]; + traceInfo(`Running pytests with arguments: ${runArgs.join(' ')}\r\n`); await execService?.exec(runArgs, spawnOptions).catch((ex) => { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); @@ -182,7 +184,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }); } } catch (ex) { - traceVerbose(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); + traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); return Promise.reject(ex); } diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 3f8ecb5797d3..9c565af78c08 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -14,6 +14,7 @@ import { TestCommandOptions, TestDiscoveryCommand, } from '../common/types'; +import { traceInfo } from '../../../logging'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. @@ -61,6 +62,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // Send the test command to the server. // The server will fire an onDataReceived event once it gets a response. + traceInfo(`Sending discover unittest script to server.`); this.testServer.sendCommand(options); return deferred.promise; diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index 0edac06c2b5a..5cba6c193d3c 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -101,6 +101,7 @@ export class WorkspaceTestAdapter { const testCaseIds = Array.from(testCaseIdsSet); // ** execution factory only defined for new rewrite way if (executionFactory !== undefined) { + traceVerbose('executionFactory defined'); rawTestExecData = await this.executionAdapter.runTests( this.workspaceUri, testCaseIds, @@ -108,7 +109,6 @@ export class WorkspaceTestAdapter { executionFactory, debugLauncher, ); - traceVerbose('executionFactory defined'); } else { rawTestExecData = await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); } @@ -300,6 +300,7 @@ export class WorkspaceTestAdapter { try { // ** execution factory only defined for new rewrite way if (executionFactory !== undefined) { + traceVerbose('executionFactory defined'); rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); } else { rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri); From cd76ee19a25426119c168bac3996bcd6ced5a431 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 2 Jun 2023 08:44:33 -0700 Subject: [PATCH 0023/1136] add pythonTestAdapter to experiment enum (#21357) allow people to opt in and out of the pythonTestAdapter rewrite via the settings `python.experiment.optInto` or `python.experiment.optOutfrom` --- package-lock.json | 2 +- package.json | 12 ++++++++---- package.nls.json | 1 + 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6bdd1be4c1c..71ee97edee23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.79.0-20230525" + "vscode": "^1.79.0-20230526" } }, "node_modules/@azure/abort-controller": { diff --git a/package.json b/package.json index b39392c8fa51..3983015784dc 100644 --- a/package.json +++ b/package.json @@ -462,13 +462,15 @@ "All", "pythonSurveyNotification", "pythonPromptNewToolsExt", - "pythonTerminalEnvVarActivation" + "pythonTerminalEnvVarActivation", + "pythonTestAdapter" ], "enumDescriptions": [ "%python.experiments.All.description%", "%python.experiments.pythonSurveyNotification.description%", "%python.experiments.pythonPromptNewToolsExt.description%", - "%python.experiments.pythonTerminalEnvVarActivation.description%" + "%python.experiments.pythonTerminalEnvVarActivation.description%", + "%python.experiments.pythonTestAdapter.description%" ] }, "scope": "machine", @@ -483,13 +485,15 @@ "All", "pythonSurveyNotification", "pythonPromptNewToolsExt", - "pythonTerminalEnvVarActivation" + "pythonTerminalEnvVarActivation", + "pythonTestAdapter" ], "enumDescriptions": [ "%python.experiments.All.description%", "%python.experiments.pythonSurveyNotification.description%", "%python.experiments.pythonPromptNewToolsExt.description%", - "%python.experiments.pythonTerminalEnvVarActivation.description%" + "%python.experiments.pythonTerminalEnvVarActivation.description%", + "%python.experiments.pythonTestAdapter.description%" ] }, "scope": "machine", diff --git a/package.nls.json b/package.nls.json index 18254fd05468..71c4b5ee42ba 100644 --- a/package.nls.json +++ b/package.nls.json @@ -43,6 +43,7 @@ "python.experiments.pythonSurveyNotification.description": "Denotes the Python Survey Notification experiment.", "python.experiments.pythonPromptNewToolsExt.description": "Denotes the Python Prompt New Tools Extension experiment.", "python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.", + "python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.", "python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.", "python.formatting.autopep8Path.description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", "python.formatting.blackArgs.description": "Arguments passed in. Each argument is a separate item in the array.", From be829b308fd2334ca999ac41b4f39d3f7abeb2f1 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 5 Jun 2023 12:50:22 -0700 Subject: [PATCH 0024/1136] Unittest for large workspaces (#21351) follows the same steps as making pytest compatible with large workspaces with many tests. Now test_ids are sent over a port as a json instead of in the exec function which can hit a cap on # of characters. Should fix https://github.com/microsoft/vscode-python/issues/21339. --- .../testing_tools/process_json_util.py | 31 +++ .../tests/unittestadapter/test_execution.py | 12 +- pythonFiles/unittestadapter/execution.py | 71 +++++- .../vscode_pytest/run_pytest_script.py | 36 +-- src/client/testing/common/debugLauncher.ts | 9 +- src/client/testing/common/types.ts | 2 +- .../testing/testController/common/server.ts | 26 +- .../testing/testController/common/types.ts | 2 +- .../pytest/pytestExecutionAdapter.ts | 2 +- .../unittest/testExecutionAdapter.ts | 46 +++- .../testExecutionAdapter.unit.test.ts | 236 +++++++++--------- 11 files changed, 285 insertions(+), 188 deletions(-) create mode 100644 pythonFiles/testing_tools/process_json_util.py diff --git a/pythonFiles/testing_tools/process_json_util.py b/pythonFiles/testing_tools/process_json_util.py new file mode 100644 index 000000000000..f116b0d9a8f3 --- /dev/null +++ b/pythonFiles/testing_tools/process_json_util.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import io +import json +from typing import List + +CONTENT_LENGTH: str = "Content-Length:" + + +def process_rpc_json(data: str) -> List[str]: + """Process the JSON data which comes from the server.""" + str_stream: io.StringIO = io.StringIO(data) + + length: int = 0 + + while True: + line: str = str_stream.readline() + if CONTENT_LENGTH.lower() in line.lower(): + length = int(line[len(CONTENT_LENGTH) :]) + break + + if not line or line.isspace(): + raise ValueError("Header does not contain Content-Length") + + while True: + line: str = str_stream.readline() + if not line or line.isspace(): + break + + raw_json: str = str_stream.read(length) + return json.loads(raw_json) diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py index 7f58049a56b7..d461ead9ad94 100644 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -20,14 +20,12 @@ "111", "--uuid", "fake-uuid", - "--testids", - "test_file.test_class.test_method", ], - (111, "fake-uuid", ["test_file.test_class.test_method"]), + (111, "fake-uuid"), ), ( - ["--port", "111", "--uuid", "fake-uuid", "--testids", ""], - (111, "fake-uuid", [""]), + ["--port", "111", "--uuid", "fake-uuid"], + (111, "fake-uuid"), ), ( [ @@ -35,12 +33,10 @@ "111", "--uuid", "fake-uuid", - "--testids", - "test_file.test_class.test_method", "-v", "-s", ], - (111, "fake-uuid", ["test_file.test_class.test_method"]), + (111, "fake-uuid"), ), ], ) diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 37288651f531..4695064396cc 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -5,12 +5,19 @@ import enum import json import os +import pathlib +import socket import sys import traceback import unittest from types import TracebackType from typing import Dict, List, Optional, Tuple, Type, Union +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) +from testing_tools import process_json_util + # Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. PYTHON_FILES = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, PYTHON_FILES) @@ -25,7 +32,7 @@ def parse_execution_cli_args( args: List[str], -) -> Tuple[int, Union[str, None], List[str]]: +) -> Tuple[int, Union[str, None]]: """Parse command-line arguments that should be processed by the script. So far this includes the port number that it needs to connect to, the uuid passed by the TS side, @@ -39,10 +46,9 @@ def parse_execution_cli_args( arg_parser = argparse.ArgumentParser() arg_parser.add_argument("--port", default=DEFAULT_PORT) arg_parser.add_argument("--uuid") - arg_parser.add_argument("--testids", nargs="+") parsed_args, _ = arg_parser.parse_known_args(args) - return (int(parsed_args.port), parsed_args.uuid, parsed_args.testids) + return (int(parsed_args.port), parsed_args.uuid) ErrorType = Union[ @@ -226,11 +232,62 @@ def run_tests( start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) - # Perform test execution. - port, uuid, testids = parse_execution_cli_args(argv[:index]) - payload = run_tests(start_dir, testids, pattern, top_level_dir, uuid) + run_test_ids_port = os.environ.get("RUN_TEST_IDS_PORT") + run_test_ids_port_int = ( + int(run_test_ids_port) if run_test_ids_port is not None else 0 + ) + + # get data from socket + test_ids_from_buffer = [] + try: + client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client_socket.connect(("localhost", run_test_ids_port_int)) + print(f"CLIENT: Server listening on port {run_test_ids_port_int}...") + buffer = b"" + + while True: + # Receive the data from the client + data = client_socket.recv(1024 * 1024) + if not data: + break + + # Append the received data to the buffer + buffer += data + + try: + # Try to parse the buffer as JSON + test_ids_from_buffer = process_json_util.process_rpc_json( + buffer.decode("utf-8") + ) + # Clear the buffer as complete JSON object is received + buffer = b"" + + # Process the JSON data + print(f"Received JSON data: {test_ids_from_buffer}") + break + except json.JSONDecodeError: + # JSON decoding error, the complete JSON object is not yet received + continue + except socket.error as e: + print(f"Error: Could not connect to runTestIdsPort: {e}") + print("Error: Could not connect to runTestIdsPort") + + port, uuid = parse_execution_cli_args(argv[:index]) + if test_ids_from_buffer: + # Perform test execution. + payload = run_tests( + start_dir, test_ids_from_buffer, pattern, top_level_dir, uuid + ) + else: + cwd = os.path.abspath(start_dir) + status = TestExecutionStatus.error + payload: PayloadDict = { + "cwd": cwd, + "status": status, + "error": "No test ids received from buffer", + } - # Build the request data (it has to be a POST request or the Node side will not process it), and send it. + # Build the request data and send it. addr = ("localhost", port) data = json.dumps(payload) request = f"""Content-Length: {len(data)} diff --git a/pythonFiles/vscode_pytest/run_pytest_script.py b/pythonFiles/vscode_pytest/run_pytest_script.py index f6d6bdcafd5f..57bb41a3a7dd 100644 --- a/pythonFiles/vscode_pytest/run_pytest_script.py +++ b/pythonFiles/vscode_pytest/run_pytest_script.py @@ -1,41 +1,17 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -import io import json import os import pathlib import socket import sys -from typing import List import pytest -CONTENT_LENGTH: str = "Content-Length:" - - -def process_rpc_json(data: str) -> List[str]: - """Process the JSON data which comes from the server which runs the pytest discovery.""" - str_stream: io.StringIO = io.StringIO(data) - - length: int = 0 - - while True: - line: str = str_stream.readline() - if CONTENT_LENGTH.lower() in line.lower(): - length = int(line[len(CONTENT_LENGTH) :]) - break - - if not line or line.isspace(): - raise ValueError("Header does not contain Content-Length") - - while True: - line: str = str_stream.readline() - if not line or line.isspace(): - break - - raw_json: str = str_stream.read(length) - return json.loads(raw_json) - +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) +from testing_tools import process_json_util # This script handles running pytest via pytest.main(). It is called via run in the # pytest execution adapter and gets the test_ids to run via stdin and the rest of the @@ -69,7 +45,9 @@ def process_rpc_json(data: str) -> List[str]: try: # Try to parse the buffer as JSON - test_ids_from_buffer = process_rpc_json(buffer.decode("utf-8")) + test_ids_from_buffer = process_json_util.process_rpc_json( + buffer.decode("utf-8") + ) # Clear the buffer as complete JSON object is received buffer = b"" diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 0c13671b6e0d..b0f318905c78 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -202,13 +202,20 @@ export class DebugLauncher implements ITestDebugLauncher { throw Error(`Invalid debug config "${debugConfig.name}"`); } launchArgs.request = 'launch'; + + // Both types of tests need to have the port for the test result server. + if (options.runTestIdsPort) { + launchArgs.env = { + ...launchArgs.env, + RUN_TEST_IDS_PORT: options.runTestIdsPort, + }; + } if (options.testProvider === 'pytest' && pythonTestAdapterRewriteExperiment) { if (options.pytestPort && options.pytestUUID) { launchArgs.env = { ...launchArgs.env, TEST_PORT: options.pytestPort, TEST_UUID: options.pytestUUID, - RUN_TEST_IDS_PORT: options.pytestRunTestIdsPort, }; } else { throw Error( diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts index 443a2c2ef20e..7104260abfcf 100644 --- a/src/client/testing/common/types.ts +++ b/src/client/testing/common/types.ts @@ -27,7 +27,7 @@ export type LaunchOptions = { outChannel?: OutputChannel; pytestPort?: string; pytestUUID?: string; - pytestRunTestIdsPort?: string; + runTestIdsPort?: string; }; export type ParserOptions = TestDiscoveryOptions; diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 62c14d451fc2..7813e2a568b2 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -106,17 +106,18 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDataReceived.event; } - async sendCommand(options: TestCommandOptions): Promise { + async sendCommand(options: TestCommandOptions, runTestIdPort?: string): Promise { const { uuid } = options; const spawnOptions: SpawnOptions = { token: options.token, cwd: options.cwd, throwOnStdErr: true, outputChannel: options.outChannel, + extraVariables: {}, }; + if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; const isRun = !options.testIds; - // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, @@ -127,23 +128,9 @@ export class PythonTestServer implements ITestServer, Disposable { // Add the generated UUID to the data to be sent (expecting to receive it back). // first check if we have testIds passed in (in case of execution) and // insert appropriate flag and test id array - let args = []; - if (options.testIds) { - args = [ - options.command.script, - '--port', - this.getPort().toString(), - '--uuid', - uuid, - '--testids', - ...options.testIds, - ].concat(options.command.args); - } else { - // if not case of execution, go with the normal args - args = [options.command.script, '--port', this.getPort().toString(), '--uuid', uuid].concat( - options.command.args, - ); - } + const args = [options.command.script, '--port', this.getPort().toString(), '--uuid', uuid].concat( + options.command.args, + ); if (options.outChannel) { options.outChannel.appendLine(`python ${args.join(' ')}`); @@ -156,6 +143,7 @@ export class PythonTestServer implements ITestServer, Disposable { args, token: options.token, testProvider: UNITTEST_PROVIDER, + runTestIdsPort: runTestIdPort, }; traceInfo(`Running DEBUG unittest with arguments: ${args}\r\n`); await this.debugLauncher!.launchDebugger(launchOptions); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 4336fee3a4b6..e3dd37d0d984 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -172,7 +172,7 @@ export type TestCommandOptionsPytest = { */ export interface ITestServer { readonly onDataReceived: Event; - sendCommand(options: TestCommandOptions): Promise; + sendCommand(options: TestCommandOptions, runTestIdsPort?: string): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 413d18497613..a6a9963cbf75 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -168,7 +168,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testProvider: PYTEST_PROVIDER, pytestPort, pytestUUID, - pytestRunTestIdsPort, + runTestIdsPort: pytestRunTestIdsPort, }; traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions); diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index b39e0cd29560..abe386171a0e 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import { Uri } from 'vscode'; +import * as net from 'net'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; @@ -14,6 +15,7 @@ import { TestCommandOptions, TestExecutionCommand, } from '../common/types'; +import { traceLog, traceError } from '../../../logging'; /** * Wrapper Class for unittest test execution. This is where we call `runTestCommand`? @@ -61,9 +63,47 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const deferred = createDeferred(); this.promiseMap.set(uuid, deferred); - // Send test command to server. - // Server fire onDataReceived event once it gets response. - this.testServer.sendCommand(options); + // create payload with testIds to send to run pytest script + const testData = JSON.stringify(testIds); + const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; + const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; + + let runTestIdsPort: string | undefined; + const startServer = (): Promise => + new Promise((resolve, reject) => { + const server = net.createServer((socket: net.Socket) => { + socket.on('end', () => { + traceLog('Client disconnected'); + }); + }); + + server.listen(0, () => { + const { port } = server.address() as net.AddressInfo; + traceLog(`Server listening on port ${port}`); + resolve(port); + }); + + server.on('error', (error: Error) => { + reject(error); + }); + server.on('connection', (socket: net.Socket) => { + socket.write(payload); + traceLog('payload sent', payload); + }); + }); + + // Start the server and wait until it is listening + await startServer() + .then((assignedPort) => { + traceLog(`Server started and listening on port ${assignedPort}`); + runTestIdsPort = assignedPort.toString(); + // Send test command to server. + // Server fire onDataReceived event once it gets response. + this.testServer.sendCommand(options, runTestIdsPort); // does this need an await? + }) + .catch((error) => { + traceError('Error starting server:', error); + }); return deferred.promise; } diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index d88f033d39a4..e5495629bf28 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -1,118 +1,118 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as assert from 'assert'; -import * as path from 'path'; -import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; -import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; -import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; -import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; - -suite('Unittest test execution adapter', () => { - let stubConfigSettings: IConfigurationService; - let outputChannel: typemoq.IMock; - - setup(() => { - stubConfigSettings = ({ - getSettings: () => ({ - testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, - }), - } as unknown) as IConfigurationService; - outputChannel = typemoq.Mock.ofType(); - }); - - test('runTests should send the run command to the test server', async () => { - let options: TestCommandOptions | undefined; - - const stubTestServer = ({ - sendCommand(opt: TestCommandOptions): Promise { - delete opt.outChannel; - options = opt; - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => '123456789', - } as unknown) as ITestServer; - - const uri = Uri.file('/foo/bar'); - const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); - - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - adapter.runTests(uri, [], false); - - const expectedOptions: TestCommandOptions = { - workspaceFolder: uri, - command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, - cwd: uri.fsPath, - uuid: '123456789', - debugBool: false, - testIds: [], - }; - - assert.deepStrictEqual(options, expectedOptions); - }); - test("onDataReceivedHandler should parse the data if the cwd from the payload matches the test adapter's cwd", async () => { - const stubTestServer = ({ - sendCommand(): Promise { - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => '123456789', - } as unknown) as ITestServer; - - const uri = Uri.file('/foo/bar'); - const data = { status: 'success' }; - const uuid = '123456789'; - - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - - // triggers runTests flow which will run onDataReceivedHandler and the - // promise resolves into the parsed data. - const promise = adapter.runTests(uri, [], false); - - adapter.onDataReceivedHandler({ uuid, data: JSON.stringify(data) }); - - const result = await promise; - - assert.deepStrictEqual(result, data); - }); - test("onDataReceivedHandler should ignore the data if the cwd from the payload does not match the test adapter's cwd", async () => { - const correctUuid = '123456789'; - const incorrectUuid = '987654321'; - const stubTestServer = ({ - sendCommand(): Promise { - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => correctUuid, - } as unknown) as ITestServer; - - const uri = Uri.file('/foo/bar'); - - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - - // triggers runTests flow which will run onDataReceivedHandler and the - // promise resolves into the parsed data. - const promise = adapter.runTests(uri, [], false); - - const data = { status: 'success' }; - // will not resolve due to incorrect UUID - adapter.onDataReceivedHandler({ uuid: incorrectUuid, data: JSON.stringify(data) }); - - const nextData = { status: 'error' }; - // will resolve and nextData will be returned as result - adapter.onDataReceivedHandler({ uuid: correctUuid, data: JSON.stringify(nextData) }); - - const result = await promise; - - assert.deepStrictEqual(result, nextData); - }); -}); +// // Copyright (c) Microsoft Corporation. All rights reserved. +// // Licensed under the MIT License. + +// import * as assert from 'assert'; +// import * as path from 'path'; +// import * as typemoq from 'typemoq'; +// import { Uri } from 'vscode'; +// import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +// import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +// import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; +// import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; + +// suite('Unittest test execution adapter', () => { +// let stubConfigSettings: IConfigurationService; +// let outputChannel: typemoq.IMock; + +// setup(() => { +// stubConfigSettings = ({ +// getSettings: () => ({ +// testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, +// }), +// } as unknown) as IConfigurationService; +// outputChannel = typemoq.Mock.ofType(); +// }); + +// test('runTests should send the run command to the test server', async () => { +// let options: TestCommandOptions | undefined; + +// const stubTestServer = ({ +// sendCommand(opt: TestCommandOptions, runTestIdPort?: string): Promise { +// delete opt.outChannel; +// options = opt; +// assert(runTestIdPort !== undefined); +// return Promise.resolve(); +// }, +// onDataReceived: () => { +// // no body +// }, +// createUUID: () => '123456789', +// } as unknown) as ITestServer; + +// const uri = Uri.file('/foo/bar'); +// const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); + +// const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); +// adapter.runTests(uri, [], false).then(() => { +// const expectedOptions: TestCommandOptions = { +// workspaceFolder: uri, +// command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, +// cwd: uri.fsPath, +// uuid: '123456789', +// debugBool: false, +// testIds: [], +// }; +// assert.deepStrictEqual(options, expectedOptions); +// }); +// }); +// test("onDataReceivedHandler should parse the data if the cwd from the payload matches the test adapter's cwd", async () => { +// const stubTestServer = ({ +// sendCommand(): Promise { +// return Promise.resolve(); +// }, +// onDataReceived: () => { +// // no body +// }, +// createUUID: () => '123456789', +// } as unknown) as ITestServer; + +// const uri = Uri.file('/foo/bar'); +// const data = { status: 'success' }; +// const uuid = '123456789'; + +// const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); + +// // triggers runTests flow which will run onDataReceivedHandler and the +// // promise resolves into the parsed data. +// const promise = adapter.runTests(uri, [], false); + +// adapter.onDataReceivedHandler({ uuid, data: JSON.stringify(data) }); + +// const result = await promise; + +// assert.deepStrictEqual(result, data); +// }); +// test("onDataReceivedHandler should ignore the data if the cwd from the payload does not match the test adapter's cwd", async () => { +// const correctUuid = '123456789'; +// const incorrectUuid = '987654321'; +// const stubTestServer = ({ +// sendCommand(): Promise { +// return Promise.resolve(); +// }, +// onDataReceived: () => { +// // no body +// }, +// createUUID: () => correctUuid, +// } as unknown) as ITestServer; + +// const uri = Uri.file('/foo/bar'); + +// const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); + +// // triggers runTests flow which will run onDataReceivedHandler and the +// // promise resolves into the parsed data. +// const promise = adapter.runTests(uri, [], false); + +// const data = { status: 'success' }; +// // will not resolve due to incorrect UUID +// adapter.onDataReceivedHandler({ uuid: incorrectUuid, data: JSON.stringify(data) }); + +// const nextData = { status: 'error' }; +// // will resolve and nextData will be returned as result +// adapter.onDataReceivedHandler({ uuid: correctUuid, data: JSON.stringify(nextData) }); + +// const result = await promise; + +// assert.deepStrictEqual(result, nextData); +// }); +// }); From a395e2ec6cdc8b4b5f5577d0cc175fde451ec590 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 5 Jun 2023 15:23:17 -0700 Subject: [PATCH 0025/1136] fix bug so canceling debug works in rewrite (#21361) fixes https://github.com/microsoft/vscode-python/issues/21336 --- src/client/testing/common/debugLauncher.ts | 3 ++- src/client/testing/common/types.ts | 2 +- src/client/testing/testController/common/server.ts | 7 +++++-- src/client/testing/testController/common/types.ts | 2 +- .../testController/pytest/pytestExecutionAdapter.ts | 9 ++++----- .../testController/unittest/testExecutionAdapter.ts | 4 +++- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index b0f318905c78..f7a027ad1304 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -31,7 +31,7 @@ export class DebugLauncher implements ITestDebugLauncher { this.configService = this.serviceContainer.get(IConfigurationService); } - public async launchDebugger(options: LaunchOptions): Promise { + public async launchDebugger(options: LaunchOptions, callback?: () => void): Promise { if (options.token && options.token.isCancellationRequested) { return undefined; } @@ -47,6 +47,7 @@ export class DebugLauncher implements ITestDebugLauncher { const deferred = createDeferred(); debugManager.onDidTerminateDebugSession(() => { deferred.resolve(); + callback?.(); }); debugManager.startDebugging(workspaceFolder, launchArgs); return deferred.promise; diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts index 7104260abfcf..29a6de7768cb 100644 --- a/src/client/testing/common/types.ts +++ b/src/client/testing/common/types.ts @@ -88,7 +88,7 @@ export interface ITestConfigurationManagerFactory { } export const ITestDebugLauncher = Symbol('ITestDebugLauncher'); export interface ITestDebugLauncher { - launchDebugger(options: LaunchOptions): Promise; + launchDebugger(options: LaunchOptions, callback?: () => void): Promise; } export const IUnitTestSocketServer = Symbol('IUnitTestSocketServer'); diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 7813e2a568b2..6bd9bf348e20 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -106,7 +106,7 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDataReceived.event; } - async sendCommand(options: TestCommandOptions, runTestIdPort?: string): Promise { + async sendCommand(options: TestCommandOptions, runTestIdPort?: string, callback?: () => void): Promise { const { uuid } = options; const spawnOptions: SpawnOptions = { token: options.token, @@ -146,7 +146,10 @@ export class PythonTestServer implements ITestServer, Disposable { runTestIdsPort: runTestIdPort, }; traceInfo(`Running DEBUG unittest with arguments: ${args}\r\n`); - await this.debugLauncher!.launchDebugger(launchOptions); + + await this.debugLauncher!.launchDebugger(launchOptions, () => { + callback?.(); + }); } else { if (isRun) { // This means it is running the test diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index e3dd37d0d984..4307d7a3913f 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -172,7 +172,7 @@ export type TestCommandOptionsPytest = { */ export interface ITestServer { readonly onDataReceived: Event; - sendCommand(options: TestCommandOptions, runTestIdsPort?: string): Promise; + sendCommand(options: TestCommandOptions, runTestIdsPort?: string, callback?: () => void): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index a6a9963cbf75..90704b5d67f4 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -171,17 +171,16 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { runTestIdsPort: pytestRunTestIdsPort, }; traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); - await debugLauncher!.launchDebugger(launchOptions); + await debugLauncher!.launchDebugger(launchOptions, () => { + deferred.resolve(); + }); } else { // combine path to run script with run args const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); const runArgs = [scriptPath, ...testArgs]; traceInfo(`Running pytests with arguments: ${runArgs.join(' ')}\r\n`); - await execService?.exec(runArgs, spawnOptions).catch((ex) => { - traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); - return Promise.reject(ex); - }); + await execService?.exec(runArgs, spawnOptions); } } catch (ex) { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index abe386171a0e..bf83c3c0feb1 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -99,7 +99,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runTestIdsPort = assignedPort.toString(); // Send test command to server. // Server fire onDataReceived event once it gets response. - this.testServer.sendCommand(options, runTestIdsPort); // does this need an await? + this.testServer.sendCommand(options, runTestIdsPort, () => { + deferred.resolve(); + }); }) .catch((error) => { traceError('Error starting server:', error); From ad9c899bea2864ba99d60997413c050f225a2d87 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 6 Jun 2023 13:11:54 -0700 Subject: [PATCH 0026/1136] Update version for release candidate (#21369) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71ee97edee23..22f40819786d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.9.0-dev", + "version": "2023.10.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.9.0-dev", + "version": "2023.10.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 3983015784dc..4f5e5ce5c225 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2023.9.0-dev", + "version": "2023.10.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From 479a577fab41b181638ac7c3c6941541c0b288bd Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 6 Jun 2023 13:50:27 -0700 Subject: [PATCH 0027/1136] update version for next pre-release (#21376) Co-authored-by: Anthony Kim --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 22f40819786d..49376d6e6cab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.10.0-rc", + "version": "2023.11.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.10.0-rc", + "version": "2023.11.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 4f5e5ce5c225..d76ba16ee16d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2023.10.0-rc", + "version": "2023.11.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From aef6e37ab7eb96b6a63d3222305f15e15db1c879 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 16:27:52 -0700 Subject: [PATCH 0028/1136] Bump typing-extensions from 4.5.0 to 4.6.3 (#21358) Bumps [typing-extensions](https://github.com/python/typing_extensions) from 4.5.0 to 4.6.3.
Changelog

Sourced from typing-extensions's changelog.

Release 4.6.3 (June 1, 2023)

  • Fix a regression introduced in v4.6.0 in the implementation of runtime-checkable protocols. The regression meant that doing class Foo(X, typing_extensions.Protocol), where X was a class that had abc.ABCMeta as its metaclass, would then cause subsequent isinstance(1, X) calls to erroneously raise TypeError. Patch by Alex Waygood (backporting the CPython PR python/cpython#105152).
  • Sync the repository's LICENSE file with that of CPython. typing_extensions is distributed under the same license as CPython itself.
  • Skip a problematic test on Python 3.12.0b1. The test fails on 3.12.0b1 due to a bug in CPython, which will be fixed in 3.12.0b2. The typing_extensions test suite now passes on 3.12.0b1.

Release 4.6.2 (May 25, 2023)

  • Fix use of @deprecated on classes with __new__ but no __init__. Patch by Jelle Zijlstra.
  • Fix regression in version 4.6.1 where comparing a generic class against a runtime-checkable protocol using isinstance() would cause AttributeError to be raised if using Python 3.7.

Release 4.6.1 (May 23, 2023)

  • Change deprecated @runtime to formal API @runtime_checkable in the error message. Patch by Xuehai Pan.
  • Fix regression in 4.6.0 where attempting to define a Protocol that was generic over a ParamSpec or a TypeVarTuple would cause TypeError to be raised. Patch by Alex Waygood.

Release 4.6.0 (May 22, 2023)

  • typing_extensions is now documented at https://typing-extensions.readthedocs.io/en/latest/. Patch by Jelle Zijlstra.

  • Add typing_extensions.Buffer, a marker class for buffer types, as proposed by PEP 688. Equivalent to collections.abc.Buffer in Python 3.12. Patch by Jelle Zijlstra.

  • Backport two CPython PRs fixing various issues with typing.Literal: python/cpython#23294 and python/cpython#23383. Both CPython PRs were originally by Yurii Karabas, and both were backported to Python >=3.9.1, but no earlier. Patch by Alex Waygood.

    A side effect of one of the changes is that equality comparisons of Literal objects will now raise a TypeError if one of the Literal objects being compared has a mutable parameter. (Using mutable parameters with Literal is not supported by PEP 586 or by any major static type checkers.)

  • Literal is now reimplemented on all Python versions <= 3.10.0. The

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typing-extensions&package-manager=pip&previous-version=4.5.0&new-version=4.6.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.in | 2 +- requirements.txt | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/requirements.in b/requirements.in index bdd5e10dfc47..37e15028bedd 100644 --- a/requirements.in +++ b/requirements.in @@ -4,7 +4,7 @@ # 2) pip-compile --generate-hashes requirements.in # Unittest test adapter -typing-extensions==4.5.0 +typing-extensions==4.6.3 # Fallback env creator for debian microvenv diff --git a/requirements.txt b/requirements.txt index 1419e0528ccc..2436fe08810f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,12 +20,10 @@ tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f # via -r requirements.in -typing-extensions==4.5.0 \ - --hash=sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb \ - --hash=sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4 - # via - # -r requirements.in - # importlib-metadata +typing-extensions==4.6.3 \ + --hash=sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26 \ + --hash=sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5 + # via -r requirements.in zipp==3.15.0 \ --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 From 85af76cd01e6877a117c97093073d10ccdb04282 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 8 Jun 2023 09:05:08 -0700 Subject: [PATCH 0029/1136] Add function node for parameterized tests pytest (#21368) fixes https://github.com/microsoft/vscode-python/issues/21340 --- .../expected_discovery_test_output.py | 78 ++++++++------ pythonFiles/vscode_pytest/__init__.py | 101 ++++++++++++++---- 2 files changed, 122 insertions(+), 57 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 8e96d109ba78..8b2283029ac7 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -1,5 +1,4 @@ import os -import pathlib from .helpers import TEST_DATA_PATH, find_test_line_number @@ -389,9 +388,10 @@ # This is the expected output for the nested_folder tests. # └── parametrize_tests.py -# └── test_adding[3+5-8] -# └── test_adding[2+4-6] -# └── test_adding[6+9-16] +# └── test_adding +# └── [3+5-8] +# └── [2+4-6] +# └── [6+9-16] parameterize_tests_path = os.fspath(TEST_DATA_PATH / "parametrize_tests.py") parametrize_tests_expected_output = { "name": ".data", @@ -405,40 +405,48 @@ "id_": parameterize_tests_path, "children": [ { - "name": "test_adding[3+5-8]", + "name": "test_adding", "path": parameterize_tests_path, - "lineno": find_test_line_number( - "test_adding[3+5-8]", - parameterize_tests_path, - ), - "type_": "test", - "id_": "parametrize_tests.py::test_adding[3+5-8]", - "runID": "parametrize_tests.py::test_adding[3+5-8]", - }, - { - "name": "test_adding[2+4-6]", - "path": parameterize_tests_path, - "lineno": find_test_line_number( - "test_adding[2+4-6]", - parameterize_tests_path, - ), - "type_": "test", - "id_": "parametrize_tests.py::test_adding[2+4-6]", - "runID": "parametrize_tests.py::test_adding[2+4-6]", - }, - { - "name": "test_adding[6+9-16]", - "path": parameterize_tests_path, - "lineno": find_test_line_number( - "test_adding[6+9-16]", - parameterize_tests_path, - ), - "type_": "test", - "id_": "parametrize_tests.py::test_adding[6+9-16]", - "runID": "parametrize_tests.py::test_adding[6+9-16]", + "type_": "function", + "id_": "parametrize_tests.py::test_adding", + "children": [ + { + "name": "[3+5-8]", + "path": parameterize_tests_path, + "lineno": find_test_line_number( + "test_adding[3+5-8]", + parameterize_tests_path, + ), + "type_": "test", + "id_": "parametrize_tests.py::test_adding[3+5-8]", + "runID": "parametrize_tests.py::test_adding[3+5-8]", + }, + { + "name": "[2+4-6]", + "path": parameterize_tests_path, + "lineno": find_test_line_number( + "test_adding[2+4-6]", + parameterize_tests_path, + ), + "type_": "test", + "id_": "parametrize_tests.py::test_adding[2+4-6]", + "runID": "parametrize_tests.py::test_adding[2+4-6]", + }, + { + "name": "[6+9-16]", + "path": parameterize_tests_path, + "lineno": find_test_line_number( + "test_adding[6+9-16]", + parameterize_tests_path, + ), + "type_": "test", + "id_": "parametrize_tests.py::test_adding[6+9-16]", + "runID": "parametrize_tests.py::test_adding[6+9-16]", + }, + ], }, ], - } + }, ], "id_": TEST_DATA_PATH_STR, } diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 0f0bcbd1d323..4f539e4ea01d 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -20,8 +20,8 @@ class TestData(TypedDict): """A general class that all test objects inherit from.""" name: str - path: str - type_: Literal["class", "file", "folder", "test", "error"] + path: pathlib.Path + type_: Literal["class", "function", "file", "folder", "test", "error"] id_: str @@ -196,12 +196,10 @@ def pytest_sessionfinish(session, exitstatus): ) post_response(os.fsdecode(cwd), session_node) except Exception as e: - ERRORS.append( - f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" - ) + f"Error Occurred, description: {e.args[0] if e.args and e.args[0] else ''} traceback: {(traceback.format_exc() if e.__traceback__ else '')}" errorNode: TestNode = { "name": "", - "path": "", + "path": cwd, "type_": "error", "children": [], "id_": "", @@ -232,6 +230,7 @@ def build_test_tree(session: pytest.Session) -> TestNode: session_children_dict: Dict[str, TestNode] = {} file_nodes_dict: Dict[Any, TestNode] = {} class_nodes_dict: Dict[str, TestNode] = {} + function_nodes_dict: Dict[str, TestNode] = {} for test_case in session.items: test_node = create_test_node(test_case) @@ -256,6 +255,35 @@ def build_test_tree(session: pytest.Session) -> TestNode: # Check if the class is already a child of the file node. if test_class_node not in test_file_node["children"]: test_file_node["children"].append(test_class_node) + elif hasattr(test_case, "callspec"): # This means it is a parameterized test. + function_name: str = "" + # parameterized test cases cut the repetitive part of the name off. + name_split = test_node["name"].split("[")[1] + test_node["name"] = "[" + name_split + try: + function_name = test_case.originalname # type: ignore + function_test_case = function_nodes_dict[function_name] + except AttributeError: # actual error has occurred + ERRORS.append( + f"unable to find original name for {test_case.name} with parameterization detected." + ) + raise VSCodePytestError( + "Unable to find original name for parameterized test case" + ) + except KeyError: + function_test_case: TestNode = create_parameterized_function_node( + function_name, test_case.path, test_case.nodeid + ) + function_nodes_dict[function_name] = function_test_case + function_test_case["children"].append(test_node) + # Now, add the function node to file node. + try: + parent_test_case = file_nodes_dict[test_case.parent] + except KeyError: + parent_test_case = create_file_node(test_case.parent) + file_nodes_dict[test_case.parent] = parent_test_case + if function_test_case not in parent_test_case["children"]: + parent_test_case["children"].append(function_test_case) else: # This includes test cases that are pytest functions or a doctests. try: parent_test_case = file_nodes_dict[test_case.parent] @@ -264,10 +292,10 @@ def build_test_tree(session: pytest.Session) -> TestNode: file_nodes_dict[test_case.parent] = parent_test_case parent_test_case["children"].append(test_node) created_files_folders_dict: Dict[str, TestNode] = {} - for file_module, file_node in file_nodes_dict.items(): + for _, file_node in file_nodes_dict.items(): # Iterate through all the files that exist and construct them into nested folders. root_folder_node: TestNode = build_nested_folders( - file_module, file_node, created_files_folders_dict, session + file_node, created_files_folders_dict, session ) # The final folder we get to is the highest folder in the path # and therefore we add this as a child to the session. @@ -279,7 +307,6 @@ def build_test_tree(session: pytest.Session) -> TestNode: def build_nested_folders( - file_module: Any, file_node: TestNode, created_files_folders_dict: Dict[str, TestNode], session: pytest.Session, @@ -295,7 +322,7 @@ def build_nested_folders( prev_folder_node = file_node # Begin the iterator_path one level above the current file. - iterator_path = file_module.path.parent + iterator_path = file_node["path"].parent while iterator_path != session.path: curr_folder_name = iterator_path.name try: @@ -325,7 +352,7 @@ def create_test_node( ) return { "name": test_case.name, - "path": os.fspath(test_case.path), + "path": test_case.path, "lineno": test_case_loc, "type_": "test", "id_": test_case.nodeid, @@ -341,7 +368,7 @@ def create_session_node(session: pytest.Session) -> TestNode: """ return { "name": session.name, - "path": os.fspath(session.path), + "path": session.path, "type_": "folder", "children": [], "id_": os.fspath(session.path), @@ -356,13 +383,34 @@ def create_class_node(class_module: pytest.Class) -> TestNode: """ return { "name": class_module.name, - "path": os.fspath(class_module.path), + "path": class_module.path, "type_": "class", "children": [], "id_": class_module.nodeid, } +def create_parameterized_function_node( + function_name: str, test_path: pathlib.Path, test_id: str +) -> TestNode: + """Creates a function node to be the parent for the parameterized test nodes. + + Keyword arguments: + function_name -- the name of the function. + test_path -- the path to the test file. + test_id -- the id of the test, which is a parameterized test so it + must be edited to get a unique id for the function node. + """ + function_id: str = test_id.split("::")[0] + "::" + function_name + return { + "name": function_name, + "path": test_path, + "type_": "function", + "children": [], + "id_": function_id, + } + + def create_file_node(file_module: Any) -> TestNode: """Creates a file node from a pytest file module. @@ -371,14 +419,14 @@ def create_file_node(file_module: Any) -> TestNode: """ return { "name": file_module.path.name, - "path": os.fspath(file_module.path), + "path": file_module.path, "type_": "file", "id_": os.fspath(file_module.path), "children": [], } -def create_folder_node(folderName: str, path_iterator: pathlib.Path) -> TestNode: +def create_folder_node(folder_name: str, path_iterator: pathlib.Path) -> TestNode: """Creates a folder node from a pytest folder name and its path. Keyword arguments: @@ -386,8 +434,8 @@ def create_folder_node(folderName: str, path_iterator: pathlib.Path) -> TestNode path_iterator -- the path of the folder. """ return { - "name": folderName, - "path": os.fspath(path_iterator), + "name": folder_name, + "path": path_iterator, "type_": "folder", "id_": os.fspath(path_iterator), "children": [], @@ -451,6 +499,15 @@ def execution_post( print(f"[vscode-pytest] data: {request}") +class PathEncoder(json.JSONEncoder): + """A custom JSON encoder that encodes pathlib.Path objects as strings.""" + + def default(self, obj): + if isinstance(obj, pathlib.Path): + return os.fspath(obj) + return super().default(obj) + + def post_response(cwd: str, session_node: TestNode) -> None: """Sends a post request to the server. @@ -467,13 +524,13 @@ def post_response(cwd: str, session_node: TestNode) -> None: } if ERRORS is not None: payload["error"] = ERRORS - testPort: Union[str, int] = os.getenv("TEST_PORT", 45454) - testuuid: Union[str, None] = os.getenv("TEST_UUID") - addr = "localhost", int(testPort) - data = json.dumps(payload) + test_port: Union[str, int] = os.getenv("TEST_PORT", 45454) + test_uuid: Union[str, None] = os.getenv("TEST_UUID") + addr = "localhost", int(test_port) + data = json.dumps(payload, cls=PathEncoder) request = f"""Content-Length: {len(data)} Content-Type: application/json -Request-uuid: {testuuid} +Request-uuid: {test_uuid} {data}""" try: From 84e67af053c6db4a04ec6e801b17ee5a7d99167e Mon Sep 17 00:00:00 2001 From: "Soojin (Min) Choi" Date: Thu, 8 Jun 2023 11:01:10 -0700 Subject: [PATCH 0030/1136] Remove jupyter depenency from readme (#21375) Edited readme to accurately represent exclusion of Jupyter ext installation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d27ec8e762f5..8a5df6720717 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ The Python extension does offer [some support](https://github.com/microsoft/vsco ## Installed extensions -The Python extension will automatically install the [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) and [Jupyter](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) extensions to give you the best experience when working with Python files and Jupyter notebooks. However, Pylance is an optional dependency, meaning the Python extension will remain fully functional if it fails to be installed. You can also [uninstall](https://code.visualstudio.com/docs/editor/extension-marketplace#_uninstall-an-extension) it at the expense of some features if you’re using a different language server. +The Python extension will automatically install the [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) extension to give you the best experience when working with Python files. However, Pylance is an optional dependency, meaning the Python extension will remain fully functional if it fails to be installed. You can also [uninstall](https://code.visualstudio.com/docs/editor/extension-marketplace#_uninstall-an-extension) it at the expense of some features if you’re using a different language server. Extensions installed through the marketplace are subject to the [Marketplace Terms of Use](https://cdn.vsassets.io/v/M146_20190123.39/_content/Microsoft-Visual-Studio-Marketplace-Terms-of-Use.pdf). From 3e5af447e52f96791aad04181c17bf6207819f0d Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Thu, 8 Jun 2023 13:59:09 -0700 Subject: [PATCH 0031/1136] Dont sent telemetry in forks (#21377) Closed #20941 - Add enableTelemetry in package.json - Add function that updates the 'enableTelemetry' value in package.json - Update release pipelines --- build/azure-pipeline.pre-release.yml | 4 ++ build/azure-pipeline.stable.yml | 4 ++ build/update_package_file.py | 21 ++++++++++ package.json | 1 + src/client/telemetry/index.ts | 16 ++++---- .../extension/adapter/factory.unit.test.ts | 4 ++ src/test/telemetry/index.unit.test.ts | 39 +++++++------------ 7 files changed, 58 insertions(+), 31 deletions(-) create mode 100644 build/update_package_file.py diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index 5bc61b2fd559..7b13978b8db2 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -61,6 +61,10 @@ extends: python ./build/update_ext_version.py --for-publishing displayName: Update build number + - script: | + python ./build/update_package_file.py + displayName: Update telemetry in package.json + - script: npm run addExtensionPackDependencies displayName: Update optional extension dependencies diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 159d856b6c3e..50ccbb9fff7a 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -63,6 +63,10 @@ extends: - script: | python ./build/update_ext_version.py --release --for-publishing displayName: Update build number + + - script: | + python ./build/update_package_file.py + displayName: Update telemetry in package.json - script: npm run addExtensionPackDependencies displayName: Update optional extension dependencies diff --git a/build/update_package_file.py b/build/update_package_file.py new file mode 100644 index 000000000000..b61460b4cc21 --- /dev/null +++ b/build/update_package_file.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import pathlib +import sys + +EXT_ROOT = pathlib.Path(__file__).parent.parent +PACKAGE_JSON_PATH = EXT_ROOT / "package.json" + +def main(package_json: pathlib.Path) -> None: + package = json.loads(package_json.read_text(encoding="utf-8")) + package['enableTelemetry'] = True + + # Overwrite package.json with new data add a new-line at the end of the file. + package_json.write_text( + json.dumps(package, indent=4, ensure_ascii=False) + "\n", encoding="utf-8" + ) + +if __name__ == "__main__": + main(PACKAGE_JSON_PATH, sys.argv[1:]) diff --git a/package.json b/package.json index d76ba16ee16d..8d9c14724438 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "engines": { "vscode": "^1.79.0-20230526" }, + "enableTelemetry": false, "keywords": [ "python", "django", diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index d0b9d463c070..20df3a21eb37 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -4,9 +4,10 @@ import TelemetryReporter from '@vscode/extension-telemetry'; +import * as path from 'path'; +import * as fs from 'fs-extra'; import { DiagnosticCodes } from '../application/diagnostics/constants'; -import { IWorkspaceService } from '../common/application/types'; -import { AppinsightsKey, isTestExecution, isUnitTestExecution } from '../common/constants'; +import { AppinsightsKey, EXTENSION_ROOT_DIR, isTestExecution, isUnitTestExecution } from '../common/constants'; import type { TerminalShellType } from '../common/terminal/types'; import { StopWatch } from '../common/utils/stopWatch'; import { isPromise } from '../common/utils/async'; @@ -41,12 +42,13 @@ function isTelemetrySupported(): boolean { } /** - * Checks if the telemetry is disabled in user settings + * Checks if the telemetry is disabled * @returns {boolean} */ -export function isTelemetryDisabled(workspaceService: IWorkspaceService): boolean { - const settings = workspaceService.getConfiguration('telemetry').inspect('enableTelemetry')!; - return settings.globalValue === false; +export function isTelemetryDisabled(): boolean { + const packageJsonPath = path.join(EXTENSION_ROOT_DIR, 'package.json'); + const packageJson = fs.readJSONSync(packageJsonPath); + return !packageJson.enableTelemetry; } const sharedProperties: Record = {}; @@ -101,7 +103,7 @@ export function sendTelemetryEvent

{ let stateFactory: IPersistentStateFactory; let state: PersistentState; let showErrorMessageStub: sinon.SinonStub; + let readJSONSyncStub: sinon.SinonStub; let commandManager: ICommandManager; const nodeExecutable = undefined; @@ -66,6 +68,8 @@ suite('Debugging - Adapter Factory', () => { setup(() => { process.env.VSC_PYTHON_UNIT_TEST = undefined; process.env.VSC_PYTHON_CI_TEST = undefined; + readJSONSyncStub = sinon.stub(fs, 'readJSONSync'); + readJSONSyncStub.returns({ enableTelemetry: true }); rewiremock.enable(); rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); stateFactory = mock(PersistentStateFactory); diff --git a/src/test/telemetry/index.unit.test.ts b/src/test/telemetry/index.unit.test.ts index 00272aad1f64..d369dce27855 100644 --- a/src/test/telemetry/index.unit.test.ts +++ b/src/test/telemetry/index.unit.test.ts @@ -4,12 +4,9 @@ import { expect } from 'chai'; import rewiremock from 'rewiremock'; -import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as fs from 'fs-extra'; -import { instance, mock, verify, when } from 'ts-mockito'; -import { WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; import { _resetSharedProperties, clearTelemetryReporter, @@ -19,9 +16,9 @@ import { } from '../../client/telemetry'; suite('Telemetry', () => { - let workspaceService: IWorkspaceService; const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + let readJSONSyncStub: sinon.SinonStub; class Reporter { public static eventName: string[] = []; @@ -48,9 +45,10 @@ suite('Telemetry', () => { } setup(() => { - workspaceService = mock(WorkspaceService); process.env.VSC_PYTHON_UNIT_TEST = undefined; process.env.VSC_PYTHON_CI_TEST = undefined; + readJSONSyncStub = sinon.stub(fs, 'readJSONSync'); + readJSONSyncStub.returns({ enableTelemetry: true }); clearTelemetryReporter(); Reporter.clear(); }); @@ -59,35 +57,28 @@ suite('Telemetry', () => { process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; rewiremock.disable(); _resetSharedProperties(); + sinon.restore(); }); const testsForisTelemetryDisabled = [ { - testName: 'Returns true when globalValue is set to false', - settings: { globalValue: false }, - expectedResult: true, + testName: 'Returns true', + settings: { enableTelemetry: true }, + expectedResult: false, }, { - testName: 'Returns false otherwise', - settings: {}, - expectedResult: false, + testName: 'Returns false ', + settings: { enableTelemetry: false }, + expectedResult: true, }, ]; suite('Function isTelemetryDisabled()', () => { testsForisTelemetryDisabled.forEach((testParams) => { test(testParams.testName, async () => { - const workspaceConfig = TypeMoq.Mock.ofType(); - when(workspaceService.getConfiguration('telemetry')).thenReturn(workspaceConfig.object); - workspaceConfig - .setup((c) => c.inspect('enableTelemetry')) - .returns(() => testParams.settings as any) - .verifiable(TypeMoq.Times.once()); - - expect(isTelemetryDisabled(instance(workspaceService))).to.equal(testParams.expectedResult); - - verify(workspaceService.getConfiguration('telemetry')).once(); - workspaceConfig.verifyAll(); + readJSONSyncStub.returns(testParams.settings); + expect(isTelemetryDisabled()).to.equal(testParams.expectedResult); + sinon.assert.calledOnce(readJSONSyncStub); }); }); }); From d5500a5b8964a52449bb7de9c79b7d4d8767019f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 9 Jun 2023 13:27:24 -0700 Subject: [PATCH 0032/1136] fix pytest run script to add cwd to path (#21399) fixes https://github.com/microsoft/vscode-python/issues/21397. The issue came from the setup of running a script that then runs pytest which is different then pytest discovery which runs pytest as a module. The run script didn't work since the script did not insert the cwd into the path like what is done when running a module. This change inserts the cwd. --- pythonFiles/vscode_pytest/run_pytest_script.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pythonFiles/vscode_pytest/run_pytest_script.py b/pythonFiles/vscode_pytest/run_pytest_script.py index 57bb41a3a7dd..ffb4d0c55b16 100644 --- a/pythonFiles/vscode_pytest/run_pytest_script.py +++ b/pythonFiles/vscode_pytest/run_pytest_script.py @@ -21,6 +21,7 @@ # Add the root directory to the path so that we can import the plugin. directory_path = pathlib.Path(__file__).parent.parent sys.path.append(os.fspath(directory_path)) + sys.path.insert(0, os.getcwd()) # Get the rest of the args to run with pytest. args = sys.argv[1:] run_test_ids_port = os.environ.get("RUN_TEST_IDS_PORT") From a9d5cff5d4075a3e21b98652931d3f112cd3c9bc Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 12 Jun 2023 11:30:29 -0700 Subject: [PATCH 0033/1136] Fix for showing button in merge result editor (#21408) Closes https://github.com/microsoft/vscode-python/issues/21398 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8d9c14724438..1401e0d1d645 100644 --- a/package.json +++ b/package.json @@ -1743,12 +1743,12 @@ { "group": "Python", "command": "python.createEnvironment-button", - "when": "showCreateEnvButton && resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && pythonDepsNotInstalled" + "when": "showCreateEnvButton && resourceLangId == pip-requirements && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && !isMergeResultEditor && pythonDepsNotInstalled" }, { "group": "Python", "command": "python.createEnvironment-button", - "when": "showCreateEnvButton && resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && pythonDepsNotInstalled" + "when": "showCreateEnvButton && resourceFilename == pyproject.toml && pipInstallableToml && !virtualWorkspace && shellExecutionSupported && !inDiffEditor && !isMergeResultEditor && pythonDepsNotInstalled" } ], "editor/context": [ From 06f7db293f369405f0b1307402ea979fe8dee092 Mon Sep 17 00:00:00 2001 From: Courtney Webster <60238438+cwebster-99@users.noreply.github.com> Date: Tue, 13 Jun 2023 17:04:55 -0500 Subject: [PATCH 0034/1136] Python Walkthrough Updates (#21411) Updated walkthrough to be put behind an experiment: - Open Folder tile will allow the walkthrough to persist if there are remaining steps to be completed in the walkthrough and the user is opening a folder from an empty workspace - Added additional environment explanation on the environment media - Updated wording on the "Explore more resources" tile --------- Co-authored-by: Luciana Abud <45497113+luabud@users.noreply.github.com> --- package.json | 88 +++++++++++++++++++++- package.nls.json | 7 +- resources/walkthrough/environments-info.md | 10 +++ 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 resources/walkthrough/environments-info.md diff --git a/package.json b/package.json index 1401e0d1d645..8b62067e8347 100644 --- a/package.json +++ b/package.json @@ -173,6 +173,92 @@ } ] }, + { + "id": "pythonWelcome2", + "title": "%walkthrough.pythonWelcome.title%", + "description": "%walkthrough.pythonWelcome.description%", + "when": "false", + "steps": [ + { + "id": "python.createPythonFolder", + "title": "%walkthrough.step.python.createPythonFolder.title%", + "description": "%walkthrough.step.python.createPythonFolder.description%", + "media": { + "svg": "resources/walkthrough/open-folder.svg", + "altText": "%walkthrough.step.python.createPythonFile.altText%" + }, + "when": "workspaceFolderCount = 0" + }, + { + "id": "python.createPythonFile", + "title": "%walkthrough.step.python.createPythonFile.title%", + "description": "%walkthrough.step.python.createPythonFile.description%", + "media": { + "svg": "resources/walkthrough/open-folder.svg", + "altText": "%walkthrough.step.python.createPythonFile.altText%" + }, + "when": "" + }, + { + "id": "python.installPythonWin8", + "title": "%walkthrough.step.python.installPythonWin8.title%", + "description": "%walkthrough.step.python.installPythonWin8.description%", + "media": { + "markdown": "resources/walkthrough/install-python-windows-8.md" + }, + "when": "workspacePlatform == windows && showInstallPythonTile" + }, + { + "id": "python.installPythonMac", + "title": "%walkthrough.step.python.installPythonMac.title%", + "description": "%walkthrough.step.python.installPythonMac.description%", + "media": { + "markdown": "resources/walkthrough/install-python-macos.md" + }, + "when": "workspacePlatform == mac && showInstallPythonTile", + "command": "workbench.action.terminal.new" + }, + { + "id": "python.installPythonLinux", + "title": "%walkthrough.step.python.installPythonLinux.title%", + "description": "%walkthrough.step.python.installPythonLinux.description%", + "media": { + "markdown": "resources/walkthrough/install-python-linux.md" + }, + "when": "workspacePlatform == linux && showInstallPythonTile", + "command": "workbench.action.terminal.new" + }, + { + "id": "python.createEnvironment2", + "title": "%walkthrough.step.python.createEnvironment.title2%", + "description": "%walkthrough.step.python.createEnvironment.description2%", + "media": { + "markdown": "resources/walkthrough/environments-info.md" + }, + "when": "" + }, + { + "id": "python.runAndDebug", + "title": "%walkthrough.step.python.runAndDebug.title%", + "description": "%walkthrough.step.python.runAndDebug.description%", + "media": { + "svg": "resources/walkthrough/rundebug2.svg", + "altText": "%walkthrough.step.python.runAndDebug.altText%" + }, + "when": "" + }, + { + "id": "python.learnMoreWithDS2", + "title": "%walkthrough.step.python.learnMoreWithDS.title%", + "description": "%walkthrough.step.python.learnMoreWithDS.description2%", + "media": { + "altText": "%walkthrough.step.python.learnMoreWithDS.altText%", + "svg": "resources/walkthrough/learnmore.svg" + }, + "when": "" + } + ] + }, { "id": "pythonDataScienceWelcome", "title": "%walkthrough.pythonDataScienceWelcome.title%", @@ -2030,4 +2116,4 @@ "webpack-require-from": "^1.8.6", "yargs": "^15.3.1" } -} +} \ No newline at end of file diff --git a/package.nls.json b/package.nls.json index 71c4b5ee42ba..6c148792ee52 100644 --- a/package.nls.json +++ b/package.nls.json @@ -130,7 +130,9 @@ "walkthrough.pythonWelcome.title": "Get Started with Python Development", "walkthrough.pythonWelcome.description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!", "walkthrough.step.python.createPythonFile.title": "Create a Python file", + "walkthrough.step.python.createPythonFolder.title": "Open a Python project folder", "walkthrough.step.python.createPythonFile.description": "[Open](command:toSide:workbench.action.files.openFile) or [create](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D) a Python file - make sure to save it as \".py\".\n[Create Python File](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D)", + "walkthrough.step.python.createPythonFolder.description": "[Open](command:workbench.action.files.openFolder) or create a project folder.\n[Open Project Folder](command:workbench.action.files.openFolder)", "walkthrough.step.python.installPythonWin8.title": "Install Python", "walkthrough.step.python.installPythonWin8.description": "The Python Extension requires Python to be installed. Install Python [from python.org](https://www.python.org/downloads).\n\n[Install Python](https://www.python.org/downloads)\n", "walkthrough.step.python.installPythonMac.title": "Install Python", @@ -140,11 +142,14 @@ "walkthrough.step.python.selectInterpreter.title": "Select a Python Interpreter", "walkthrough.step.python.selectInterpreter.description": "Choose which Python interpreter/environment you want to use for your Python project.\n[Select Python Interpreter](command:python.setInterpreter)\n**Tip**: Run the ``Python: Select Interpreter`` command in the [Command Palette](command:workbench.action.showCommands).", "walkthrough.step.python.createEnvironment.title": "Create a Python Environment ", + "walkthrough.step.python.createEnvironment.title2": "Create or select a Python Environment ", "walkthrough.step.python.createEnvironment.description": "Create an environment for your Python project.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).\n 🔍 Check out our [docs](https://aka.ms/pythonenvs) to learn more.", + "walkthrough.step.python.createEnvironment.description2": "Create an environment for your Python project or use [Select Python Interpreter](command:python.setInterpreter) to select an existing one.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).", "walkthrough.step.python.runAndDebug.title": "Run and debug your Python file", "walkthrough.step.python.runAndDebug.description": "Open your Python file and click on the play button on the top right of the editor, or press F5 when on the file and select \"Python File\" to run with the debugger. \n \n[Learn more](https://code.visualstudio.com/docs/python/python-tutorial#_run-hello-world)", "walkthrough.step.python.learnMoreWithDS.title": "Explore more resources", "walkthrough.step.python.learnMoreWithDS.description": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Learn More](https://aka.ms/AA8dqti)", + "walkthrough.step.python.learnMoreWithDS.description2": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Follow along with the Python Tutorial](https://aka.ms/AA8dqti)", "walkthrough.pythonDataScienceWelcome.title": "Get Started with Python for Data Science", "walkthrough.pythonDataScienceWelcome.description": "Your first steps to getting started with a Data Science project with Python!", "walkthrough.step.python.installJupyterExt.title": "Install Jupyter extension", @@ -164,4 +169,4 @@ "walkthrough.step.python.createNewNotebook.altText": "Creating a new Jupyter notebook", "walkthrough.step.python.openInteractiveWindow.altText": "Opening Python interactive window", "walkthrough.step.python.dataScienceLearnMore.altText": "Image representing our documentation page and mailing list resources." -} +} \ No newline at end of file diff --git a/resources/walkthrough/environments-info.md b/resources/walkthrough/environments-info.md new file mode 100644 index 000000000000..7bdc61a96e2e --- /dev/null +++ b/resources/walkthrough/environments-info.md @@ -0,0 +1,10 @@ +## Python Environments + +Create Environment Dropdown + +Python virtual environments are considered a best practice in Python development. A virtual environment includes a Python interpreter and any packages you have installed into it, such as numpy or Flask. + +After you create a virtual environment using the **Python: Create Environment** command, you can install packages into the environment. +For example, type `python -m pip install numpy` in an activated terminal to install `numpy` into the environment. + +🔍 Check out our [docs](https://aka.ms/pythonenvs) to learn more. From cb859ca0900a03a08a5d73ad9d8e01c0f5e9c52f Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 13 Jun 2023 15:09:55 -0700 Subject: [PATCH 0035/1136] Revert: Run in dedicated terminal feature due to regressions (#21418) For https://github.com/microsoft/vscode-python/issues/21393 --- package.json | 19 ------- package.nls.json | 1 - src/client/common/constants.ts | 1 - src/client/common/terminal/factory.ts | 21 ++----- src/client/common/terminal/types.ts | 2 +- src/client/telemetry/index.ts | 6 -- .../codeExecution/codeExecutionManager.ts | 56 +++++++------------ .../codeExecution/terminalCodeExecution.ts | 34 +++++------ src/client/terminals/types.ts | 2 +- .../common/terminals/factory.unit.test.ts | 47 +--------------- .../codeExecutionManager.unit.test.ts | 25 +++------ 11 files changed, 55 insertions(+), 159 deletions(-) diff --git a/package.json b/package.json index 8b62067e8347..ce407ed9ee4d 100644 --- a/package.json +++ b/package.json @@ -392,12 +392,6 @@ "icon": "$(play)", "title": "%python.command.python.execInTerminalIcon.title%" }, - { - "category": "Python", - "command": "python.execInDedicatedTerminal", - "icon": "$(play)", - "title": "%python.command.python.execInDedicatedTerminal.title%" - }, { "category": "Python", "command": "python.debugInTerminal", @@ -1728,13 +1722,6 @@ "title": "%python.command.python.execInTerminalIcon.title%", "when": "false" }, - { - "category": "Python", - "command": "python.execInDedicatedTerminal", - "icon": "$(play)", - "title": "%python.command.python.execInDedicatedTerminal.title%", - "when": "false" - }, { "category": "Python", "command": "python.debugInTerminal", @@ -1893,12 +1880,6 @@ "title": "%python.command.python.execInTerminalIcon.title%", "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" }, - { - "command": "python.execInDedicatedTerminal", - "group": "navigation@0", - "title": "%python.command.python.execInDedicatedTerminal.title%", - "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" - }, { "command": "python.debugInTerminal", "group": "navigation@1", diff --git a/package.nls.json b/package.nls.json index 6c148792ee52..e228e6a19552 100644 --- a/package.nls.json +++ b/package.nls.json @@ -7,7 +7,6 @@ "python.command.python.execInTerminal.title": "Run Python File in Terminal", "python.command.python.debugInTerminal.title": "Debug Python File", "python.command.python.execInTerminalIcon.title": "Run Python File", - "python.command.python.execInDedicatedTerminal.title": "Run Python File in Dedicated Terminal", "python.command.python.setInterpreter.title": "Select Interpreter", "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", "python.command.python.viewOutput.title": "Show Output", diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index bea0ef9e235c..b285667aaa6a 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -44,7 +44,6 @@ export namespace Commands { export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; export const Exec_In_Terminal = 'python.execInTerminal'; export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; - export const Exec_In_Separate_Terminal = 'python.execInDedicatedTerminal'; export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; export const GetSelectedInterpreterPath = 'python.interpreterPath'; diff --git a/src/client/common/terminal/factory.ts b/src/client/common/terminal/factory.ts index 39cc88c4b024..3855cb6cee3c 100644 --- a/src/client/common/terminal/factory.ts +++ b/src/client/common/terminal/factory.ts @@ -3,7 +3,6 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; -import * as path from 'path'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; @@ -24,17 +23,13 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { ) { this.terminalServices = new Map(); } - public getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService { + public getTerminalService(options: TerminalCreationOptions): ITerminalService { const resource = options?.resource; const title = options?.title; - let terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; + const terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; const interpreter = options?.interpreter; - const id = this.getTerminalId(terminalTitle, resource, interpreter, options.newTerminalPerFile); + const id = this.getTerminalId(terminalTitle, resource, interpreter); if (!this.terminalServices.has(id)) { - if (resource && options.newTerminalPerFile) { - terminalTitle = `${terminalTitle}: ${path.basename(resource.fsPath).replace('.py', '')}`; - } - options.title = terminalTitle; const terminalService = new TerminalService(this.serviceContainer, options); this.terminalServices.set(id, terminalService); } @@ -51,19 +46,13 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { title = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; return new TerminalService(this.serviceContainer, { resource, title }); } - private getTerminalId( - title: string, - resource?: Uri, - interpreter?: PythonEnvironment, - newTerminalPerFile?: boolean, - ): string { + private getTerminalId(title: string, resource?: Uri, interpreter?: PythonEnvironment): string { if (!resource && !interpreter) { return title; } const workspaceFolder = this.serviceContainer .get(IWorkspaceService) .getWorkspaceFolder(resource || undefined); - const fileId = resource && newTerminalPerFile ? resource.fsPath : ''; - return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}:${fileId}`; + return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}`; } } diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts index 303188682378..880bf0dd72fb 100644 --- a/src/client/common/terminal/types.ts +++ b/src/client/common/terminal/types.ts @@ -97,7 +97,7 @@ export interface ITerminalServiceFactory { * @returns {ITerminalService} * @memberof ITerminalServiceFactory */ - getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService; + getTerminalService(options: TerminalCreationOptions): ITerminalService; createTerminalService(resource?: Uri, title?: string): ITerminalService; } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 20df3a21eb37..7881ada5653c 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -827,12 +827,6 @@ export interface IEventNamePropertyMapping { * @type {('command' | 'icon')} */ trigger?: 'command' | 'icon'; - /** - * Whether user chose to execute this Python file in a separate terminal or not. - * - * @type {boolean} - */ - newTerminalPerFile?: boolean; }; /** * Telemetry Event sent when user executes code against Django Shell. diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index 9f1ba6e90d90..ed671f2846a2 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -36,31 +36,25 @@ export class CodeExecutionManager implements ICodeExecutionManager { } public registerCommands() { - [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon, Commands.Exec_In_Separate_Terminal].forEach( - (cmd) => { - this.disposableRegistry.push( - this.commandManager.registerCommand(cmd as any, async (file: Resource) => { - const interpreterService = this.serviceContainer.get(IInterpreterService); - const interpreter = await interpreterService.getActiveInterpreter(file); - if (!interpreter) { - this.commandManager - .executeCommand(Commands.TriggerEnvironmentSelection, file) - .then(noop, noop); - return; - } - const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; - await this.executeFileInTerminal(file, trigger, { - newTerminalPerFile: cmd === Commands.Exec_In_Separate_Terminal, + [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon].forEach((cmd) => { + this.disposableRegistry.push( + this.commandManager.registerCommand(cmd as any, async (file: Resource) => { + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); + return; + } + const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; + await this.executeFileInTerminal(file, trigger) + .then(() => { + if (this.shouldTerminalFocusOnStart(file)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); }) - .then(() => { - if (this.shouldTerminalFocusOnStart(file)) - this.commandManager.executeCommand('workbench.action.terminal.focus'); - }) - .catch((ex) => traceError('Failed to execute file in terminal', ex)); - }), - ); - }, - ); + .catch((ex) => traceError('Failed to execute file in terminal', ex)); + }), + ); + }); this.disposableRegistry.push( this.commandManager.registerCommand(Commands.Exec_Selection_In_Terminal as any, async (file: Resource) => { const interpreterService = this.serviceContainer.get(IInterpreterService); @@ -93,16 +87,8 @@ export class CodeExecutionManager implements ICodeExecutionManager { ), ); } - private async executeFileInTerminal( - file: Resource, - trigger: 'command' | 'icon', - options?: { newTerminalPerFile: boolean }, - ): Promise { - sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { - scope: 'file', - trigger, - newTerminalPerFile: options?.newTerminalPerFile, - }); + private async executeFileInTerminal(file: Resource, trigger: 'command' | 'icon') { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'file', trigger }); const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); file = file instanceof Uri ? file : undefined; let fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); @@ -124,7 +110,7 @@ export class CodeExecutionManager implements ICodeExecutionManager { } const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); - await executionService.executeFile(fileToExecute, options); + await executionService.executeFile(fileToExecute); } @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index ca7488530f75..9261483b45e1 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -19,6 +19,7 @@ import { ICodeExecutionService } from '../../terminals/types'; export class TerminalCodeExecutionProvider implements ICodeExecutionService { private hasRanOutsideCurrentDrive = false; protected terminalTitle!: string; + private _terminalService!: ITerminalService; private replActive?: Promise; constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, @@ -29,13 +30,13 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { @inject(IInterpreterService) protected readonly interpreterService: IInterpreterService, ) {} - public async executeFile(file: Uri, options?: { newTerminalPerFile: boolean }) { + public async executeFile(file: Uri) { await this.setCwdForFileExecution(file); const { command, args } = await this.getExecuteFileArgs(file, [ file.fsPath.fileToCommandArgumentForPythonExt(), ]); - await this.getTerminalService(file, options).sendCommand(command, args); + await this.getTerminalService(file).sendCommand(command, args); } public async execute(code: string, resource?: Uri): Promise { @@ -47,23 +48,17 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { await this.getTerminalService(resource).sendText(code); } public async initializeRepl(resource?: Uri) { - const terminalService = this.getTerminalService(resource); if (this.replActive && (await this.replActive)) { - await terminalService.show(); + await this._terminalService.show(); return; } this.replActive = new Promise(async (resolve) => { const replCommandArgs = await this.getExecutableInfo(resource); - terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args); + await this.getTerminalService(resource).sendCommand(replCommandArgs.command, replCommandArgs.args); // Give python repl time to start before we start sending text. setTimeout(() => resolve(true), 1000); }); - this.disposables.push( - terminalService.onDidCloseTerminal(() => { - this.replActive = undefined; - }), - ); await this.replActive; } @@ -81,12 +76,19 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise { return this.getExecutableInfo(resource, executeArgs); } - private getTerminalService(resource?: Uri, options?: { newTerminalPerFile: boolean }): ITerminalService { - return this.terminalServiceFactory.getTerminalService({ - resource, - title: this.terminalTitle, - newTerminalPerFile: options?.newTerminalPerFile, - }); + private getTerminalService(resource?: Uri): ITerminalService { + if (!this._terminalService) { + this._terminalService = this.terminalServiceFactory.getTerminalService({ + resource, + title: this.terminalTitle, + }); + this.disposables.push( + this._terminalService.onDidCloseTerminal(() => { + this.replActive = undefined; + }), + ); + } + return this._terminalService; } private async setCwdForFileExecution(file: Uri) { const pythonSettings = this.configurationService.getSettings(file); diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index 47ac16d9e08b..cf31f4ef1dd0 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -8,7 +8,7 @@ export const ICodeExecutionService = Symbol('ICodeExecutionService'); export interface ICodeExecutionService { execute(code: string, resource?: Uri): Promise; - executeFile(file: Uri, options?: { newTerminalPerFile: boolean }): Promise; + executeFile(file: Uri): Promise; initializeRepl(resource?: Uri): Promise; } diff --git a/src/test/common/terminals/factory.unit.test.ts b/src/test/common/terminals/factory.unit.test.ts index 5ad2da8e793a..ef6b7d8f5b0f 100644 --- a/src/test/common/terminals/factory.unit.test.ts +++ b/src/test/common/terminals/factory.unit.test.ts @@ -105,7 +105,7 @@ suite('Terminal Service Factory', () => { expect(notSameAsThirdInstance).to.not.equal(true, 'Instances are the same'); }); - test('Ensure same terminal is returned when using different resources from the same workspace', () => { + test('Ensure same terminal is returned when using resources from the same workspace', () => { const file1A = Uri.file('1a'); const file2A = Uri.file('2a'); const fileB = Uri.file('b'); @@ -140,49 +140,4 @@ suite('Terminal Service Factory', () => { 'Instances should be different for different workspaces', ); }); - - test('When `newTerminalPerFile` is true, ensure different terminal is returned when using different resources from the same workspace', () => { - const file1A = Uri.file('1a'); - const file2A = Uri.file('2a'); - const fileB = Uri.file('b'); - const workspaceUriA = Uri.file('A'); - const workspaceUriB = Uri.file('B'); - const workspaceFolderA = TypeMoq.Mock.ofType(); - workspaceFolderA.setup((w) => w.uri).returns(() => workspaceUriA); - const workspaceFolderB = TypeMoq.Mock.ofType(); - workspaceFolderB.setup((w) => w.uri).returns(() => workspaceUriB); - - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file1A))) - .returns(() => workspaceFolderA.object); - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file2A))) - .returns(() => workspaceFolderA.object); - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(fileB))) - .returns(() => workspaceFolderB.object); - - const terminalForFile1A = factory.getTerminalService({ - resource: file1A, - newTerminalPerFile: true, - }) as SynchronousTerminalService; - const terminalForFile2A = factory.getTerminalService({ - resource: file2A, - newTerminalPerFile: true, - }) as SynchronousTerminalService; - const terminalForFileB = factory.getTerminalService({ - resource: fileB, - newTerminalPerFile: true, - }) as SynchronousTerminalService; - - const terminalsAreSameForWorkspaceA = terminalForFile1A.terminalService === terminalForFile2A.terminalService; - expect(terminalsAreSameForWorkspaceA).to.equal(false, 'Instances are the same for Workspace A'); - - const terminalsForWorkspaceABAreDifferent = - terminalForFile1A.terminalService === terminalForFileB.terminalService; - expect(terminalsForWorkspaceABAreDifferent).to.equal( - false, - 'Instances should be different for different workspaces', - ); - }); }); diff --git a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts index 30f95c94d217..3676834873a0 100644 --- a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +++ b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -77,15 +77,12 @@ suite('Terminal - Code Execution Manager', () => { executionManager.registerCommands(); const sorted = registered.sort(); - expect(sorted).to.deep.equal( - [ - Commands.Exec_In_Separate_Terminal, - Commands.Exec_In_Terminal, - Commands.Exec_In_Terminal_Icon, - Commands.Exec_Selection_In_Django_Shell, - Commands.Exec_Selection_In_Terminal, - ].sort(), - ); + expect(sorted).to.deep.equal([ + Commands.Exec_In_Terminal, + Commands.Exec_In_Terminal_Icon, + Commands.Exec_Selection_In_Django_Shell, + Commands.Exec_Selection_In_Terminal, + ]); }); test('Ensure executeFileInterTerminal will do nothing if no file is avialble', async () => { @@ -138,10 +135,7 @@ suite('Terminal - Code Execution Manager', () => { const fileToExecute = Uri.file('x'); await commandHandler!(fileToExecute); helper.verify(async (h) => h.getFileToExecute(), TypeMoq.Times.never()); - executionService.verify( - async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), - TypeMoq.Times.once(), - ); + executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); }); test('Ensure executeFileInterTerminal will use active file', async () => { @@ -170,10 +164,7 @@ suite('Terminal - Code Execution Manager', () => { .returns(() => executionService.object); await commandHandler!(fileToExecute); - executionService.verify( - async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), - TypeMoq.Times.once(), - ); + executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); }); async function testExecutionOfSelectionWithoutAnyActiveDocument(commandId: string, executionSericeId: string) { From 625b8609bf29d8c20e6e9ebaa63e9163208aa587 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 13 Jun 2023 15:28:30 -0700 Subject: [PATCH 0036/1136] Fix build failure (#21425) Closes https://github.com/microsoft/vscode-python/issues/21424 --- build/update_package_file.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build/update_package_file.py b/build/update_package_file.py index b61460b4cc21..f82587ced846 100644 --- a/build/update_package_file.py +++ b/build/update_package_file.py @@ -3,19 +3,20 @@ import json import pathlib -import sys EXT_ROOT = pathlib.Path(__file__).parent.parent PACKAGE_JSON_PATH = EXT_ROOT / "package.json" + def main(package_json: pathlib.Path) -> None: package = json.loads(package_json.read_text(encoding="utf-8")) - package['enableTelemetry'] = True + package["enableTelemetry"] = True # Overwrite package.json with new data add a new-line at the end of the file. package_json.write_text( json.dumps(package, indent=4, ensure_ascii=False) + "\n", encoding="utf-8" ) + if __name__ == "__main__": - main(PACKAGE_JSON_PATH, sys.argv[1:]) + main(PACKAGE_JSON_PATH) From f5acb54d8d4b6f612cf44754073d831d960c1a93 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 14 Jun 2023 01:54:46 -0700 Subject: [PATCH 0037/1136] Add flag to publish releases on manual pre-release builds (#21429) --- build/azure-pipeline.pre-release.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index 7b13978b8db2..f2e4c2461a6d 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -18,9 +18,16 @@ resources: ref: main endpoint: Monaco +parameters: + - name: publishExtension + displayName: 🚀 Publish Extension + type: boolean + default: false + extends: template: azure-pipelines/extension/pre-release.yml@templates parameters: + publishExtension: ${{ parameters.publishExtension }} l10nSourcePaths: ./src/client buildSteps: - task: NodeTool@0 From e1c50ae8ff0bf35b8492122e312268513e9189ce Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Wed, 14 Jun 2023 11:16:24 -0700 Subject: [PATCH 0038/1136] 21249 read launch config in workspace file (#21426) Closed: https://github.com/microsoft/vscode-python/issues/21249 --- .../launch.json/launchJsonReader.ts | 13 ++- .../launch.json/launchJsonReader.unit.test.ts | 86 +++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts diff --git a/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts b/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts index 789dda510e37..1d76e3b8cd26 100644 --- a/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts +++ b/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts @@ -5,13 +5,19 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import { parse } from 'jsonc-parser'; import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; -import { getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; +import { getConfiguration, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; +import { traceLog } from '../../../../logging'; export async function getConfigurationsForWorkspace(workspace: WorkspaceFolder): Promise { const filename = path.join(workspace.uri.fsPath, '.vscode', 'launch.json'); - if (!(await fs.pathExists(filename))) { - return []; + // Check launch config in the workspace file + const codeWorkspaceConfig = getConfiguration('launch'); + if (!codeWorkspaceConfig.configurations || !Array.isArray(codeWorkspaceConfig.configurations)) { + return []; + } + traceLog(`Using launch configuration in workspace folder.`); + return codeWorkspaceConfig.configurations; } const text = await fs.readFile(filename, 'utf-8'); @@ -23,6 +29,7 @@ export async function getConfigurationsForWorkspace(workspace: WorkspaceFolder): throw Error('Missing field in launch.json: version'); } // We do not bother ensuring each item is a DebugConfiguration... + traceLog(`Using launch configuration in launch.json file.`); return parsed.configurations; } diff --git a/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts new file mode 100644 index 000000000000..8ed19dc254aa --- /dev/null +++ b/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as sinon from 'sinon'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { assert } from 'chai'; +import { getConfigurationsForWorkspace } from '../../../../../client/debugger/extension/configuration/launch.json/launchJsonReader'; +import * as vscodeApis from '../../../../../client/common/vscodeApis/workspaceApis'; + +suite('Launch Json Reader', () => { + let pathExistsStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + const workspacePath = 'path/to/workspace'; + const workspaceFolder = { + name: 'workspace', + uri: Uri.file(workspacePath), + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + readFileStub = sinon.stub(fs, 'readFile'); + getConfigurationStub = sinon.stub(vscodeApis, 'getConfiguration'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Return the config in the launch.json file', async () => { + const launchPath = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); + pathExistsStub.withArgs(launchPath).resolves(true); + const launchJson = `{ + "version": "0.1.0", + "configurations": [ + { + "name": "Python: Launch.json", + "type": "python", + "request": "launch", + "purpose": ["debug-test"], + }, + ] + }`; + readFileStub.withArgs(launchPath, 'utf-8').returns(launchJson); + + const config = await getConfigurationsForWorkspace(workspaceFolder); + + assert.deepStrictEqual(config, [ + { + name: 'Python: Launch.json', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ]); + }); + + test('If there is no launch.json return the config in the workspace file', async () => { + getConfigurationStub.withArgs('launch').returns({ + configurations: [ + { + name: 'Python: Workspace File', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ], + }); + + const config = await getConfigurationsForWorkspace(workspaceFolder); + + assert.deepStrictEqual(config, [ + { + name: 'Python: Workspace File', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ]); + }); +}); From 88e2ef57fe684473184642f19ce6b673b471979c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 15 Jun 2023 09:50:27 -0700 Subject: [PATCH 0039/1136] Fix bug that errors on repeat folder name (#21433) fixes https://github.com/microsoft/vscode-python/issues/21423 fixes so the dictionary that stores the folders created so far in the plugin architecture is using the folder path as the key instead of folder name so the key is always unique. --- .../folder_b/folder_a}/test_nest.py | 0 .../expected_discovery_test_output.py | 55 ++++++++----------- .../expected_execution_test_output.py | 17 +++--- .../tests/pytestadapter/test_discovery.py | 2 +- .../tests/pytestadapter/test_execution.py | 4 +- pythonFiles/vscode_pytest/__init__.py | 8 ++- 6 files changed, 40 insertions(+), 46 deletions(-) rename pythonFiles/tests/pytestadapter/.data/{double_nested_folder/nested_folder_one/nested_folder_two => folder_a/folder_b/folder_a}/test_nest.py (100%) diff --git a/pythonFiles/tests/pytestadapter/.data/double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py b/pythonFiles/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py similarity index 100% rename from pythonFiles/tests/pytestadapter/.data/double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py rename to pythonFiles/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 8b2283029ac7..494d8794ca73 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -315,24 +315,17 @@ } # This is the expected output for the double_nested_folder tests. -# └── double_nested_folder -# └── nested_folder_one -# └── nested_folder_two +# └── folder_a +# └── folder_b +# └── folder_a # └── test_nest.py # └── test_function -double_nested_folder_path = os.fspath(TEST_DATA_PATH / "double_nested_folder") -double_nested_folder_one_path = os.fspath( - TEST_DATA_PATH / "double_nested_folder" / "nested_folder_one" -) -double_nested_folder_two_path = os.fspath( - TEST_DATA_PATH / "double_nested_folder" / "nested_folder_one" / "nested_folder_two" -) -double_nested_test_nest_path = os.fspath( - TEST_DATA_PATH - / "double_nested_folder" - / "nested_folder_one" - / "nested_folder_two" - / "test_nest.py" + +folder_a_path = os.fspath(TEST_DATA_PATH / "folder_a") +folder_b_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b") +folder_a_nested_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a") +test_nest_path = os.fspath( + TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" ) double_nested_folder_expected_output = { "name": ".data", @@ -340,39 +333,39 @@ "type_": "folder", "children": [ { - "name": "double_nested_folder", - "path": double_nested_folder_path, + "name": "folder_a", + "path": folder_a_path, "type_": "folder", - "id_": double_nested_folder_path, + "id_": folder_a_path, "children": [ { - "name": "nested_folder_one", - "path": double_nested_folder_one_path, + "name": "folder_b", + "path": folder_b_path, "type_": "folder", - "id_": double_nested_folder_one_path, + "id_": folder_b_path, "children": [ { - "name": "nested_folder_two", - "path": double_nested_folder_two_path, + "name": "folder_a", + "path": folder_a_nested_path, "type_": "folder", - "id_": double_nested_folder_two_path, + "id_": folder_a_nested_path, "children": [ { "name": "test_nest.py", - "path": double_nested_test_nest_path, + "path": test_nest_path, "type_": "file", - "id_": double_nested_test_nest_path, + "id_": test_nest_path, "children": [ { "name": "test_function", - "path": double_nested_test_nest_path, + "path": test_nest_path, "lineno": find_test_line_number( "test_function", - double_nested_test_nest_path, + test_nest_path, ), "type_": "test", - "id_": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", - "runID": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", + "id_": "folder_a/folder_b/folder_a/test_nest.py::test_function", + "runID": "folder_a/folder_b/folder_a/test_nest.py::test_function", } ], } diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index a894403c7d71..abe27ffc79ce 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -191,13 +191,14 @@ } # This is the expected output for the nested_folder tests. -# └── nested_folder_one -# └── nested_folder_two -# └── test_nest.py -# └── test_function: success +# └── folder_a +# └── folder_b +# └── folder_a +# └── test_nest.py +# └── test_function: success double_nested_folder_expected_execution_output = { - "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function": { - "test": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", + "folder_a/folder_b/folder_a/test_nest.py::test_function": { + "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", "outcome": "success", "message": None, "traceback": None, @@ -262,8 +263,8 @@ # Will run all tests in the cwd that fit the test file naming pattern. no_test_ids_pytest_execution_expected_output = { - "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function": { - "test": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", + "folder_a/folder_b/folder_a/test_nest.py::test_function": { + "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", "outcome": "success", "message": None, "traceback": None, diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index ab7abb508153..8c0f8dbae68c 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -85,7 +85,7 @@ def test_parameterized_error_collect(): expected_discovery_test_output.dual_level_nested_folder_expected_output, ), ( - "double_nested_folder", + "folder_a", expected_discovery_test_output.double_nested_folder_expected_output, ), ( diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index d54e4e758d35..5b2cbddb1644 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -103,9 +103,7 @@ def test_bad_id_error_execution(): expected_execution_test_output.dual_level_nested_folder_execution_expected_output, ), ( - [ - "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function" - ], + ["folder_a/folder_b/folder_a/test_nest.py::test_function"], expected_execution_test_output.double_nested_folder_expected_execution_output, ), ( diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 4f539e4ea01d..f9f147c381a9 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -316,7 +316,7 @@ def build_nested_folders( Keyword arguments: file_module -- the created module for the file we are nesting. file_node -- the file node that we are building the nested folders for. - created_files_folders_dict -- Dictionary of all the folders and files that have been created. + created_files_folders_dict -- Dictionary of all the folders and files that have been created where the key is the path. session -- the pytest session object. """ prev_folder_node = file_node @@ -326,12 +326,14 @@ def build_nested_folders( while iterator_path != session.path: curr_folder_name = iterator_path.name try: - curr_folder_node: TestNode = created_files_folders_dict[curr_folder_name] + curr_folder_node: TestNode = created_files_folders_dict[ + os.fspath(iterator_path) + ] except KeyError: curr_folder_node: TestNode = create_folder_node( curr_folder_name, iterator_path ) - created_files_folders_dict[curr_folder_name] = curr_folder_node + created_files_folders_dict[os.fspath(iterator_path)] = curr_folder_node if prev_folder_node not in curr_folder_node["children"]: curr_folder_node["children"].append(prev_folder_node) iterator_path = iterator_path.parent From 3dc11d2ec54054343d7daf9373852a467a84f354 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Thu, 15 Jun 2023 10:28:08 -0700 Subject: [PATCH 0040/1136] 20711 use cwd in debug testing (#21437) Closed: #20711 --- src/client/testing/common/debugLauncher.ts | 2 +- .../unittest/testExecutionAdapter.ts | 4 ++-- .../testing/common/debugLauncher.unit.test.ts | 17 ++++++++++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index f7a027ad1304..dcf23c0478db 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -146,7 +146,7 @@ export class DebugLauncher implements ITestDebugLauncher { cfg.console = 'internalConsole'; } if (!cfg.cwd) { - cfg.cwd = workspaceFolder.uri.fsPath; + cfg.cwd = configSettings.testing.cwd || workspaceFolder.uri.fsPath; } if (!cfg.env) { cfg.env = {}; diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index bf83c3c0feb1..b671a64138cb 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -44,10 +44,10 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { public async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { const settings = this.configSettings.getSettings(uri); - const { unittestArgs } = settings.testing; + const { cwd, unittestArgs } = settings.testing; const command = buildExecutionCommand(unittestArgs); - this.cwd = uri.fsPath; + this.cwd = cwd || uri.fsPath; const uuid = this.testServer.createUUID(uri.fsPath); const options: TestCommandOptions = { diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index 41b95c66040e..4712c9b6136a 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -77,7 +77,7 @@ suite('Unit Tests - Debug Launcher', () => { settings = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - unitTestSettings = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + unitTestSettings = TypeMoq.Mock.ofType(); settings.setup((p) => p.testing).returns(() => unitTestSettings.object); debugEnvHelper = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); @@ -333,6 +333,21 @@ suite('Unit Tests - Debug Launcher', () => { debugService.verifyAll(); }); + test('Use cwd value in settings if exist', async () => { + unitTestSettings.setup((p) => p.cwd).returns(() => 'path/to/settings/cwd'); + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + }; + const expected = getDefaultDebugConfig(); + expected.cwd = 'path/to/settings/cwd'; + setupSuccess(options, 'unittest', expected); + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + test('Full debug config', async () => { const options: LaunchOptions = { cwd: 'one/two/three', From 1323a6a02dffb53661f795c721e3009448a1b9e1 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 21 Jun 2023 02:01:18 +0200 Subject: [PATCH 0041/1136] Engineering - add TSAOptions (#21460) --- build/azure-pipeline.stable.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 50ccbb9fff7a..5c8444de9b2e 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -63,7 +63,7 @@ extends: - script: | python ./build/update_ext_version.py --release --for-publishing displayName: Update build number - + - script: | python ./build/update_package_file.py displayName: Update telemetry in package.json @@ -73,3 +73,16 @@ extends: - script: gulp prePublishBundle displayName: Build + tsa: + enabled: true + options: + codebaseName: 'devdiv_$(Build.Repository.Name)' + serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' + instanceUrl: 'https://devdiv.visualstudio.com/defaultcollection' + projectName: 'DevDiv' + areaPath: "DevDiv\\VS Code (compliance tracking only)\\Visual Studio Code Python Extensions" + notificationAliases: + - 'stbatt@microsoft.com' + - 'lszomoru@microsoft.com' + - 'brcan@microsoft.com' + - 'kanadig@microsoft.com' From 0f232386375b8012cd2dd838183e0e82e4803076 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 21 Jun 2023 10:28:34 -0700 Subject: [PATCH 0042/1136] Result resolver feature branch (#21457) fixes https://github.com/microsoft/vscode-python/issues/21394 --- pythonFiles/unittestadapter/execution.py | 2 - src/client/testing/common/socketServer.ts | 2 +- .../testController/common/resultResolver.ts | 233 ++++++++ .../testing/testController/common/server.ts | 43 +- .../testing/testController/common/types.ts | 12 +- .../testing/testController/common/utils.ts | 141 ++++- .../testing/testController/controller.ts | 16 +- .../pytest/pytestDiscoveryAdapter.ts | 73 +-- .../pytest/pytestExecutionAdapter.ts | 99 ++-- .../unittest/testDiscoveryAdapter.ts | 36 +- .../unittest/testExecutionAdapter.ts | 94 ++-- .../testController/workspaceTestAdapter.ts | 306 +---------- src/test/mocks/vsc/index.ts | 112 ++++ .../pytestDiscoveryAdapter.unit.test.ts | 123 +++-- .../pytestExecutionAdapter.unit.test.ts | 257 ++++++--- .../resultResolver.unit.test.ts | 368 +++++++++++++ .../testController/server.unit.test.ts | 507 ++++++++++++------ .../testDiscoveryAdapter.unit.test.ts | 62 +-- .../testExecutionAdapter.unit.test.ts | 183 +++---- .../workspaceTestAdapter.unit.test.ts | 404 +++++++++++++- src/test/vscode-mock.ts | 2 + 21 files changed, 2091 insertions(+), 984 deletions(-) create mode 100644 src/client/testing/testController/common/resultResolver.ts create mode 100644 src/test/testing/testController/resultResolver.unit.test.ts diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 4695064396cc..17c125e5843a 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -242,7 +242,6 @@ def run_tests( try: client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client_socket.connect(("localhost", run_test_ids_port_int)) - print(f"CLIENT: Server listening on port {run_test_ids_port_int}...") buffer = b"" while True: @@ -263,7 +262,6 @@ def run_tests( buffer = b"" # Process the JSON data - print(f"Received JSON data: {test_ids_from_buffer}") break except json.JSONDecodeError: # JSON decoding error, the complete JSON object is not yet received diff --git a/src/client/testing/common/socketServer.ts b/src/client/testing/common/socketServer.ts index 554d8c8a0c76..c27bf5a1606c 100644 --- a/src/client/testing/common/socketServer.ts +++ b/src/client/testing/common/socketServer.ts @@ -123,7 +123,7 @@ export class UnitTestSocketServer extends EventEmitter implements IUnitTestSocke if ((socket as any).id) { destroyedSocketId = (socket as any).id; } - this.log('socket disconnected', destroyedSocketId.toString()); + this.log('socket disconnected', destroyedSocketId?.toString()); if (socket && socket.destroy) { socket.destroy(); } diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts new file mode 100644 index 000000000000..49243390ad0f --- /dev/null +++ b/src/client/testing/testController/common/resultResolver.ts @@ -0,0 +1,233 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, TestController, TestItem, Uri, TestMessage, Location, TestRun } from 'vscode'; +import * as util from 'util'; +import { DiscoveredTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; +import { TestProvider } from '../../types'; +import { traceError, traceLog } from '../../../logging'; +import { Testing } from '../../../common/utils/localize'; +import { clearAllChildren, createErrorTestItem, getTestCaseNodes } from './testItemUtilities'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { splitLines } from '../../../common/stringUtils'; +import { buildErrorNodeOptions, fixLogLines, populateTestTree } from './utils'; + +export class PythonResultResolver implements ITestResultResolver { + testController: TestController; + + testProvider: TestProvider; + + public runIdToTestItem: Map; + + public runIdToVSid: Map; + + public vsIdToRunId: Map; + + constructor(testController: TestController, testProvider: TestProvider, private workspaceUri: Uri) { + this.testController = testController; + this.testProvider = testProvider; + + this.runIdToTestItem = new Map(); + this.runIdToVSid = new Map(); + this.vsIdToRunId = new Map(); + } + + public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise { + const workspacePath = this.workspaceUri.fsPath; + traceLog('Using result resolver for discovery'); + + const rawTestData = payload; + if (!rawTestData) { + // No test data is available + return Promise.resolve(); + } + + // Check if there were any errors in the discovery process. + if (rawTestData.status === 'error') { + const testingErrorConst = + this.testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; + const { errors } = rawTestData; + traceError(testingErrorConst, '\r\n', errors!.join('\r\n\r\n')); + + let errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`); + const message = util.format( + `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, + errors!.join('\r\n\r\n'), + ); + + if (errorNode === undefined) { + const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider); + errorNode = createErrorTestItem(this.testController, options); + this.testController.items.add(errorNode); + } + errorNode.error = message; + } else { + // Remove the error node if necessary, + // then parse and insert test data. + this.testController.items.delete(`DiscoveryError:${workspacePath}`); + + if (rawTestData.tests) { + // If the test root for this folder exists: Workspace refresh, update its children. + // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. + populateTestTree(this.testController, rawTestData.tests, undefined, this, token); + } else { + // Delete everything from the test controller. + this.testController.items.replace([]); + } + } + + sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { + tool: this.testProvider, + failed: false, + }); + return Promise.resolve(); + } + + public resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise { + const rawTestExecData = payload; + if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { + // Map which holds the subtest information for each test item. + const subTestStats: Map = new Map(); + + // iterate through payload and update the UI accordingly. + for (const keyTemp of Object.keys(rawTestExecData.result)) { + const testCases: TestItem[] = []; + + // grab leaf level test items + this.testController.items.forEach((i) => { + const tempArr: TestItem[] = getTestCaseNodes(i); + testCases.push(...tempArr); + }); + + if ( + rawTestExecData.result[keyTemp].outcome === 'failure' || + rawTestExecData.result[keyTemp].outcome === 'passed-unexpected' + ) { + const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + + const text = `${rawTestExecData.result[keyTemp].test} failed: ${ + rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome + }\r\n${traceback}\r\n`; + const message = new TestMessage(text); + + // note that keyTemp is a runId for unittest library... + const grabVSid = this.runIdToVSid.get(keyTemp); + // search through freshly built array of testItem to find the failed test and update UI. + testCases.forEach((indiItem) => { + if (indiItem.id === grabVSid) { + if (indiItem.uri && indiItem.range) { + message.location = new Location(indiItem.uri, indiItem.range); + runInstance.failed(indiItem, message); + runInstance.appendOutput(fixLogLines(text)); + } + } + }); + } else if ( + rawTestExecData.result[keyTemp].outcome === 'success' || + rawTestExecData.result[keyTemp].outcome === 'expected-failure' + ) { + const grabTestItem = this.runIdToTestItem.get(keyTemp); + const grabVSid = this.runIdToVSid.get(keyTemp); + if (grabTestItem !== undefined) { + testCases.forEach((indiItem) => { + if (indiItem.id === grabVSid) { + if (indiItem.uri && indiItem.range) { + runInstance.passed(grabTestItem); + runInstance.appendOutput('Passed here'); + } + } + }); + } + } else if (rawTestExecData.result[keyTemp].outcome === 'skipped') { + const grabTestItem = this.runIdToTestItem.get(keyTemp); + const grabVSid = this.runIdToVSid.get(keyTemp); + if (grabTestItem !== undefined) { + testCases.forEach((indiItem) => { + if (indiItem.id === grabVSid) { + if (indiItem.uri && indiItem.range) { + runInstance.skipped(grabTestItem); + runInstance.appendOutput('Skipped here'); + } + } + }); + } + } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { + // split on " " since the subtest ID has the parent test ID in the first part of the ID. + const parentTestCaseId = keyTemp.split(' ')[0]; + const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); + const data = rawTestExecData.result[keyTemp]; + // find the subtest's parent test item + if (parentTestItem) { + const subtestStats = subTestStats.get(parentTestCaseId); + if (subtestStats) { + subtestStats.failed += 1; + } else { + subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); + runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); + // clear since subtest items don't persist between runs + clearAllChildren(parentTestItem); + } + const subtestId = keyTemp; + const subTestItem = this.testController?.createTestItem(subtestId, subtestId); + runInstance.appendOutput(fixLogLines(`${subtestId} Failed\r\n`)); + // create a new test item for the subtest + if (subTestItem) { + const traceback = data.traceback ?? ''; + const text = `${data.subtest} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`; + runInstance.appendOutput(fixLogLines(text)); + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + const message = new TestMessage(rawTestExecData?.result[keyTemp].message ?? ''); + if (parentTestItem.uri && parentTestItem.range) { + message.location = new Location(parentTestItem.uri, parentTestItem.range); + } + runInstance.failed(subTestItem, message); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { + // split on " " since the subtest ID has the parent test ID in the first part of the ID. + const parentTestCaseId = keyTemp.split(' ')[0]; + const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); + + // find the subtest's parent test item + if (parentTestItem) { + const subtestStats = subTestStats.get(parentTestCaseId); + if (subtestStats) { + subtestStats.passed += 1; + } else { + subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); + runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); + // clear since subtest items don't persist between runs + clearAllChildren(parentTestItem); + } + const subtestId = keyTemp; + const subTestItem = this.testController?.createTestItem(subtestId, subtestId); + // create a new test item for the subtest + if (subTestItem) { + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + runInstance.passed(subTestItem); + runInstance.appendOutput(fixLogLines(`${subtestId} Passed\r\n`)); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } + } + } + return Promise.resolve(); + } +} + +// had to switch the order of the original parameter since required param cannot follow optional. diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 6bd9bf348e20..32829e355ccb 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -9,7 +9,7 @@ import { IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceLog } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; @@ -24,6 +24,10 @@ export class PythonTestServer implements ITestServer, Disposable { private ready: Promise; + private _onRunDataReceived: EventEmitter = new EventEmitter(); + + private _onDiscoveryDataReceived: EventEmitter = new EventEmitter(); + constructor(private executionFactory: IPythonExecutionFactory, private debugLauncher: ITestDebugLauncher) { this.server = net.createServer((socket: net.Socket) => { let buffer: Buffer = Buffer.alloc(0); // Buffer to accumulate received data @@ -48,11 +52,28 @@ export class PythonTestServer implements ITestServer, Disposable { rawData = rpcHeaders.remainingRawData; const rpcContent = jsonRPCContent(rpcHeaders.headers, rawData); const extractedData = rpcContent.extractedJSON; + // do not send until we have the full content if (extractedData.length === Number(totalContentLength)) { - // do not send until we have the full content - traceVerbose(`Received data from test server: ${extractedData}`); - this._onDataReceived.fire({ uuid, data: extractedData }); - this.uuids = this.uuids.filter((u) => u !== uuid); + // if the rawData includes tests then this is a discovery request + if (rawData.includes(`"tests":`)) { + this._onDiscoveryDataReceived.fire({ + uuid, + data: rpcContent.extractedJSON, + }); + // if the rawData includes result then this is a run request + } else if (rawData.includes(`"result":`)) { + this._onRunDataReceived.fire({ + uuid, + data: rpcContent.extractedJSON, + }); + } else { + traceLog( + `Error processing test server request: request is not recognized as discovery or run.`, + ); + this._onDataReceived.fire({ uuid: '', data: '' }); + return; + } + // this.uuids = this.uuids.filter((u) => u !== uuid); WHERE DOES THIS GO?? buffer = Buffer.alloc(0); } else { break; @@ -97,6 +118,18 @@ export class PythonTestServer implements ITestServer, Disposable { return uuid; } + public deleteUUID(uuid: string): void { + this.uuids = this.uuids.filter((u) => u !== uuid); + } + + public get onRunDataReceived(): Event { + return this._onRunDataReceived.event; + } + + public get onDiscoveryDataReceived(): Event { + return this._onDiscoveryDataReceived.event; + } + public dispose(): void { this.server.close(); this._onDataReceived.dispose(); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 4307d7a3913f..cb7fda797c4a 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -172,12 +172,21 @@ export type TestCommandOptionsPytest = { */ export interface ITestServer { readonly onDataReceived: Event; + readonly onRunDataReceived: Event; + readonly onDiscoveryDataReceived: Event; sendCommand(options: TestCommandOptions, runTestIdsPort?: string, callback?: () => void): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; + deleteUUID(uuid: string): void; +} +export interface ITestResultResolver { + runIdToVSid: Map; + runIdToTestItem: Map; + vsIdToRunId: Map; + resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise; + resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise; } - export interface ITestDiscoveryAdapter { // ** first line old method signature, second line new method signature discoverTests(uri: Uri): Promise; @@ -192,6 +201,7 @@ export interface ITestExecutionAdapter { uri: Uri, testIds: string[], debugBool?: boolean, + runInstance?: TestRun, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, ): Promise; diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 1bf31e80e11a..f98550d3e72b 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -1,11 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as net from 'net'; -import { traceLog } from '../../../logging'; +import * as path from 'path'; +import { CancellationToken, Position, TestController, TestItem, Uri, Range } from 'vscode'; +import { traceError, traceLog, traceVerbose } from '../../../logging'; import { EnableTestAdapterRewrite } from '../../../common/experiments/groups'; import { IExperimentService } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; +import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilities'; +import { DiscoveredTestItem, DiscoveredTestNode, ITestResultResolver } from './types'; export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); @@ -62,38 +66,125 @@ export function pythonTestAdapterRewriteEnabled(serviceContainer: IServiceContai return experiment.inExperimentSync(EnableTestAdapterRewrite.experiment); } -export const startServer = (testIds: string): Promise => - new Promise((resolve, reject) => { - const server = net.createServer((socket: net.Socket) => { - // Convert the test_ids array to JSON - const testData = JSON.stringify(testIds); +export async function startTestIdServer(testIds: string[]): Promise { + const startServer = (): Promise => + new Promise((resolve, reject) => { + const server = net.createServer((socket: net.Socket) => { + // Convert the test_ids array to JSON + const testData = JSON.stringify(testIds); - // Create the headers - const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; + // Create the headers + const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; - // Create the payload by concatenating the headers and the test data - const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; + // Create the payload by concatenating the headers and the test data + const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; - // Send the payload to the socket - socket.write(payload); + // Send the payload to the socket + socket.write(payload); - // Handle socket events - socket.on('data', (data) => { - traceLog('Received data:', data.toString()); + // Handle socket events + socket.on('data', (data) => { + traceLog('Received data:', data.toString()); + }); + + socket.on('end', () => { + traceLog('Client disconnected'); + }); }); - socket.on('end', () => { - traceLog('Client disconnected'); + server.listen(0, () => { + const { port } = server.address() as net.AddressInfo; + traceLog(`Server listening on port ${port}`); + resolve(port); }); - }); - server.listen(0, () => { - const { port } = server.address() as net.AddressInfo; - traceLog(`Server listening on port ${port}`); - resolve(port); + server.on('error', (error: Error) => { + reject(error); + }); }); - server.on('error', (error: Error) => { - reject(error); - }); + // Start the server and wait until it is listening + let returnPort = 0; + try { + await startServer() + .then((assignedPort) => { + traceVerbose(`Server started for pytest test ids server and listening on port ${assignedPort}`); + returnPort = assignedPort; + }) + .catch((error) => { + traceError('Error starting server for pytest test ids server:', error); + return 0; + }) + .finally(() => returnPort); + return returnPort; + } catch { + traceError('Error starting server for pytest test ids server, cannot get port.'); + return returnPort; + } +} + +export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { + const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error'; + return { + id: `DiscoveryError:${uri.fsPath}`, + label: `${labelText} [${path.basename(uri.fsPath)}]`, + error: message, + }; +} + +export function populateTestTree( + testController: TestController, + testTreeData: DiscoveredTestNode, + testRoot: TestItem | undefined, + resultResolver: ITestResultResolver, + token?: CancellationToken, +): void { + // If testRoot is undefined, use the info of the root item of testTreeData to create a test item, and append it to the test controller. + if (!testRoot) { + testRoot = testController.createTestItem(testTreeData.path, testTreeData.name, Uri.file(testTreeData.path)); + + testRoot.canResolveChildren = true; + testRoot.tags = [RunTestTag, DebugTestTag]; + + testController.items.add(testRoot); + } + + // Recursively populate the tree with test data. + testTreeData.children.forEach((child) => { + if (!token?.isCancellationRequested) { + if (isTestItem(child)) { + const testItem = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); + testItem.tags = [RunTestTag, DebugTestTag]; + + const range = new Range( + new Position(Number(child.lineno) - 1, 0), + new Position(Number(child.lineno), 0), + ); + testItem.canResolveChildren = false; + testItem.range = range; + testItem.tags = [RunTestTag, DebugTestTag]; + + testRoot!.children.add(testItem); + // add to our map + resultResolver.runIdToTestItem.set(child.runID, testItem); + resultResolver.runIdToVSid.set(child.runID, child.id_); + resultResolver.vsIdToRunId.set(child.id_, child.runID); + } else { + let node = testController.items.get(child.path); + + if (!node) { + node = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); + + node.canResolveChildren = true; + node.tags = [RunTestTag, DebugTestTag]; + testRoot!.children.add(node); + } + populateTestTree(testController, child, node, resultResolver, token); + } + } }); +} + +function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem { + return test.type_ === 'test'; +} diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 0d3487855380..eff333a4cdd9 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -31,12 +31,14 @@ import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../common/constants'; import { TestProvider } from '../types'; import { PythonTestServer } from './common/server'; import { DebugTestTag, getNodeByUri, RunTestTag } from './common/testItemUtilities'; +import { pythonTestAdapterRewriteEnabled } from './common/utils'; import { ITestController, ITestDiscoveryAdapter, ITestFrameworkController, TestRefreshOptions, ITestExecutionAdapter, + ITestResultResolver, } from './common/types'; import { UnittestTestDiscoveryAdapter } from './unittest/testDiscoveryAdapter'; import { UnittestTestExecutionAdapter } from './unittest/testExecutionAdapter'; @@ -44,8 +46,8 @@ import { PytestTestDiscoveryAdapter } from './pytest/pytestDiscoveryAdapter'; import { PytestTestExecutionAdapter } from './pytest/pytestExecutionAdapter'; import { WorkspaceTestAdapter } from './workspaceTestAdapter'; import { ITestDebugLauncher } from '../common/types'; -import { pythonTestAdapterRewriteEnabled } from './common/utils'; import { IServiceContainer } from '../../ioc/types'; +import { PythonResultResolver } from './common/resultResolver'; // Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER]; @@ -161,30 +163,37 @@ export class PythonTestController implements ITestController, IExtensionSingleAc let discoveryAdapter: ITestDiscoveryAdapter; let executionAdapter: ITestExecutionAdapter; let testProvider: TestProvider; + let resultResolver: ITestResultResolver; if (settings.testing.unittestEnabled) { + testProvider = UNITTEST_PROVIDER; + resultResolver = new PythonResultResolver(this.testController, testProvider, workspace.uri); discoveryAdapter = new UnittestTestDiscoveryAdapter( this.pythonTestServer, this.configSettings, this.testOutputChannel, + resultResolver, ); executionAdapter = new UnittestTestExecutionAdapter( this.pythonTestServer, this.configSettings, this.testOutputChannel, + resultResolver, ); - testProvider = UNITTEST_PROVIDER; } else { + testProvider = PYTEST_PROVIDER; + resultResolver = new PythonResultResolver(this.testController, testProvider, workspace.uri); discoveryAdapter = new PytestTestDiscoveryAdapter( this.pythonTestServer, this.configSettings, this.testOutputChannel, + resultResolver, ); executionAdapter = new PytestTestExecutionAdapter( this.pythonTestServer, this.configSettings, this.testOutputChannel, + resultResolver, ); - testProvider = PYTEST_PROVIDER; } const workspaceTestAdapter = new WorkspaceTestAdapter( @@ -192,6 +201,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc discoveryAdapter, executionAdapter, workspace.uri, + resultResolver, ); this.testAdapters.set(workspace.uri, workspaceTestAdapter); diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index aeb920407cd2..4378c68b534c 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -10,8 +10,14 @@ import { import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceError, traceLog, traceVerbose } from '../../../logging'; -import { DataReceivedEvent, DiscoveredTestPayload, ITestDiscoveryAdapter, ITestServer } from '../common/types'; +import { traceError, traceVerbose } from '../../../logging'; +import { + DataReceivedEvent, + DiscoveredTestPayload, + ITestDiscoveryAdapter, + ITestResultResolver, + ITestServer, +} from '../common/types'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied @@ -19,39 +25,32 @@ import { DataReceivedEvent, DiscoveredTestPayload, ITestDiscoveryAdapter, ITestS export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { private promiseMap: Map> = new Map(); - private deferred: Deferred | undefined; - constructor( public testServer: ITestServer, public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, - ) { - testServer.onDataReceived(this.onDataReceivedHandler, this); - } - - public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { - const deferred = this.promiseMap.get(uuid); - if (deferred) { - deferred.resolve(JSON.parse(data)); - this.promiseMap.delete(uuid); - } - } + private readonly resultResolver?: ITestResultResolver, + ) {} - discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { - if (executionFactory !== undefined) { - // ** new version of discover tests. - const settings = this.configSettings.getSettings(uri); - const { pytestArgs } = settings.testing; - traceVerbose(pytestArgs); - return this.runPytestDiscovery(uri, executionFactory); + async discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { + const settings = this.configSettings.getSettings(uri); + const { pytestArgs } = settings.testing; + traceVerbose(pytestArgs); + const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { + // cancelation token ? + this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); + }); + try { + await this.runPytestDiscovery(uri, executionFactory); + } finally { + disposable.dispose(); } - // if executionFactory is undefined, we are using the old method signature of discover tests. - traceVerbose(uri); - this.deferred = createDeferred(); - return this.deferred.promise; + // this is only a placeholder to handle function overloading until rewrite is finished + const discoveryPayload: DiscoveredTestPayload = { cwd: uri.fsPath, status: 'success' }; + return discoveryPayload; } - async runPytestDiscovery(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { + async runPytestDiscovery(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); @@ -79,13 +78,19 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { allowEnvironmentFetchExceptions: false, resource: uri, }; - const execService = await executionFactory.createActivatedEnvironment(creationOptions); - const discoveryArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); - traceLog(`Discovering pytest tests with arguments: ${discoveryArgs.join(' ')}`); - execService.exec(discoveryArgs, spawnOptions).catch((ex) => { - traceError(`Error occurred while discovering tests: ${ex}`); - deferred.reject(ex as Error); - }); + const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + // delete UUID following entire discovery finishing. + execService + ?.exec(['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), spawnOptions) + .then(() => { + this.testServer.deleteUUID(uuid); + return deferred.resolve(); + }) + .catch((err) => { + traceError(`Error while trying to run pytest discovery, \n${err}\r\n\r\n`); + this.testServer.deleteUUID(uuid); + return deferred.reject(err); + }); return deferred.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 90704b5d67f4..1bf032b9b594 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -1,13 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Uri } from 'vscode'; +import { TestRun, Uri } from 'vscode'; import * as path from 'path'; -import * as net from 'net'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; -import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; +import { + DataReceivedEvent, + ExecutionTestPayload, + ITestExecutionAdapter, + ITestResultResolver, + ITestServer, +} from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, IPythonExecutionFactory, @@ -17,6 +22,7 @@ import { removePositionalFoldersAndFiles } from './arguments'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { PYTEST_PROVIDER } from '../../common/constants'; import { EXTENSION_ROOT_DIR } from '../../../common/constants'; +import { startTestIdServer } from '../common/utils'; // eslint-disable-next-line @typescript-eslint/no-explicit-any // (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; @@ -27,39 +33,35 @@ import { EXTENSION_ROOT_DIR } from '../../../common/constants'; export class PytestTestExecutionAdapter implements ITestExecutionAdapter { private promiseMap: Map> = new Map(); - private deferred: Deferred | undefined; - constructor( public testServer: ITestServer, public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, - ) { - testServer.onDataReceived(this.onDataReceivedHandler, this); - } - - public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { - const deferred = this.promiseMap.get(uuid); - if (deferred) { - deferred.resolve(JSON.parse(data)); - this.promiseMap.delete(uuid); - } - } + private readonly resultResolver?: ITestResultResolver, + ) {} async runTests( uri: Uri, testIds: string[], debugBool?: boolean, + runInstance?: TestRun, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, ): Promise { - if (executionFactory !== undefined) { - // ** new version of run tests. - return this.runTestsNew(uri, testIds, debugBool, executionFactory, debugLauncher); + traceVerbose(uri, testIds, debugBool); + const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + if (runInstance) { + this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); + } + }); + try { + await this.runTestsNew(uri, testIds, debugBool, executionFactory, debugLauncher); + } finally { + disposable.dispose(); + // confirm with testing that this gets called (it must clean this up) } - // if executionFactory is undefined, we are using the old method signature of run tests. - this.outputChannel.appendLine('Running tests.'); - this.deferred = createDeferred(); - return this.deferred.promise; + const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; + return executionPayload; } private async runTestsNew( @@ -114,49 +116,11 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { testArgs.push('--capture', 'no'); } + traceLog(`Running PYTEST execution for the following test ids: ${testIds}`); - // create payload with testIds to send to run pytest script - const testData = JSON.stringify(testIds); - const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; - const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; - traceLog(`Running pytest execution for the following test ids: ${testIds}`); - - let pytestRunTestIdsPort: string | undefined; - const startServer = (): Promise => - new Promise((resolve, reject) => { - const server = net.createServer((socket: net.Socket) => { - socket.on('end', () => { - traceVerbose('Client disconnected for pytest test ids server'); - }); - }); - - server.listen(0, () => { - const { port } = server.address() as net.AddressInfo; - traceVerbose(`Server listening on port ${port} for pytest test ids server`); - resolve(port); - }); - - server.on('error', (error: Error) => { - traceError('Error starting server for pytest test ids server:', error); - reject(error); - }); - server.on('connection', (socket: net.Socket) => { - socket.write(payload); - traceVerbose('payload sent for pytest execution', payload); - }); - }); - - // Start the server and wait until it is listening - await startServer() - .then((assignedPort) => { - traceVerbose(`Server started for pytest test ids server and listening on port ${assignedPort}`); - pytestRunTestIdsPort = assignedPort.toString(); - if (spawnOptions.extraVariables) - spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort; - }) - .catch((error) => { - traceError('Error starting server for pytest test ids server:', error); - }); + const pytestRunTestIdsPort = await startTestIdServer(testIds); + if (spawnOptions.extraVariables) + spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); if (debugBool) { const pytestPort = this.testServer.getPort().toString(); @@ -168,7 +132,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testProvider: PYTEST_PROVIDER, pytestPort, pytestUUID, - runTestIdsPort: pytestRunTestIdsPort, + runTestIdsPort: pytestRunTestIdsPort.toString(), }; traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions, () => { @@ -187,6 +151,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { return Promise.reject(ex); } - return deferred.promise; + const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; + return executionPayload; } } diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 9c565af78c08..8d393a8da18d 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -10,11 +10,11 @@ import { DataReceivedEvent, DiscoveredTestPayload, ITestDiscoveryAdapter, + ITestResultResolver, ITestServer, TestCommandOptions, TestDiscoveryCommand, } from '../common/types'; -import { traceInfo } from '../../../logging'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. @@ -28,17 +28,8 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { public testServer: ITestServer, public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, - ) { - testServer.onDataReceived(this.onDataReceivedHandler, this); - } - - public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { - const deferred = this.promiseMap.get(uuid); - if (deferred) { - deferred.resolve(JSON.parse(data)); - this.promiseMap.delete(uuid); - } - } + private readonly resultResolver?: ITestResultResolver, + ) {} public async discoverTests(uri: Uri): Promise { const deferred = createDeferred(); @@ -60,12 +51,23 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { this.promiseMap.set(uuid, deferred); - // Send the test command to the server. - // The server will fire an onDataReceived event once it gets a response. - traceInfo(`Sending discover unittest script to server.`); - this.testServer.sendCommand(options); + const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { + this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); + }); + try { + await this.callSendCommand(options); + } finally { + disposable.dispose(); + // confirm with testing that this gets called (it must clean this up) + } + const discoveryPayload: DiscoveredTestPayload = { cwd: uri.fsPath, status: 'success' }; + return discoveryPayload; + } - return deferred.promise; + private async callSendCommand(options: TestCommandOptions): Promise { + await this.testServer.sendCommand(options); + const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; + return discoveryPayload; } } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index b671a64138cb..ca88d3871706 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -2,20 +2,21 @@ // Licensed under the MIT License. import * as path from 'path'; -import { Uri } from 'vscode'; -import * as net from 'net'; +import { TestRun, Uri } from 'vscode'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; +import { Deferred, createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, + ITestResultResolver, ITestServer, TestCommandOptions, TestExecutionCommand, } from '../common/types'; -import { traceLog, traceError } from '../../../logging'; +import { traceLog } from '../../../logging'; +import { startTestIdServer } from '../common/utils'; /** * Wrapper Class for unittest test execution. This is where we call `runTestCommand`? @@ -30,19 +31,31 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { public testServer: ITestServer, public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, - ) { - testServer.onDataReceived(this.onDataReceivedHandler, this); - } - - public onDataReceivedHandler({ uuid, data }: DataReceivedEvent): void { - const deferred = this.promiseMap.get(uuid); - if (deferred) { - deferred.resolve(JSON.parse(data)); - this.promiseMap.delete(uuid); + private readonly resultResolver?: ITestResultResolver, + ) {} + + public async runTests( + uri: Uri, + testIds: string[], + debugBool?: boolean, + runInstance?: TestRun, + ): Promise { + const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + if (runInstance) { + this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); + } + }); + try { + await this.runTestsNew(uri, testIds, debugBool); + } finally { + disposable.dispose(); + // confirm with testing that this gets called (it must clean this up) } + const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; + return executionPayload; } - public async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { + private async runTestsNew(uri: Uri, testIds: string[], debugBool?: boolean): Promise { const settings = this.configSettings.getSettings(uri); const { cwd, unittestArgs } = settings.testing; @@ -62,52 +75,17 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const deferred = createDeferred(); this.promiseMap.set(uuid, deferred); + traceLog(`Running UNITTEST execution for the following test ids: ${testIds}`); - // create payload with testIds to send to run pytest script - const testData = JSON.stringify(testIds); - const headers = [`Content-Length: ${Buffer.byteLength(testData)}`, 'Content-Type: application/json']; - const payload = `${headers.join('\r\n')}\r\n\r\n${testData}`; - - let runTestIdsPort: string | undefined; - const startServer = (): Promise => - new Promise((resolve, reject) => { - const server = net.createServer((socket: net.Socket) => { - socket.on('end', () => { - traceLog('Client disconnected'); - }); - }); - - server.listen(0, () => { - const { port } = server.address() as net.AddressInfo; - traceLog(`Server listening on port ${port}`); - resolve(port); - }); - - server.on('error', (error: Error) => { - reject(error); - }); - server.on('connection', (socket: net.Socket) => { - socket.write(payload); - traceLog('payload sent', payload); - }); - }); - - // Start the server and wait until it is listening - await startServer() - .then((assignedPort) => { - traceLog(`Server started and listening on port ${assignedPort}`); - runTestIdsPort = assignedPort.toString(); - // Send test command to server. - // Server fire onDataReceived event once it gets response. - this.testServer.sendCommand(options, runTestIdsPort, () => { - deferred.resolve(); - }); - }) - .catch((error) => { - traceError('Error starting server:', error); - }); + const runTestIdsPort = await startTestIdServer(testIds); - return deferred.promise; + await this.testServer.sendCommand(options, runTestIdsPort.toString(), () => { + // disposable.dispose(); + deferred.resolve(); + }); + // return deferred.promise; + const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; + return executionPayload; } } diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index 5cba6c193d3c..f3ea0b9f6193 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -1,38 +1,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as path from 'path'; import * as util from 'util'; -import { - CancellationToken, - Position, - Range, - TestController, - TestItem, - TestMessage, - TestRun, - Uri, - Location, -} from 'vscode'; -import { splitLines } from '../../common/stringUtils'; +import { CancellationToken, TestController, TestItem, TestRun, Uri } from 'vscode'; import { createDeferred, Deferred } from '../../common/utils/async'; import { Testing } from '../../common/utils/localize'; -import { traceError, traceVerbose } from '../../logging'; +import { traceError } from '../../logging'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { TestProvider } from '../types'; -import { - clearAllChildren, - createErrorTestItem, - DebugTestTag, - ErrorTestItemOptions, - getTestCaseNodes, - RunTestTag, -} from './common/testItemUtilities'; -import { DiscoveredTestItem, DiscoveredTestNode, ITestDiscoveryAdapter, ITestExecutionAdapter } from './common/types'; -import { fixLogLines } from './common/utils'; +import { createErrorTestItem, getTestCaseNodes } from './common/testItemUtilities'; +import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './common/types'; import { IPythonExecutionFactory } from '../../common/process/types'; import { ITestDebugLauncher } from '../common/types'; +import { buildErrorNodeOptions } from './common/utils'; /** * This class exposes a test-provider-agnostic way of discovering tests. @@ -48,22 +29,13 @@ export class WorkspaceTestAdapter { private executing: Deferred | undefined; - runIdToTestItem: Map; - - runIdToVSid: Map; - - vsIdToRunId: Map; - constructor( private testProvider: TestProvider, private discoveryAdapter: ITestDiscoveryAdapter, private executionAdapter: ITestExecutionAdapter, private workspaceUri: Uri, - ) { - this.runIdToTestItem = new Map(); - this.runIdToVSid = new Map(); - this.vsIdToRunId = new Map(); - } + private resultResolver: ITestResultResolver, + ) {} public async executeTests( testController: TestController, @@ -81,7 +53,6 @@ export class WorkspaceTestAdapter { const deferred = createDeferred(); this.executing = deferred; - let rawTestExecData; const testCaseNodes: TestItem[] = []; const testCaseIdsSet = new Set(); try { @@ -93,7 +64,7 @@ export class WorkspaceTestAdapter { // iterate through testItems nodes and fetch their unittest runID to pass in as argument testCaseNodes.forEach((node) => { runInstance.started(node); // do the vscode ui test item start here before runtest - const runId = this.vsIdToRunId.get(node.id); + const runId = this.resultResolver.vsIdToRunId.get(node.id); if (runId) { testCaseIdsSet.add(runId); } @@ -101,16 +72,16 @@ export class WorkspaceTestAdapter { const testCaseIds = Array.from(testCaseIdsSet); // ** execution factory only defined for new rewrite way if (executionFactory !== undefined) { - traceVerbose('executionFactory defined'); - rawTestExecData = await this.executionAdapter.runTests( + await this.executionAdapter.runTests( this.workspaceUri, testCaseIds, debugBool, + runInstance, executionFactory, debugLauncher, ); } else { - rawTestExecData = await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); + await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); } deferred.resolve(); } catch (ex) { @@ -136,146 +107,6 @@ export class WorkspaceTestAdapter { this.executing = undefined; } - if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { - // Map which holds the subtest information for each test item. - const subTestStats: Map = new Map(); - - // iterate through payload and update the UI accordingly. - for (const keyTemp of Object.keys(rawTestExecData.result)) { - const testCases: TestItem[] = []; - - // grab leaf level test items - testController.items.forEach((i) => { - const tempArr: TestItem[] = getTestCaseNodes(i); - testCases.push(...tempArr); - }); - - if ( - rawTestExecData.result[keyTemp].outcome === 'failure' || - rawTestExecData.result[keyTemp].outcome === 'passed-unexpected' - ) { - const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; - const traceback = splitLines(rawTraceback, { - trim: false, - removeEmptyEntries: true, - }).join('\r\n'); - - const text = `${rawTestExecData.result[keyTemp].test} failed: ${ - rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome - }\r\n${traceback}\r\n`; - const message = new TestMessage(text); - - // note that keyTemp is a runId for unittest library... - const grabVSid = this.runIdToVSid.get(keyTemp); - // search through freshly built array of testItem to find the failed test and update UI. - testCases.forEach((indiItem) => { - if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { - message.location = new Location(indiItem.uri, indiItem.range); - runInstance.failed(indiItem, message); - runInstance.appendOutput(fixLogLines(text)); - } - } - }); - } else if ( - rawTestExecData.result[keyTemp].outcome === 'success' || - rawTestExecData.result[keyTemp].outcome === 'expected-failure' - ) { - const grabTestItem = this.runIdToTestItem.get(keyTemp); - const grabVSid = this.runIdToVSid.get(keyTemp); - if (grabTestItem !== undefined) { - testCases.forEach((indiItem) => { - if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { - runInstance.passed(grabTestItem); - runInstance.appendOutput('Passed here'); - } - } - }); - } - } else if (rawTestExecData.result[keyTemp].outcome === 'skipped') { - const grabTestItem = this.runIdToTestItem.get(keyTemp); - const grabVSid = this.runIdToVSid.get(keyTemp); - if (grabTestItem !== undefined) { - testCases.forEach((indiItem) => { - if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { - runInstance.skipped(grabTestItem); - runInstance.appendOutput('Skipped here'); - } - } - }); - } - } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { - // split on " " since the subtest ID has the parent test ID in the first part of the ID. - const parentTestCaseId = keyTemp.split(' ')[0]; - const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); - const data = rawTestExecData.result[keyTemp]; - // find the subtest's parent test item - if (parentTestItem) { - const subtestStats = subTestStats.get(parentTestCaseId); - if (subtestStats) { - subtestStats.failed += 1; - } else { - subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); - runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); - // clear since subtest items don't persist between runs - clearAllChildren(parentTestItem); - } - const subtestId = keyTemp; - const subTestItem = testController?.createTestItem(subtestId, subtestId); - runInstance.appendOutput(fixLogLines(`${subtestId} Failed\r\n`)); - // create a new test item for the subtest - if (subTestItem) { - const traceback = data.traceback ?? ''; - const text = `${data.subtest} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`; - runInstance.appendOutput(fixLogLines(text)); - parentTestItem.children.add(subTestItem); - runInstance.started(subTestItem); - const message = new TestMessage(rawTestExecData?.result[keyTemp].message ?? ''); - if (parentTestItem.uri && parentTestItem.range) { - message.location = new Location(parentTestItem.uri, parentTestItem.range); - } - runInstance.failed(subTestItem, message); - } else { - throw new Error('Unable to create new child node for subtest'); - } - } else { - throw new Error('Parent test item not found'); - } - } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { - // split on " " since the subtest ID has the parent test ID in the first part of the ID. - const parentTestCaseId = keyTemp.split(' ')[0]; - const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); - - // find the subtest's parent test item - if (parentTestItem) { - const subtestStats = subTestStats.get(parentTestCaseId); - if (subtestStats) { - subtestStats.passed += 1; - } else { - subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); - runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); - // clear since subtest items don't persist between runs - clearAllChildren(parentTestItem); - } - const subtestId = keyTemp; - const subTestItem = testController?.createTestItem(subtestId, subtestId); - // create a new test item for the subtest - if (subTestItem) { - parentTestItem.children.add(subTestItem); - runInstance.started(subTestItem); - runInstance.passed(subTestItem); - runInstance.appendOutput(fixLogLines(`${subtestId} Passed\r\n`)); - } else { - throw new Error('Unable to create new child node for subtest'); - } - } else { - throw new Error('Parent test item not found'); - } - } - } - } return Promise.resolve(); } @@ -286,8 +117,6 @@ export class WorkspaceTestAdapter { ): Promise { sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: this.testProvider }); - const workspacePath = this.workspaceUri.fsPath; - // Discovery is expensive. If it is already running, use the existing promise. if (this.discovering) { return this.discovering.promise; @@ -296,14 +125,12 @@ export class WorkspaceTestAdapter { const deferred = createDeferred(); this.discovering = deferred; - let rawTestData; try { // ** execution factory only defined for new rewrite way if (executionFactory !== undefined) { - traceVerbose('executionFactory defined'); - rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); + await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); } else { - rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri); + await this.discoveryAdapter.discoverTests(this.workspaceUri); } deferred.resolve(); } catch (ex) { @@ -324,119 +151,14 @@ export class WorkspaceTestAdapter { const errorNode = createErrorTestItem(testController, options); testController.items.add(errorNode); - deferred.reject(ex as Error); + return deferred.reject(ex as Error); } finally { // Discovery has finished running, we have the data, // we don't need the deferred promise anymore. this.discovering = undefined; } - if (!rawTestData) { - // No test data is available - return Promise.resolve(); - } - - // Check if there were any errors in the discovery process. - if (rawTestData.status === 'error') { - const testingErrorConst = - this.testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; - const { errors } = rawTestData; - traceError(testingErrorConst, '\r\n', errors?.join('\r\n\r\n')); - let errorNode = testController.items.get(`DiscoveryError:${workspacePath}`); - const message = util.format( - `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, - errors?.join('\r\n\r\n'), - ); - - if (errorNode === undefined) { - const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider); - errorNode = createErrorTestItem(testController, options); - testController.items.add(errorNode); - } - errorNode.error = message; - } else { - // Remove the error node if necessary, - // then parse and insert test data. - testController.items.delete(`DiscoveryError:${workspacePath}`); - - if (rawTestData.tests) { - // If the test root for this folder exists: Workspace refresh, update its children. - // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. - populateTestTree(testController, rawTestData.tests, undefined, this, token); - } else { - // Delete everything from the test controller. - testController.items.replace([]); - } - } - sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: false }); return Promise.resolve(); } } - -function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem { - return test.type_ === 'test'; -} - -// had to switch the order of the original parameter since required param cannot follow optional. -function populateTestTree( - testController: TestController, - testTreeData: DiscoveredTestNode, - testRoot: TestItem | undefined, - wstAdapter: WorkspaceTestAdapter, - token?: CancellationToken, -): void { - // If testRoot is undefined, use the info of the root item of testTreeData to create a test item, and append it to the test controller. - if (!testRoot) { - testRoot = testController.createTestItem(testTreeData.path, testTreeData.name, Uri.file(testTreeData.path)); - - testRoot.canResolveChildren = true; - testRoot.tags = [RunTestTag, DebugTestTag]; - - testController.items.add(testRoot); - } - - // Recursively populate the tree with test data. - testTreeData.children.forEach((child) => { - if (!token?.isCancellationRequested) { - if (isTestItem(child)) { - const testItem = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); - testItem.tags = [RunTestTag, DebugTestTag]; - - const range = new Range( - new Position(Number(child.lineno) - 1, 0), - new Position(Number(child.lineno), 0), - ); - testItem.canResolveChildren = false; - testItem.range = range; - testItem.tags = [RunTestTag, DebugTestTag]; - - testRoot!.children.add(testItem); - // add to our map - wstAdapter.runIdToTestItem.set(child.runID, testItem); - wstAdapter.runIdToVSid.set(child.runID, child.id_); - wstAdapter.vsIdToRunId.set(child.id_, child.runID); - } else { - let node = testController.items.get(child.path); - - if (!node) { - node = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); - - node.canResolveChildren = true; - node.tags = [RunTestTag, DebugTestTag]; - testRoot!.children.add(node); - } - populateTestTree(testController, child, node, wstAdapter, token); - } - } - }); -} - -function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { - const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error'; - return { - id: `DiscoveryError:${uri.fsPath}`, - label: `${labelText} [${path.basename(uri.fsPath)}]`, - error: message, - }; -} diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts index 89f4ab1a2d07..092fc67da6c6 100644 --- a/src/test/mocks/vsc/index.ts +++ b/src/test/mocks/vsc/index.ts @@ -6,6 +6,7 @@ import { EventEmitter as NodeEventEmitter } from 'events'; import * as vscode from 'vscode'; + // export * from './range'; // export * from './position'; // export * from './selection'; @@ -443,3 +444,114 @@ export enum LogLevel { */ Error = 5, } + +export class TestMessage { + /** + * Human-readable message text to display. + */ + message: string | MarkdownString; + + /** + * Expected test output. If given with {@link TestMessage.actualOutput actualOutput }, a diff view will be shown. + */ + expectedOutput?: string; + + /** + * Actual test output. If given with {@link TestMessage.expectedOutput expectedOutput }, a diff view will be shown. + */ + actualOutput?: string; + + /** + * Associated file location. + */ + location?: vscode.Location; + + /** + * Creates a new TestMessage that will present as a diff in the editor. + * @param message Message to display to the user. + * @param expected Expected output. + * @param actual Actual output. + */ + static diff(message: string | MarkdownString, expected: string, actual: string): TestMessage { + const testMessage = new TestMessage(message); + testMessage.expectedOutput = expected; + testMessage.actualOutput = actual; + return testMessage; + } + + /** + * Creates a new TestMessage instance. + * @param message The message to show to the user. + */ + constructor(message: string | MarkdownString) { + this.message = message; + } +} + +export interface TestItemCollection extends Iterable<[string, vscode.TestItem]> { + /** + * Gets the number of items in the collection. + */ + readonly size: number; + + /** + * Replaces the items stored by the collection. + * @param items Items to store. + */ + replace(items: readonly vscode.TestItem[]): void; + + /** + * Iterate over each entry in this collection. + * + * @param callback Function to execute for each entry. + * @param thisArg The `this` context used when invoking the handler function. + */ + forEach(callback: (item: vscode.TestItem, collection: TestItemCollection) => unknown, thisArg?: unknown): void; + + /** + * Adds the test item to the children. If an item with the same ID already + * exists, it'll be replaced. + * @param item Item to add. + */ + add(item: vscode.TestItem): void; + + /** + * Removes a single test item from the collection. + * @param itemId Item ID to delete. + */ + delete(itemId: string): void; + + /** + * Efficiently gets a test item by ID, if it exists, in the children. + * @param itemId Item ID to get. + * @returns The found item or undefined if it does not exist. + */ + get(itemId: string): vscode.TestItem | undefined; +} + +/** + * Represents a location inside a resource, such as a line + * inside a text file. + */ +export class Location { + /** + * The resource identifier of this location. + */ + uri: vscode.Uri; + + /** + * The document range of this location. + */ + range: vscode.Range; + + /** + * Creates a new location object. + * + * @param uri The resource identifier. + * @param rangeOrPosition The range or position. Positions will be converted to an empty range. + */ + constructor(uri: vscode.Uri, rangeOrPosition: vscode.Range) { + this.uri = uri; + this.range = rangeOrPosition; + } +} diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 12c79a23c7fd..0286235be1bf 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -4,11 +4,17 @@ import * as assert from 'assert'; import { Uri } from 'vscode'; import * as typeMoq from 'typemoq'; +import * as path from 'path'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; -import { DataReceivedEvent, ITestServer } from '../../../../client/testing/testController/common/types'; -import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; +import { ITestServer } from '../../../../client/testing/testController/common/types'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + SpawnOptions, +} from '../../../../client/common/process/types'; import { createDeferred, Deferred } from '../../../../client/common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; suite('pytest test discovery adapter', () => { let testServer: typeMoq.IMock; @@ -18,27 +24,56 @@ suite('pytest test discovery adapter', () => { let execService: typeMoq.IMock; let deferred: Deferred; let outputChannel: typeMoq.IMock; + let portNum: number; + let uuid: string; + let expectedPath: string; + let uri: Uri; + let expectedExtraVariables: Record; setup(() => { + const mockExtensionRootDir = typeMoq.Mock.ofType(); + mockExtensionRootDir.setup((m) => m.toString()).returns(() => '/mocked/extension/root/dir'); + + // constants + portNum = 12345; + uuid = 'uuid123'; + expectedPath = path.join('/', 'my', 'test', 'path'); + uri = Uri.file(expectedPath); + const relativePathToPytest = 'pythonFiles'; + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); + expectedExtraVariables = { + PYTHONPATH: fullPluginPath, + TEST_UUID: uuid, + TEST_PORT: portNum.toString(), + }; + + // set up test server testServer = typeMoq.Mock.ofType(); - testServer.setup((t) => t.getPort()).returns(() => 12345); + testServer.setup((t) => t.getPort()).returns(() => portNum); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); testServer - .setup((t) => t.onDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ }, })); + + // set up config service configService = ({ getSettings: () => ({ testing: { pytestArgs: ['.'] }, }), } as unknown) as IConfigurationService; + + // set up exec factory execFactory = typeMoq.Mock.ofType(); - execService = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) .returns(() => Promise.resolve(execService.object)); + + // set up exec service + execService = typeMoq.Mock.ofType(); deferred = createDeferred(); execService .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) @@ -46,49 +81,51 @@ suite('pytest test discovery adapter', () => { deferred.resolve(); return Promise.resolve({ stdout: '{}' }); }); - execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); outputChannel = typeMoq.Mock.ofType(); }); - test('onDataReceivedHandler should parse only if known UUID', async () => { - const uri = Uri.file('/my/test/path/'); - const uuid = 'uuid123'; - const data = { status: 'success' }; - testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); - const eventData: DataReceivedEvent = { - uuid, - data: JSON.stringify(data), - }; - + test('Discovery should call exec with correct basic args', async () => { adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); - const promise = adapter.discoverTests(uri, execFactory.object); - // const promise = adapter.discoverTests(uri); - await deferred.promise; - adapter.onDataReceivedHandler(eventData); - const result = await promise; - assert.deepStrictEqual(result, data); + await adapter.discoverTests(uri, execFactory.object); + const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; + + execService.verify( + (x) => + x.exec( + expectedArgs, + typeMoq.It.is((options) => { + assert.deepEqual(options.extraVariables, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); }); - test('onDataReceivedHandler should not parse if it is unknown UUID', async () => { - const uri = Uri.file('/my/test/path/'); - const uuid = 'uuid456'; - let data = { status: 'error' }; - testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); - const wrongUriEventData: DataReceivedEvent = { - uuid: 'incorrect-uuid456', - data: JSON.stringify(data), - }; - adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); - const promise = adapter.discoverTests(uri, execFactory.object); - // const promise = adapter.discoverTests(uri); - adapter.onDataReceivedHandler(wrongUriEventData); + test('Test discovery correctly pulls pytest args from config service settings', async () => { + // set up a config service with different pytest args + const configServiceNew: IConfigurationService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.', 'abc', 'xyz'] }, + }), + } as unknown) as IConfigurationService; - data = { status: 'success' }; - const correctUriEventData: DataReceivedEvent = { - uuid, - data: JSON.stringify(data), - }; - adapter.onDataReceivedHandler(correctUriEventData); - const result = await promise; - assert.deepStrictEqual(result, data); + adapter = new PytestTestDiscoveryAdapter(testServer.object, configServiceNew, outputChannel.object); + await adapter.discoverTests(uri, execFactory.object); + const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.', 'abc', 'xyz']; + execService.verify( + (x) => + x.exec( + expectedArgs, + typeMoq.It.is((options) => { + assert.deepEqual(options.extraVariables, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); }); }); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index ac6c6bd274a4..9f4c41ba8309 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -1,90 +1,173 @@ -// /* eslint-disable @typescript-eslint/no-explicit-any */ -// // Copyright (c) Microsoft Corporation. All rights reserved. -// // Licensed under the MIT License. -// import * as assert from 'assert'; -// import { Uri } from 'vscode'; -// import * as typeMoq from 'typemoq'; -// import { IConfigurationService } from '../../../../client/common/types'; -// import { DataReceivedEvent, ITestServer } from '../../../../client/testing/testController/common/types'; -// import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; -// import { createDeferred, Deferred } from '../../../../client/common/utils/async'; -// import { PytestTestExecutionAdapter } from '../../../../client/testing/testController/pytest/pytestExecutionAdapter'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as assert from 'assert'; +import { TestRun, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +import { ITestServer } from '../../../../client/testing/testController/common/types'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + SpawnOptions, +} from '../../../../client/common/process/types'; +import { createDeferred, Deferred } from '../../../../client/common/utils/async'; +import { PytestTestExecutionAdapter } from '../../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; +import * as util from '../../../../client/testing/testController/common/utils'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -// suite('pytest test execution adapter', () => { -// let testServer: typeMoq.IMock; -// let configService: IConfigurationService; -// let execFactory = typeMoq.Mock.ofType(); -// let adapter: PytestTestExecutionAdapter; -// let execService: typeMoq.IMock; -// let deferred: Deferred; -// setup(() => { -// testServer = typeMoq.Mock.ofType(); -// testServer.setup((t) => t.getPort()).returns(() => 12345); -// testServer -// .setup((t) => t.onDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) -// .returns(() => ({ -// dispose: () => { -// /* no-body */ -// }, -// })); -// configService = ({ -// getSettings: () => ({ -// testing: { pytestArgs: ['.'] }, -// }), -// isTestExecution: () => false, -// } as unknown) as IConfigurationService; -// execFactory = typeMoq.Mock.ofType(); -// execService = typeMoq.Mock.ofType(); -// execFactory -// .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) -// .returns(() => Promise.resolve(execService.object)); -// deferred = createDeferred(); -// execService -// .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) -// .returns(() => { -// deferred.resolve(); -// return Promise.resolve({ stdout: '{}' }); -// }); -// execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); -// execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); -// }); -// test('onDataReceivedHandler should parse only if known UUID', async () => { -// const uri = Uri.file('/my/test/path/'); -// const uuid = 'uuid123'; -// const data = { status: 'success' }; -// testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); -// const eventData: DataReceivedEvent = { -// uuid, -// data: JSON.stringify(data), -// }; +suite('pytest test execution adapter', () => { + let testServer: typeMoq.IMock; + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType(); + let adapter: PytestTestExecutionAdapter; + let execService: typeMoq.IMock; + let deferred: Deferred; + let debugLauncher: typeMoq.IMock; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let startTestIdServerStub: sinon.SinonStub; -// adapter = new PytestTestExecutionAdapter(testServer.object, configService); -// const promise = adapter.runTests(uri, [], false); -// await deferred.promise; -// adapter.onDataReceivedHandler(eventData); -// const result = await promise; -// assert.deepStrictEqual(result, data); -// }); -// test('onDataReceivedHandler should not parse if it is unknown UUID', async () => { -// const uri = Uri.file('/my/test/path/'); -// const uuid = 'uuid456'; -// let data = { status: 'error' }; -// testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); -// const wrongUriEventData: DataReceivedEvent = { -// uuid: 'incorrect-uuid456', -// data: JSON.stringify(data), -// }; -// adapter = new PytestTestExecutionAdapter(testServer.object, configService); -// const promise = adapter.runTests(uri, [], false); -// adapter.onDataReceivedHandler(wrongUriEventData); + setup(() => { + testServer = typeMoq.Mock.ofType(); + testServer.setup((t) => t.getPort()).returns(() => 12345); + testServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'] }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + execFactory = typeMoq.Mock.ofType(); + execService = typeMoq.Mock.ofType(); + debugLauncher = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve({ stdout: '{}' }); + }); + debugLauncher + .setup((d) => d.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(); + }); + startTestIdServerStub = sinon.stub(util, 'startTestIdServer').returns(Promise.resolve(54321)); -// data = { status: 'success' }; -// const correctUriEventData: DataReceivedEvent = { -// uuid, -// data: JSON.stringify(data), -// }; -// adapter.onDataReceivedHandler(correctUriEventData); -// const result = await promise; -// assert.deepStrictEqual(result, data); -// }); -// }); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + }); + teardown(() => { + sinon.restore(); + }); + test('startTestIdServer called with correct testIds', async () => { + const uri = Uri.file(myTestPath); + const uuid = 'uuid123'; + testServer + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); + const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); + adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); + + const testIds = ['test1id', 'test2id']; + await adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); + + sinon.assert.calledWithExactly(startTestIdServerStub, testIds); + }); + test('pytest execution called with correct args', async () => { + const uri = Uri.file(myTestPath); + const uuid = 'uuid123'; + testServer + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); + const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); + adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); + await adapter.runTests(uri, [], false, testRun.object, execFactory.object); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); + const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); + const expectedArgs = [pathToPythonScript, '--rootdir', myTestPath]; + const expectedExtraVariables = { + PYTHONPATH: pathToPythonFiles, + TEST_UUID: 'uuid123', + TEST_PORT: '12345', + }; + // execService.verify((x) => x.exec(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); + execService.verify( + (x) => + x.exec( + expectedArgs, + typeMoq.It.is((options) => { + assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.extraVariables?.TEST_UUID, expectedExtraVariables.TEST_UUID); + assert.equal(options.extraVariables?.TEST_PORT, expectedExtraVariables.TEST_PORT); + assert.equal(options.extraVariables?.RUN_TEST_IDS_PORT, '54321'); + assert.equal(options.cwd, uri.fsPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('Debug launched correctly for pytest', async () => { + const uri = Uri.file(myTestPath); + const uuid = 'uuid123'; + testServer + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); + const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); + adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); + await adapter.runTests(uri, [], true, testRun.object, execFactory.object, debugLauncher.object); + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is((launchOptions) => { + assert.equal(launchOptions.cwd, uri.fsPath); + assert.deepEqual(launchOptions.args, ['--rootdir', myTestPath, '--capture', 'no']); + assert.equal(launchOptions.testProvider, 'pytest'); + assert.equal(launchOptions.pytestPort, '12345'); + assert.equal(launchOptions.pytestUUID, 'uuid123'); + assert.strictEqual(launchOptions.runTestIdsPort, '54321'); + return true; + }), + typeMoq.It.isAny(), + ), + typeMoq.Times.once(), + ); + }); +}); diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts new file mode 100644 index 000000000000..57b321c2e36c --- /dev/null +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -0,0 +1,368 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, Uri, TestItem, CancellationToken, TestRun, TestItemCollection, Range } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as sinon from 'sinon'; +import { TestProvider } from '../../../client/testing/types'; +import { + DiscoveredTestNode, + DiscoveredTestPayload, + ExecutionTestPayload, +} from '../../../client/testing/testController/common/types'; +import * as testItemUtilities from '../../../client/testing/testController/common/testItemUtilities'; +import * as ResultResolver from '../../../client/testing/testController/common/resultResolver'; +import * as util from '../../../client/testing/testController/common/utils'; + +suite('Result Resolver tests', () => { + suite('Test discovery', () => { + let resultResolver: ResultResolver.PythonResultResolver; + let testController: TestController; + const log: string[] = []; + let workspaceUri: Uri; + let testProvider: TestProvider; + let defaultErrorMessage: string; + let blankTestItem: TestItem; + let cancelationToken: CancellationToken; + + setup(() => { + testController = ({ + items: { + get: () => { + log.push('get'); + }, + add: () => { + log.push('add'); + }, + replace: () => { + log.push('replace'); + }, + delete: () => { + log.push('delete'); + }, + }, + + dispose: () => { + // empty + }, + } as unknown) as TestController; + defaultErrorMessage = 'pytest test discovery error (see Output > Python)'; + blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + }); + teardown(() => { + sinon.restore(); + }); + + test('resolveDiscovery calls populate test tree correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const tests: DiscoveredTestNode = { + path: 'path', + name: 'name', + type_: 'folder', + id_: 'id', + children: [], + }; + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + // stub out functionality of populateTestTreeStub which is called in resolveDiscovery + const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); + + // call resolve discovery + resultResolver.resolveDiscovery(payload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // header of populateTestTree is (testController: TestController, testTreeData: DiscoveredTestNode, testRoot: TestItem | undefined, resultResolver: ITestResultResolver, token?: CancellationToken) + sinon.assert.calledWithMatch( + populateTestTreeStub, + testController, // testController + tests, // testTreeData + undefined, // testRoot + resultResolver, // resultResolver + cancelationToken, // token + ); + }); + // what about if the error node already exists: this.testController.items.get(`DiscoveryError:${workspacePath}`); + test('resolveDiscovery should create error node on error with correct params', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const errorMessage = 'error msg A'; + const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`; + + // stub out return values of functions called in resolveDiscovery + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + errors: [errorMessage], + }; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + + // stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + + // call resolve discovery + resultResolver.resolveDiscovery(payload); + + // assert the stub functions were called with the correct parameters + + // header of buildErrorNodeOptions is (uri: Uri, message: string, testType: string) + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider); + // header of createErrorTestItem is (options: ErrorTestItemOptions, testController: TestController, uri: Uri) + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + }); + }); + suite('Test execution result resolver', () => { + let resultResolver: ResultResolver.PythonResultResolver; + const log: string[] = []; + let workspaceUri: Uri; + let testProvider: TestProvider; + let cancelationToken: CancellationToken; + let runInstance: typemoq.IMock; + let testControllerMock: typemoq.IMock; + let mockTestItem1: TestItem; + let mockTestItem2: TestItem; + + setup(() => { + // create mock test items + mockTestItem1 = createMockTestItem('mockTestItem1'); + mockTestItem2 = createMockTestItem('mockTestItem2'); + + // create mock testItems to pass into a iterable + const mockTestItems: [string, TestItem][] = [ + ['1', mockTestItem1], + ['2', mockTestItem2], + ]; + const iterableMock = mockTestItems[Symbol.iterator](); + + // create mock testItemCollection + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + let result = iterableMock.next(); + while (!result.done) { + callback(result.value[1]); + result = iterableMock.next(); + } + }) + .returns(() => mockTestItem1); + + // create mock testController + testControllerMock = typemoq.Mock.ofType(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + + // define functions within runInstance + runInstance = typemoq.Mock.ofType(); + runInstance.setup((r) => r.name).returns(() => 'name'); + runInstance.setup((r) => r.token).returns(() => cancelationToken); + runInstance.setup((r) => r.isPersisted).returns(() => true); + runInstance + .setup((r) => r.enqueued(typemoq.It.isAny())) + .returns(() => { + // empty + log.push('enqueue'); + return undefined; + }); + runInstance + .setup((r) => r.started(typemoq.It.isAny())) + .returns(() => { + // empty + log.push('start'); + }); + + // mock getTestCaseNodes to just return the given testNode added + sinon.stub(testItemUtilities, 'getTestCaseNodes').callsFake((testNode: TestItem) => [testNode]); + }); + teardown(() => { + sinon.restore(); + }); + test('resolveExecution handles failed tests correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'failure', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles skipped correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'skipped', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles success correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'success', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles error correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + + const errorPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: 'error', + }; + + resultResolver.resolveExecution(errorPayload, runInstance.object); + + // verify that none of these functions are called + + runInstance.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.never()); + runInstance.verify((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); + runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.never()); + }); + }); +}); + +function createMockTestItem(id: string): TestItem { + const range = new Range(0, 0, 0, 0); + const mockTestItem = ({ + id, + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + range, + uri: Uri.file('/foo/bar'), + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index d7b3a242ee9a..38b71992aefb 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -1,158 +1,349 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as assert from 'assert'; -import * as net from 'net'; -import * as sinon from 'sinon'; -import * as crypto from 'crypto'; -import { OutputChannel, Uri } from 'vscode'; -import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; -import { PythonTestServer } from '../../../client/testing/testController/common/server'; -import { ITestDebugLauncher } from '../../../client/testing/common/types'; -import { createDeferred } from '../../../client/common/utils/async'; - -suite('Python Test Server', () => { - const fakeUuid = 'fake-uuid'; - - let stubExecutionFactory: IPythonExecutionFactory; - let stubExecutionService: IPythonExecutionService; - let server: PythonTestServer; - let sandbox: sinon.SinonSandbox; - let execArgs: string[]; - let v4Stub: sinon.SinonStub; - let debugLauncher: ITestDebugLauncher; - - setup(() => { - sandbox = sinon.createSandbox(); - v4Stub = sandbox.stub(crypto, 'randomUUID'); - - v4Stub.returns(fakeUuid); - stubExecutionService = ({ - exec: (args: string[]) => { - execArgs = args; - return Promise.resolve({ stdout: '', stderr: '' }); - }, - } as unknown) as IPythonExecutionService; - - stubExecutionFactory = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService), - } as unknown) as IPythonExecutionFactory; - }); - - teardown(() => { - sandbox.restore(); - execArgs = []; - server.dispose(); - }); - - test('sendCommand should add the port to the command being sent', async () => { - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - - await server.sendCommand(options); - const port = server.getPort(); - - assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); - }); - - test('sendCommand should write to an output channel if it is provided as an option', async () => { - const output: string[] = []; - const outChannel = { - appendLine: (str: string) => { - output.push(str); - }, - } as OutputChannel; - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - outChannel, - }; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - - await server.sendCommand(options); - - const port = server.getPort(); - const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); - - assert.deepStrictEqual(output, [expected]); - }); - - test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { - let eventData: { status: string; errors: string[] }; - stubExecutionService = ({ - exec: () => { - throw new Error('Failed to execute'); - }, - } as unknown) as IPythonExecutionService; - - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - - server.onDataReceived(({ data }) => { - eventData = JSON.parse(data); - }); - - await server.sendCommand(options); - - assert.deepStrictEqual(eventData!.status, 'error'); - assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); - }); - - test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const deferred = createDeferred(); - - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - stubExecutionService = ({ - exec: async () => { - client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); - }, - } as unknown) as IPythonExecutionService; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('malformed data'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - await server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, ''); - }); -}); +// // Copyright (c) Microsoft Corporation. All rights reserved. +// // Licensed under the MIT License. + +// import * as assert from 'assert'; +// import * as net from 'net'; +// import * as sinon from 'sinon'; +// import * as crypto from 'crypto'; +// import { Uri } from 'vscode'; +// import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; +// import { PythonTestServer } from '../../../client/testing/testController/common/server'; +// import { ITestDebugLauncher } from '../../../client/testing/common/types'; +// import { createDeferred } from '../../../client/common/utils/async'; + +// suite('Python Test Server', () => { +// const fakeUuid = 'fake-uuid'; + +// let stubExecutionFactory: IPythonExecutionFactory; +// let stubExecutionService: IPythonExecutionService; +// let server: PythonTestServer; +// let sandbox: sinon.SinonSandbox; +// let execArgs: string[]; +// let v4Stub: sinon.SinonStub; +// let debugLauncher: ITestDebugLauncher; + +// setup(() => { +// sandbox = sinon.createSandbox(); +// v4Stub = sandbox.stub(crypto, 'randomUUID'); + +// v4Stub.returns(fakeUuid); +// stubExecutionService = ({ +// exec: (args: string[]) => { +// execArgs = args; +// return Promise.resolve({ stdout: '', stderr: '' }); +// }, +// } as unknown) as IPythonExecutionService; + +// stubExecutionFactory = ({ +// createActivatedEnvironment: () => Promise.resolve(stubExecutionService), +// } as unknown) as IPythonExecutionFactory; +// }); + +// teardown(() => { +// sandbox.restore(); +// execArgs = []; +// server.dispose(); +// }); + +// // test('sendCommand should add the port to the command being sent', async () => { +// // const options = { +// // command: { script: 'myscript', args: ['-foo', 'foo'] }, +// // workspaceFolder: Uri.file('/foo/bar'), +// // cwd: '/foo/bar', +// // uuid: fakeUuid, +// // }; + +// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// // await server.serverReady(); + +// // await server.sendCommand(options); +// // const port = server.getPort(); + +// // assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); +// // }); + +// // test('sendCommand should write to an output channel if it is provided as an option', async () => { +// // const output: string[] = []; +// // const outChannel = { +// // appendLine: (str: string) => { +// // output.push(str); +// // }, +// // } as OutputChannel; +// // const options = { +// // command: { script: 'myscript', args: ['-foo', 'foo'] }, +// // workspaceFolder: Uri.file('/foo/bar'), +// // cwd: '/foo/bar', +// // uuid: fakeUuid, +// // outChannel, +// // }; + +// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// // await server.serverReady(); + +// // await server.sendCommand(options); + +// // const port = server.getPort(); +// // const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); + +// // assert.deepStrictEqual(output, [expected]); +// // }); + +// // test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { +// // let eventData: { status: string; errors: string[] }; +// // stubExecutionService = ({ +// // exec: () => { +// // throw new Error('Failed to execute'); +// // }, +// // } as unknown) as IPythonExecutionService; + +// // const options = { +// // command: { script: 'myscript', args: ['-foo', 'foo'] }, +// // workspaceFolder: Uri.file('/foo/bar'), +// // cwd: '/foo/bar', +// // uuid: fakeUuid, +// // }; + +// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// // await server.serverReady(); + +// // server.onDataReceived(({ data }) => { +// // eventData = JSON.parse(data); +// // }); + +// // await server.sendCommand(options); + +// // assert.deepStrictEqual(eventData!.status, 'error'); +// // assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); +// // }); + +// // test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { +// // let eventData: string | undefined; +// // const client = new net.Socket(); +// // const deferred = createDeferred(); + +// // const options = { +// // command: { script: 'myscript', args: ['-foo', 'foo'] }, +// // workspaceFolder: Uri.file('/foo/bar'), +// // cwd: '/foo/bar', +// // uuid: fakeUuid, +// // }; + +// // stubExecutionService = ({ +// // exec: async () => { +// // client.connect(server.getPort()); +// // return Promise.resolve({ stdout: '', stderr: '' }); +// // }, +// // } as unknown) as IPythonExecutionService; + +// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// // await server.serverReady(); +// // server.onDataReceived(({ data }) => { +// // eventData = data; +// // deferred.resolve(); +// // }); + +// // client.on('connect', () => { +// // console.log('Socket connected, local port:', client.localPort); +// // client.write('malformed data'); +// // client.end(); +// // }); +// // client.on('error', (error) => { +// // console.log('Socket connection error:', error); +// // }); + +// // await server.sendCommand(options); +// // await deferred.promise; +// // assert.deepStrictEqual(eventData, ''); +// // }); + +// // test('If the server doesnt recognize the UUID it should ignore it', async () => { +// // let eventData: string | undefined; +// // const client = new net.Socket(); +// // const deferred = createDeferred(); + +// // const options = { +// // command: { script: 'myscript', args: ['-foo', 'foo'] }, +// // workspaceFolder: Uri.file('/foo/bar'), +// // cwd: '/foo/bar', +// // uuid: fakeUuid, +// // }; + +// // stubExecutionService = ({ +// // exec: async () => { +// // client.connect(server.getPort()); +// // return Promise.resolve({ stdout: '', stderr: '' }); +// // }, +// // } as unknown) as IPythonExecutionService; + +// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// // await server.serverReady(); +// // server.onDataReceived(({ data }) => { +// // eventData = data; +// // deferred.resolve(); +// // }); + +// // client.on('connect', () => { +// // console.log('Socket connected, local port:', client.localPort); +// // client.write('{"Request-uuid": "unknown-uuid"}'); +// // client.end(); +// // }); +// // client.on('error', (error) => { +// // console.log('Socket connection error:', error); +// // }); + +// // await server.sendCommand(options); +// // await deferred.promise; +// // assert.deepStrictEqual(eventData, ''); +// // }); + +// // required to have "tests" or "results" +// // the heading length not being equal and yes being equal +// // multiple payloads +// // test('Error if payload does not have a content length header', async () => { +// // let eventData: string | undefined; +// // const client = new net.Socket(); +// // const deferred = createDeferred(); + +// // const options = { +// // command: { script: 'myscript', args: ['-foo', 'foo'] }, +// // workspaceFolder: Uri.file('/foo/bar'), +// // cwd: '/foo/bar', +// // uuid: fakeUuid, +// // }; + +// // stubExecutionService = ({ +// // exec: async () => { +// // client.connect(server.getPort()); +// // return Promise.resolve({ stdout: '', stderr: '' }); +// // }, +// // } as unknown) as IPythonExecutionService; + +// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// // await server.serverReady(); +// // server.onDataReceived(({ data }) => { +// // eventData = data; +// // deferred.resolve(); +// // }); + +// // client.on('connect', () => { +// // console.log('Socket connected, local port:', client.localPort); +// // client.write('{"not content length": "5"}'); +// // client.end(); +// // }); +// // client.on('error', (error) => { +// // console.log('Socket connection error:', error); +// // }); + +// // await server.sendCommand(options); +// // await deferred.promise; +// // assert.deepStrictEqual(eventData, ''); +// // }); + +// const testData = [ +// { +// testName: 'fires discovery correctly on test payload', +// payload: `Content-Length: 52 +// Content-Type: application/json +// Request-uuid: UUID_HERE + +// {"cwd": "path", "status": "success", "tests": "xyz"}`, +// expectedResult: '{"cwd": "path", "status": "success", "tests": "xyz"}', +// }, +// // Add more test data as needed +// ]; + +// testData.forEach(({ testName, payload, expectedResult }) => { +// test(`test: ${testName}`, async () => { +// // Your test logic here +// let eventData: string | undefined; +// const client = new net.Socket(); +// const deferred = createDeferred(); + +// const options = { +// command: { script: 'myscript', args: ['-foo', 'foo'] }, +// workspaceFolder: Uri.file('/foo/bar'), +// cwd: '/foo/bar', +// uuid: fakeUuid, +// }; + +// stubExecutionService = ({ +// exec: async () => { +// client.connect(server.getPort()); +// return Promise.resolve({ stdout: '', stderr: '' }); +// }, +// } as unknown) as IPythonExecutionService; + +// server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// await server.serverReady(); +// const uuid = server.createUUID(); +// payload = payload.replace('UUID_HERE', uuid); +// server.onDiscoveryDataReceived(({ data }) => { +// eventData = data; +// deferred.resolve(); +// }); + +// client.on('connect', () => { +// console.log('Socket connected, local port:', client.localPort); +// client.write(payload); +// client.end(); +// }); +// client.on('error', (error) => { +// console.log('Socket connection error:', error); +// }); + +// await server.sendCommand(options); +// await deferred.promise; +// assert.deepStrictEqual(eventData, expectedResult); +// }); +// }); + +// test('Calls run resolver if the result header is in the payload', async () => { +// let eventData: string | undefined; +// const client = new net.Socket(); +// const deferred = createDeferred(); + +// const options = { +// command: { script: 'myscript', args: ['-foo', 'foo'] }, +// workspaceFolder: Uri.file('/foo/bar'), +// cwd: '/foo/bar', +// uuid: fakeUuid, +// }; + +// stubExecutionService = ({ +// exec: async () => { +// client.connect(server.getPort()); +// return Promise.resolve({ stdout: '', stderr: '' }); +// }, +// } as unknown) as IPythonExecutionService; + +// server = new PythonTestServer(stubExecutionFactory, debugLauncher); +// await server.serverReady(); +// const uuid = server.createUUID(); +// server.onRunDataReceived(({ data }) => { +// eventData = data; +// deferred.resolve(); +// }); + +// const payload = `Content-Length: 87 +// Content-Type: application/json +// Request-uuid: ${uuid} + +// {"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}`; + +// client.on('connect', () => { +// console.log('Socket connected, local port:', client.localPort); +// client.write(payload); +// client.end(); +// }); +// client.on('error', (error) => { +// console.log('Socket connection error:', error); +// }); + +// await server.sendCommand(options); +// await deferred.promise; +// console.log('event data', eventData); +// const expectedResult = +// '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; +// assert.deepStrictEqual(eventData, expectedResult); +// }); +// }); diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index 3d3521291f74..ef21655e93e4 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -23,7 +23,7 @@ suite('Unittest test discovery adapter', () => { outputChannel = typemoq.Mock.ofType(); }); - test('discoverTests should send the discovery command to the test server', async () => { + test('DiscoverTests should send the discovery command to the test server with the correct args', async () => { let options: TestCommandOptions | undefined; const stubTestServer = ({ @@ -32,7 +32,7 @@ suite('Unittest test discovery adapter', () => { options = opt; return Promise.resolve(); }, - onDataReceived: () => { + onDiscoveryDataReceived: () => { // no body }, createUUID: () => '123456789', @@ -47,61 +47,11 @@ suite('Unittest test discovery adapter', () => { assert.deepStrictEqual(options, { workspaceFolder: uri, cwd: uri.fsPath, - command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, + command: { + script, + args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'], + }, uuid: '123456789', }); }); - - test("onDataReceivedHandler should parse the data if the cwd from the payload matches the test adapter's cwd", async () => { - const stubTestServer = ({ - sendCommand(): Promise { - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => '123456789', - } as unknown) as ITestServer; - - const uri = Uri.file('/foo/bar'); - const data = { status: 'success' }; - const uuid = '123456789'; - const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - const promise = adapter.discoverTests(uri); - - adapter.onDataReceivedHandler({ uuid, data: JSON.stringify(data) }); - - const result = await promise; - - assert.deepStrictEqual(result, data); - }); - - test("onDataReceivedHandler should ignore the data if the cwd from the payload does not match the test adapter's cwd", async () => { - const correctUuid = '123456789'; - const incorrectUuid = '987654321'; - const stubTestServer = ({ - sendCommand(): Promise { - return Promise.resolve(); - }, - onDataReceived: () => { - // no body - }, - createUUID: () => correctUuid, - } as unknown) as ITestServer; - - const uri = Uri.file('/foo/bar'); - - const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - const promise = adapter.discoverTests(uri); - - const data = { status: 'success' }; - adapter.onDataReceivedHandler({ uuid: incorrectUuid, data: JSON.stringify(data) }); - - const nextData = { status: 'error' }; - adapter.onDataReceivedHandler({ uuid: correctUuid, data: JSON.stringify(nextData) }); - - const result = await promise; - - assert.deepStrictEqual(result, nextData); - }); }); diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index e5495629bf28..88126225a177 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -1,118 +1,65 @@ -// // Copyright (c) Microsoft Corporation. All rights reserved. -// // Licensed under the MIT License. - -// import * as assert from 'assert'; -// import * as path from 'path'; -// import * as typemoq from 'typemoq'; -// import { Uri } from 'vscode'; -// import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; -// import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -// import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; -// import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; - -// suite('Unittest test execution adapter', () => { -// let stubConfigSettings: IConfigurationService; -// let outputChannel: typemoq.IMock; - -// setup(() => { -// stubConfigSettings = ({ -// getSettings: () => ({ -// testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, -// }), -// } as unknown) as IConfigurationService; -// outputChannel = typemoq.Mock.ofType(); -// }); - -// test('runTests should send the run command to the test server', async () => { -// let options: TestCommandOptions | undefined; - -// const stubTestServer = ({ -// sendCommand(opt: TestCommandOptions, runTestIdPort?: string): Promise { -// delete opt.outChannel; -// options = opt; -// assert(runTestIdPort !== undefined); -// return Promise.resolve(); -// }, -// onDataReceived: () => { -// // no body -// }, -// createUUID: () => '123456789', -// } as unknown) as ITestServer; - -// const uri = Uri.file('/foo/bar'); -// const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); - -// const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); -// adapter.runTests(uri, [], false).then(() => { -// const expectedOptions: TestCommandOptions = { -// workspaceFolder: uri, -// command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, -// cwd: uri.fsPath, -// uuid: '123456789', -// debugBool: false, -// testIds: [], -// }; -// assert.deepStrictEqual(options, expectedOptions); -// }); -// }); -// test("onDataReceivedHandler should parse the data if the cwd from the payload matches the test adapter's cwd", async () => { -// const stubTestServer = ({ -// sendCommand(): Promise { -// return Promise.resolve(); -// }, -// onDataReceived: () => { -// // no body -// }, -// createUUID: () => '123456789', -// } as unknown) as ITestServer; - -// const uri = Uri.file('/foo/bar'); -// const data = { status: 'success' }; -// const uuid = '123456789'; - -// const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - -// // triggers runTests flow which will run onDataReceivedHandler and the -// // promise resolves into the parsed data. -// const promise = adapter.runTests(uri, [], false); - -// adapter.onDataReceivedHandler({ uuid, data: JSON.stringify(data) }); - -// const result = await promise; - -// assert.deepStrictEqual(result, data); -// }); -// test("onDataReceivedHandler should ignore the data if the cwd from the payload does not match the test adapter's cwd", async () => { -// const correctUuid = '123456789'; -// const incorrectUuid = '987654321'; -// const stubTestServer = ({ -// sendCommand(): Promise { -// return Promise.resolve(); -// }, -// onDataReceived: () => { -// // no body -// }, -// createUUID: () => correctUuid, -// } as unknown) as ITestServer; - -// const uri = Uri.file('/foo/bar'); - -// const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); - -// // triggers runTests flow which will run onDataReceivedHandler and the -// // promise resolves into the parsed data. -// const promise = adapter.runTests(uri, [], false); - -// const data = { status: 'success' }; -// // will not resolve due to incorrect UUID -// adapter.onDataReceivedHandler({ uuid: incorrectUuid, data: JSON.stringify(data) }); - -// const nextData = { status: 'error' }; -// // will resolve and nextData will be returned as result -// adapter.onDataReceivedHandler({ uuid: correctUuid, data: JSON.stringify(nextData) }); - -// const result = await promise; - -// assert.deepStrictEqual(result, nextData); -// }); -// }); +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import * as sinon from 'sinon'; +import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; +import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; +import * as util from '../../../../client/testing/testController/common/utils'; + +suite('Unittest test execution adapter', () => { + let stubConfigSettings: IConfigurationService; + let outputChannel: typemoq.IMock; + + setup(() => { + stubConfigSettings = ({ + getSettings: () => ({ + testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, + }), + } as unknown) as IConfigurationService; + outputChannel = typemoq.Mock.ofType(); + sinon.stub(util, 'startTestIdServer').returns(Promise.resolve(54321)); + }); + teardown(() => { + sinon.restore(); + }); + + test('runTests should send the run command to the test server', async () => { + let options: TestCommandOptions | undefined; + + const stubTestServer = ({ + sendCommand(opt: TestCommandOptions, runTestIdPort?: string): Promise { + delete opt.outChannel; + options = opt; + assert(runTestIdPort !== undefined); + return Promise.resolve(); + }, + onRunDataReceived: () => { + // no body + }, + createUUID: () => '123456789', + } as unknown) as ITestServer; + + const uri = Uri.file('/foo/bar'); + const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); + + const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); + const testIds = ['test1id', 'test2id']; + adapter.runTests(uri, testIds, false).then(() => { + const expectedOptions: TestCommandOptions = { + workspaceFolder: uri, + command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, + cwd: uri.fsPath, + uuid: '123456789', + debugBool: false, + testIds, + }; + assert.deepStrictEqual(options, expectedOptions); + }); + }); +}); diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts index 539647aece9f..5a2e48130746 100644 --- a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -5,19 +5,23 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; -import { TestController, TestItem, Uri } from 'vscode'; +import { TestController, TestItem, TestItemCollection, TestRun, Uri } from 'vscode'; import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; // 7/7 import { WorkspaceTestAdapter } from '../../../client/testing/testController/workspaceTestAdapter'; import * as Telemetry from '../../../client/telemetry'; import { EventName } from '../../../client/telemetry/constants'; -import { ITestServer } from '../../../client/testing/testController/common/types'; +import { ITestResultResolver, ITestServer } from '../../../client/testing/testController/common/types'; +import * as testItemUtilities from '../../../client/testing/testController/common/testItemUtilities'; +import * as util from '../../../client/testing/testController/common/utils'; +import * as ResultResolver from '../../../client/testing/testController/common/resultResolver'; suite('Workspace test adapter', () => { suite('Test discovery', () => { let stubTestServer: ITestServer; let stubConfigSettings: IConfigurationService; + let stubResultResolver: ITestResultResolver; let discoverTestsStub: sinon.SinonStub; let sendTelemetryStub: sinon.SinonStub; @@ -29,8 +33,6 @@ suite('Workspace test adapter', () => { let testController: TestController; let log: string[] = []; - const sandbox = sinon.createSandbox(); - setup(() => { stubConfigSettings = ({ getSettings: () => ({ @@ -47,6 +49,19 @@ suite('Workspace test adapter', () => { }, } as unknown) as ITestServer; + stubResultResolver = ({ + resolveDiscovery: () => { + // no body + }, + resolveExecution: () => { + // no body + }, + } as unknown) as ITestResultResolver; + + // const vsIdToRunIdGetStub = sinon.stub(stubResultResolver.vsIdToRunId, 'get'); + // const expectedRunId = 'expectedRunId'; + // vsIdToRunIdGetStub.withArgs(sinon.match.any).returns(expectedRunId); + // For some reason the 'tests' namespace in vscode returns undefined. // While I figure out how to expose to the tests, they will run // against a stub test controller and stub test items. @@ -97,8 +112,8 @@ suite('Workspace test adapter', () => { }); }; - discoverTestsStub = sandbox.stub(UnittestTestDiscoveryAdapter.prototype, 'discoverTests'); - sendTelemetryStub = sandbox.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + discoverTestsStub = sinon.stub(UnittestTestDiscoveryAdapter.prototype, 'discoverTests'); + sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); outputChannel = typemoq.Mock.ofType(); }); @@ -106,7 +121,54 @@ suite('Workspace test adapter', () => { telemetryEvent = []; log = []; testController.dispose(); - sandbox.restore(); + sinon.restore(); + }); + + test('If discovery failed correctly create error node', async () => { + discoverTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + const blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const testProvider = 'unittest'; + + const abc = await workspaceTestAdapter.discoverTests(testController); + console.log(abc); + + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider); }); test("When discovering tests, the workspace test adapter should call the test discovery adapter's discoverTest method", async () => { @@ -127,6 +189,7 @@ suite('Workspace test adapter', () => { testDiscoveryAdapter, testExecutionAdapter, Uri.parse('foo'), + stubResultResolver, ); await workspaceTestAdapter.discoverTests(testController); @@ -160,6 +223,7 @@ suite('Workspace test adapter', () => { testDiscoveryAdapter, testExecutionAdapter, Uri.parse('foo'), + stubResultResolver, ); // Try running discovery twice @@ -190,6 +254,7 @@ suite('Workspace test adapter', () => { testDiscoveryAdapter, testExecutionAdapter, Uri.parse('foo'), + stubResultResolver, ); await workspaceTestAdapter.discoverTests(testController); @@ -220,6 +285,7 @@ suite('Workspace test adapter', () => { testDiscoveryAdapter, testExecutionAdapter, Uri.parse('foo'), + stubResultResolver, ); await workspaceTestAdapter.discoverTests(testController); @@ -229,18 +295,322 @@ suite('Workspace test adapter', () => { const lastEvent = telemetryEvent[1]; assert.ok(lastEvent.properties.failed); + }); + }); + suite('Test execution workspace test adapter', () => { + let stubTestServer: ITestServer; + let stubConfigSettings: IConfigurationService; + let stubResultResolver: ITestResultResolver; + let executionTestsStub: sinon.SinonStub; + let sendTelemetryStub: sinon.SinonStub; + let outputChannel: typemoq.IMock; + let runInstance: typemoq.IMock; + let testControllerMock: typemoq.IMock; + let telemetryEvent: { eventName: EventName; properties: Record }[] = []; + let resultResolver: ResultResolver.PythonResultResolver; + + // Stubbed test controller (see comment around L.40) + let testController: TestController; + let log: string[] = []; + + const sandbox = sinon.createSandbox(); + + setup(() => { + stubConfigSettings = ({ + getSettings: () => ({ + testing: { unittestArgs: ['--foo'] }, + }), + } as unknown) as IConfigurationService; + + stubTestServer = ({ + sendCommand(): Promise { + return Promise.resolve(); + }, + onDataReceived: () => { + // no body + }, + } as unknown) as ITestServer; - assert.deepStrictEqual(log, ['createTestItem', 'add']); + stubResultResolver = ({ + resolveDiscovery: () => { + // no body + }, + resolveExecution: () => { + // no body + }, + vsIdToRunId: { + get: sinon.stub().returns('expectedRunId'), + }, + } as unknown) as ITestResultResolver; + const testItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + + testController = ({ + items: { + get: () => { + log.push('get'); + }, + add: () => { + log.push('add'); + }, + replace: () => { + log.push('replace'); + }, + delete: () => { + log.push('delete'); + }, + }, + createTestItem: () => { + log.push('createTestItem'); + return testItem; + }, + dispose: () => { + // empty + }, + } as unknown) as TestController; + + const mockSendTelemetryEvent = ( + eventName: EventName, + _: number | Record | undefined, + properties: unknown, + ) => { + telemetryEvent.push({ + eventName, + properties: properties as Record, + }); + }; + + executionTestsStub = sandbox.stub(UnittestTestExecutionAdapter.prototype, 'runTests'); + sendTelemetryStub = sandbox.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + outputChannel = typemoq.Mock.ofType(); + runInstance = typemoq.Mock.ofType(); + + const testProvider = 'pytest'; + const workspaceUri = Uri.file('foo'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); }); - /** - * TODO To test: - * - successful discovery but no data: delete everything from the test controller - * - successful discovery with error status: add error node to tree - * - single root: populate tree if there's no root node - * - single root: update tree if there's a root node - * - single root: delete tree if there are no tests in the test data - * - multiroot: update the correct folders - */ + teardown(() => { + telemetryEvent = []; + log = []; + testController.dispose(); + sandbox.restore(); + }); + test('When executing tests, the right tests should be sent to be executed', async () => { + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + resultResolver, + ); + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + + sinon.stub(testItemUtilities, 'getTestCaseNodes').callsFake((testNode: TestItem) => + // Custom implementation logic here based on the provided testNode and collection + + // Example implementation: returning a predefined array of TestItem objects + [testNode], + ); + + const mockTestItem1 = createMockTestItem('mockTestItem1'); + const mockTestItem2 = createMockTestItem('mockTestItem2'); + const mockTestItems: [string, TestItem][] = [ + ['1', mockTestItem1], + ['2', mockTestItem2], + // Add as many mock TestItems as needed + ]; + const iterableMock = mockTestItems[Symbol.iterator](); + + const testItemCollectionMock = typemoq.Mock.ofType(); + + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + let result = iterableMock.next(); + while (!result.done) { + callback(result.value[1]); + result = iterableMock.next(); + } + }) + .returns(() => mockTestItem1); + testControllerMock = typemoq.Mock.ofType(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + await workspaceTestAdapter.executeTests(testController, runInstance.object, [mockTestItem1, mockTestItem2]); + + runInstance.verify((r) => r.started(typemoq.It.isAny()), typemoq.Times.exactly(2)); + }); + + test("When executing tests, the workspace test adapter should call the test execute adapter's executionTest method", async () => { + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.executeTests(testController, runInstance.object, []); + + sinon.assert.calledOnce(executionTestsStub); + }); + + test('If execution is already running, do not call executionAdapter.runTests again', async () => { + executionTestsStub.callsFake( + async () => + new Promise((resolve) => { + setTimeout(() => { + // Simulate time taken by discovery. + resolve(); + }, 2000); + }), + ); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + // Try running discovery twice + const one = workspaceTestAdapter.executeTests(testController, runInstance.object, []); + const two = workspaceTestAdapter.executeTests(testController, runInstance.object, []); + + Promise.all([one, two]); + + sinon.assert.calledOnce(executionTestsStub); + }); + + test('If execution failed correctly create error node', async () => { + executionTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + const blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const testProvider = 'unittest'; + + await workspaceTestAdapter.executeTests(testController, runInstance.object, []); + + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider); + }); + + test('If execution failed, send a telemetry event with the "failed" key set to true, and add an error node to the test controller', async () => { + executionTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.executeTests(testController, runInstance.object, []); + + sinon.assert.calledWith(sendTelemetryStub, EventName.UNITTEST_RUN_ALL_FAILED); + assert.strictEqual(telemetryEvent.length, 1); + }); }); }); + +function createMockTestItem(id: string): TestItem { + const range = typemoq.Mock.ofType(); + const mockTestItem = ({ + id, + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + range, + uri: Uri.file('/foo/bar'), + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index ebbe7ca59e72..44518e7575a7 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -70,6 +70,8 @@ mockedVSCode.Hover = vscodeMocks.Hover; mockedVSCode.Disposable = vscodeMocks.Disposable as any; mockedVSCode.ExtensionKind = vscodeMocks.ExtensionKind; mockedVSCode.CodeAction = vscodeMocks.CodeAction; +mockedVSCode.TestMessage = vscodeMocks.TestMessage; +mockedVSCode.Location = vscodeMocks.Location; mockedVSCode.EventEmitter = vscodeMocks.EventEmitter; mockedVSCode.CancellationTokenSource = vscodeMocks.CancellationTokenSource; mockedVSCode.CompletionItemKind = vscodeMocks.CompletionItemKind; From acd12995eacfd52577a5aedf494e1cd163433476 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Wed, 21 Jun 2023 14:07:29 -0700 Subject: [PATCH 0043/1136] Fix error checking python version (#21464) Closed: https://github.com/microsoft/vscode-python/issues/19482 --- src/client/debugger/extension/adapter/factory.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/debugger/extension/adapter/factory.ts b/src/client/debugger/extension/adapter/factory.ts index fc9232729eae..067c2e405ea0 100644 --- a/src/client/debugger/extension/adapter/factory.ts +++ b/src/client/debugger/extension/adapter/factory.ts @@ -183,7 +183,10 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac private async getExecutableCommand(interpreter: PythonEnvironment | undefined): Promise { if (interpreter) { - if ((interpreter.version?.major ?? 0) < 3 || (interpreter.version?.minor ?? 0) <= 6) { + if ( + (interpreter.version?.major ?? 0) < 3 || + ((interpreter.version?.major ?? 0) <= 3 && (interpreter.version?.minor ?? 0) <= 6) + ) { this.showDeprecatedPythonMessage(); } return interpreter.path.length > 0 ? [interpreter.path] : []; From 2e936ef93b686fc7d1d7054fd1707798a0037606 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Thu, 22 Jun 2023 09:48:28 -0700 Subject: [PATCH 0044/1136] Adopt lifecycle managed by parent (#21467) closed: https://github.com/microsoft/vscode-python/issues/20376 --- src/client/common/application/types.ts | 3 ++- .../extension/hooks/childProcessAttachService.ts | 14 ++++++++++---- .../hooks/childProcessAttachService.unit.test.ts | 8 ++++---- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 72cd357d62d8..fa2ced6c45da 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -16,6 +16,7 @@ import { DebugConsole, DebugSession, DebugSessionCustomEvent, + DebugSessionOptions, DecorationRenderOptions, Disposable, DocumentSelector, @@ -975,7 +976,7 @@ export interface IDebugService { startDebugging( folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, - parentSession?: DebugSession, + parentSession?: DebugSession | DebugSessionOptions, ): Thenable; /** diff --git a/src/client/debugger/extension/hooks/childProcessAttachService.ts b/src/client/debugger/extension/hooks/childProcessAttachService.ts index c6556a62eaa1..08f44bc3cea5 100644 --- a/src/client/debugger/extension/hooks/childProcessAttachService.ts +++ b/src/client/debugger/extension/hooks/childProcessAttachService.ts @@ -5,7 +5,7 @@ import { inject, injectable } from 'inversify'; import { IDebugService } from '../../../common/application/types'; -import { DebugConfiguration, DebugSession, l10n, WorkspaceFolder } from 'vscode'; +import { DebugConfiguration, DebugSession, l10n, WorkspaceFolder, DebugSessionOptions } from 'vscode'; import { noop } from '../../../common/utils/misc'; import { captureTelemetry } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; @@ -28,11 +28,17 @@ export class ChildProcessAttachService implements IChildProcessAttachService { @captureTelemetry(EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS) public async attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise { const debugConfig: AttachRequestArguments & DebugConfiguration = data; - const processId = debugConfig.subProcessId!; const folder = this.getRelatedWorkspaceFolder(debugConfig); - const launched = await this.debugService.startDebugging(folder, debugConfig, parentSession); + const debugSessionOption: DebugSessionOptions = { + parentSession: parentSession, + lifecycleManagedByParent: true, + }; + const launched = await this.debugService.startDebugging(folder, debugConfig, debugSessionOption); if (!launched) { - showErrorMessage(l10n.t('Failed to launch debugger for child process {0}', processId)).then(noop, noop); + showErrorMessage(l10n.t('Failed to launch debugger for child process {0}', debugConfig.subProcessId!)).then( + noop, + noop, + ); } } diff --git a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts index 2ab9d3e30d2c..118efe416e94 100644 --- a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts +++ b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts @@ -117,7 +117,7 @@ suite('Debug - Attach to Child Process', () => { verify(debugService.startDebugging(undefined, anything(), anything())).once(); sinon.assert.notCalled(showErrorMessageStub); }); - test('Validate debug config is passed as is', async () => { + test('Validate debug config is passed with the correct params', async () => { const data: LaunchRequestArguments | AttachRequestArguments = { request: 'attach', type: 'python', @@ -140,7 +140,7 @@ suite('Debug - Attach to Child Process', () => { verify(debugService.startDebugging(undefined, anything(), anything())).once(); const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); - expect(thirdArg).to.deep.equal(session); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); sinon.assert.notCalled(showErrorMessageStub); }); test('Pass data as is if data is attach debug configuration', async () => { @@ -161,7 +161,7 @@ suite('Debug - Attach to Child Process', () => { verify(debugService.startDebugging(undefined, anything(), anything())).once(); const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); - expect(thirdArg).to.deep.equal(session); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); sinon.assert.notCalled(showErrorMessageStub); }); test('Validate debug config when parent/root parent was attached', async () => { @@ -189,7 +189,7 @@ suite('Debug - Attach to Child Process', () => { verify(debugService.startDebugging(undefined, anything(), anything())).once(); const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); - expect(thirdArg).to.deep.equal(session); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); sinon.assert.notCalled(showErrorMessageStub); }); }); From 55cd2e041efee42ace3460fb6b2533d299b171ff Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 22 Jun 2023 11:09:21 -0700 Subject: [PATCH 0045/1136] add test for import pytest error (#21472) fixes https://github.com/microsoft/vscode-python/issues/21470 --- .../.data/error_pytest_import.txt | 6 ++++ .../tests/pytestadapter/test_discovery.py | 30 +++++++++++++++++++ pythonFiles/vscode_pytest/__init__.py | 2 +- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 pythonFiles/tests/pytestadapter/.data/error_pytest_import.txt diff --git a/pythonFiles/tests/pytestadapter/.data/error_pytest_import.txt b/pythonFiles/tests/pytestadapter/.data/error_pytest_import.txt new file mode 100644 index 000000000000..7d65dee2ccc6 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/error_pytest_import.txt @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +@pytest.mark.parametrize("num", range(1, 89)) +def test_odd_even(num): + assert True diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 8c0f8dbae68c..3cbaa270cab9 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -10,6 +10,36 @@ from .helpers import TEST_DATA_PATH, runner +def test_import_error(tmp_path): + """Test pytest discovery on a file that has a pytest marker but does not import pytest. + + Copies the contents of a .txt file to a .py file in the temporary directory + to then run pytest discovery on. + + The json should still be returned but the errors list should be present. + + Keyword arguments: + tmp_path -- pytest fixture that creates a temporary directory. + """ + # Saving some files as .txt to avoid that file displaying a syntax error for + # the extension as a whole. Instead, rename it before running this test + # in order to test the error handling. + file_path = TEST_DATA_PATH / "error_pytest_import.txt" + temp_dir = tmp_path / "temp_data" + temp_dir.mkdir() + p = temp_dir / "error_pytest_import.py" + shutil.copyfile(file_path, p) + actual_list: Optional[List[Dict[str, Any]]] = runner( + ["--collect-only", os.fspath(p)] + ) + assert actual_list + for actual in actual_list: + assert all(item in actual for item in ("status", "cwd", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["error"]) == 2 + + def test_syntax_error(tmp_path): """Test pytest discovery on a file that has a syntax error. diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index f9f147c381a9..726dba8c9a97 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -79,7 +79,7 @@ def pytest_keyboard_interrupt(excinfo): Keyword arguments: excinfo -- the exception information of type ExceptionInfo. """ - # The function execonly() returns the exception as a string. + # The function exconly() returns the exception as a string. ERRORS.append(excinfo.exconly()) From 8c86417c55ff4317c9173d34600f4fc8885c58b2 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 23 Jun 2023 15:12:00 -0700 Subject: [PATCH 0046/1136] Add dynamic result (#21466) fixes, https://github.com/microsoft/vscode-python/issues/21148 and https://github.com/microsoft/vscode-python/issues/21149 --- pythonFiles/tests/pytestadapter/helpers.py | 69 ++++++++++++------- .../tests/pytestadapter/test_discovery.py | 24 +++---- .../tests/pytestadapter/test_execution.py | 42 +++++------ .../tests/unittestadapter/test_execution.py | 5 ++ pythonFiles/unittestadapter/discovery.py | 4 +- pythonFiles/unittestadapter/execution.py | 67 ++++++++++-------- pythonFiles/vscode_pytest/__init__.py | 66 +++++++++++++----- .../testController/common/resultResolver.ts | 19 +++-- .../testing/testController/common/types.ts | 2 +- .../pytest/pytestDiscoveryAdapter.ts | 5 +- .../pytest/pytestExecutionAdapter.ts | 19 +++-- .../unittest/testDiscoveryAdapter.ts | 15 ++-- .../unittest/testExecutionAdapter.ts | 21 +++--- .../resultResolver.unit.test.ts | 2 +- 14 files changed, 215 insertions(+), 145 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index 013e4bb31fca..c3e01d52170a 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + import io import json import os @@ -9,7 +10,7 @@ import sys import threading import uuid -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" from typing_extensions import TypedDict @@ -72,21 +73,34 @@ def process_rpc_message(data: str) -> Tuple[Dict[str, Any], str]: str_stream: io.StringIO = io.StringIO(data) length: int = 0 + while True: line: str = str_stream.readline() if CONTENT_LENGTH.lower() in line.lower(): length = int(line[len(CONTENT_LENGTH) :]) break + if not line or line.isspace(): raise ValueError("Header does not contain Content-Length") + while True: line: str = str_stream.readline() if not line or line.isspace(): break raw_json: str = str_stream.read(length) - dict_json: Dict[str, Any] = json.loads(raw_json) - return dict_json, str_stream.read() + return json.loads(raw_json), str_stream.read() + + +def process_rpc_json(data: str) -> List[Dict[str, Any]]: + """Process the JSON data which comes from the server which runs the pytest discovery.""" + json_messages = [] + remaining = data + while remaining: + json_data, remaining = process_rpc_message(remaining) + json_messages.append(json_data) + + return json_messages def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: @@ -110,53 +124,58 @@ def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: "PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent), } ) + completed = threading.Event() - result: list = [] + result = [] t1: threading.Thread = threading.Thread( - target=_listen_on_socket, args=(listener, result) + target=_listen_on_socket, args=(listener, result, completed) ) t1.start() t2 = threading.Thread( - target=lambda proc_args, proc_env, proc_cwd: subprocess.run( - proc_args, env=proc_env, cwd=proc_cwd - ), - args=(process_args, env, TEST_DATA_PATH), + target=_run_test_code, + args=(process_args, env, TEST_DATA_PATH, completed), ) t2.start() t1.join() t2.join() - a = process_rpc_json(result[0]) - return a if result else None + return process_rpc_json(result[0]) if result else None -def process_rpc_json(data: str) -> List[Dict[str, Any]]: - """Process the JSON data which comes from the server which runs the pytest discovery.""" - json_messages = [] - remaining = data - while remaining: - json_data, remaining = process_rpc_message(remaining) - json_messages.append(json_data) - - return json_messages - - -def _listen_on_socket(listener: socket.socket, result: List[str]): +def _listen_on_socket( + listener: socket.socket, result: List[str], completed: threading.Event +): """Listen on the socket for the JSON data from the server. - Created as a seperate function for clarity in threading. + Created as a separate function for clarity in threading. """ sock, (other_host, other_port) = listener.accept() + listener.settimeout(1) all_data: list = [] while True: data: bytes = sock.recv(1024 * 1024) if not data: - break + if completed.is_set(): + break + else: + try: + sock, (other_host, other_port) = listener.accept() + except socket.timeout: + result.append("".join(all_data)) + return all_data.append(data.decode("utf-8")) result.append("".join(all_data)) +def _run_test_code( + proc_args: List[str], proc_env, proc_cwd: str, completed: threading.Event +): + result = subprocess.run(proc_args, env=proc_env, cwd=proc_cwd) + completed.set() + return result + + def find_test_line_number(test_name: str, test_file_path) -> str: """Function which finds the correct line number for a test by looking for the "test_marker--[test_name]" string. diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 3cbaa270cab9..7f2355129c65 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -59,11 +59,10 @@ def test_syntax_error(tmp_path): temp_dir.mkdir() p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) - actual_list: Optional[List[Dict[str, Any]]] = runner( - ["--collect-only", os.fspath(p)] - ) - assert actual_list - for actual in actual_list: + actual = runner(["--collect-only", os.fspath(p)]) + if actual: + actual = actual[0] + assert actual assert all(item in actual for item in ("status", "cwd", "error")) assert actual["status"] == "error" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) @@ -76,11 +75,9 @@ def test_parameterized_error_collect(): The json should still be returned but the errors list should be present. """ file_path_str = "error_parametrize_discovery.py" - actual_list: Optional[List[Dict[str, Any]]] = runner( - ["--collect-only", file_path_str] - ) - assert actual_list - for actual in actual_list: + actual = runner(["--collect-only", file_path_str]) + if actual: + actual = actual[0] assert all(item in actual for item in ("status", "cwd", "error")) assert actual["status"] == "error" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) @@ -135,14 +132,15 @@ def test_pytest_collect(file, expected_const): file -- a string with the file or folder to run pytest discovery on. expected_const -- the expected output from running pytest discovery on the file. """ - actual_list: Optional[List[Dict[str, Any]]] = runner( + actual = runner( [ "--collect-only", os.fspath(TEST_DATA_PATH / file), ] ) - assert actual_list - for actual in actual_list: + if actual: + actual = actual[0] + assert actual assert all(item in actual for item in ("status", "cwd", "tests")) assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index 5b2cbddb1644..d11766027a91 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -2,7 +2,6 @@ # Licensed under the MIT License. import os import shutil -from typing import Any, Dict, List, Optional import pytest from tests.pytestadapter import expected_execution_test_output @@ -29,11 +28,10 @@ def test_syntax_error_execution(tmp_path): temp_dir.mkdir() p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) - actual_list: Optional[List[Dict[str, Any]]] = runner( - ["error_syntax_discover.py::test_function"] - ) - assert actual_list - for actual in actual_list: + actual = runner(["error_syntax_discover.py::test_function"]) + if actual: + actual = actual[0] + assert actual assert all(item in actual for item in ("status", "cwd", "error")) assert actual["status"] == "error" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) @@ -45,9 +43,10 @@ def test_bad_id_error_execution(): The json should still be returned but the errors list should be present. """ - actual_list: Optional[List[Dict[str, Any]]] = runner(["not/a/real::test_id"]) - assert actual_list - for actual in actual_list: + actual = runner(["not/a/real::test_id"]) + if actual: + actual = actual[0] + assert actual assert all(item in actual for item in ("status", "cwd", "error")) assert actual["status"] == "error" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) @@ -156,14 +155,17 @@ def test_pytest_execution(test_ids, expected_const): expected_const -- a dictionary of the expected output from running pytest discovery on the files. """ args = test_ids - actual_list: Optional[List[Dict[str, Any]]] = runner(args) - assert actual_list - for actual in actual_list: - assert all(item in actual for item in ("status", "cwd", "result")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - result_data = actual["result"] - for key in result_data: - if result_data[key]["outcome"] == "failure": - result_data[key]["message"] = "ERROR MESSAGE" - assert result_data == expected_const + actual = runner(args) + assert actual + print(actual) + assert len(actual) == len(expected_const) + actual_result_dict = dict() + for a in actual: + assert all(item in a for item in ("status", "cwd", "result")) + assert a["status"] == "success" + assert a["cwd"] == os.fspath(TEST_DATA_PATH) + actual_result_dict.update(a["result"]) + for key in actual_result_dict: + if actual_result_dict[key]["outcome"] == "failure": + actual_result_dict[key]["message"] = "ERROR MESSAGE" + assert actual_result_dict == expected_const diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py index d461ead9ad94..c7a5cb0eef61 100644 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -3,9 +3,14 @@ import os import pathlib +import sys from typing import List import pytest + +PYTHON_FILES = pathlib.Path(__file__).parent.parent + +sys.path.insert(0, os.fspath(PYTHON_FILES)) from unittestadapter.execution import parse_execution_cli_args, run_tests TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index bcc2fd967f78..02490024b217 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -17,8 +17,8 @@ from typing_extensions import Literal # Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. -PYTHON_FILES = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, PYTHON_FILES) +PYTHON_FILES = pathlib.Path(__file__).parent.parent +sys.path.insert(0, os.fspath(PYTHON_FILES)) from testing_tools import socket_manager diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 17c125e5843a..d9d5f41d624b 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -13,17 +13,14 @@ from types import TracebackType from typing import Dict, List, Optional, Tuple, Type, Union -script_dir = pathlib.Path(__file__).parent.parent -sys.path.append(os.fspath(script_dir)) -sys.path.append(os.fspath(script_dir / "lib" / "python")) -from testing_tools import process_json_util - +directory_path = pathlib.Path(__file__).parent.parent / "lib" / "python" # Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. -PYTHON_FILES = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, PYTHON_FILES) +PYTHON_FILES = pathlib.Path(__file__).parent.parent + +sys.path.insert(0, os.fspath(PYTHON_FILES)) # Add the lib path to sys.path to find the typing_extensions module. sys.path.insert(0, os.path.join(PYTHON_FILES, "lib", "python")) -from testing_tools import socket_manager +from testing_tools import process_json_util, socket_manager from typing_extensions import NotRequired, TypeAlias, TypedDict from unittestadapter.utils import parse_unittest_args @@ -54,6 +51,9 @@ def parse_execution_cli_args( ErrorType = Union[ Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None] ] +PORT = 0 +UUID = 0 +START_DIR = "" class TestOutcomeEnum(str, enum.Enum): @@ -148,8 +148,10 @@ def formatResult( "traceback": tb, "subtest": subtest.id() if subtest else None, } - self.formatted[test_id] = result + if PORT == 0 or UUID == 0: + print("Error sending response, port or uuid unknown to python server.") + send_run_data(result, PORT, UUID) class TestExecutionStatus(str, enum.Enum): @@ -225,6 +227,33 @@ def run_tests( return payload +def send_run_data(raw_data, port, uuid): + # Build the request data (it has to be a POST request or the Node side will not process it), and send it. + status = raw_data["outcome"] + cwd = os.path.abspath(START_DIR) + if raw_data["subtest"]: + test_id = raw_data["subtest"] + else: + test_id = raw_data["test"] + test_dict = {} + test_dict[test_id] = raw_data + payload: PayloadDict = {"cwd": cwd, "status": status, "result": test_dict} + addr = ("localhost", port) + data = json.dumps(payload) + request = f"""Content-Length: {len(data)} +Content-Type: application/json +Request-uuid: {uuid} + +{data}""" + try: + with socket_manager.SocketManager(addr) as s: + if s.socket is not None: + s.socket.sendall(request.encode("utf-8")) + except Exception as e: + print(f"Error sending response: {e}") + print(f"Request data: {request}") + + if __name__ == "__main__": # Get unittest test execution arguments. argv = sys.argv[1:] @@ -270,11 +299,11 @@ def run_tests( print(f"Error: Could not connect to runTestIdsPort: {e}") print("Error: Could not connect to runTestIdsPort") - port, uuid = parse_execution_cli_args(argv[:index]) + PORT, UUID = parse_execution_cli_args(argv[:index]) if test_ids_from_buffer: # Perform test execution. payload = run_tests( - start_dir, test_ids_from_buffer, pattern, top_level_dir, uuid + start_dir, test_ids_from_buffer, pattern, top_level_dir, UUID ) else: cwd = os.path.abspath(start_dir) @@ -284,19 +313,3 @@ def run_tests( "status": status, "error": "No test ids received from buffer", } - - # Build the request data and send it. - addr = ("localhost", port) - data = json.dumps(payload) - request = f"""Content-Length: {len(data)} -Content-Type: application/json -Request-uuid: {uuid} - -{data}""" - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Error sending response: {e}") - print(f"Request data: {request}") diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 726dba8c9a97..1178abdb93d6 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -56,7 +56,7 @@ def pytest_internalerror(excrepr, excinfo): excinfo -- the exception information of type ExceptionInfo. """ # call.excinfo.exconly() returns the exception as a string. - ERRORS.append(excinfo.exconly()) + ERRORS.append(excinfo.exconly() + "\n Check Python Test Logs for more details.") def pytest_exception_interact(node, call, report): @@ -70,7 +70,13 @@ def pytest_exception_interact(node, call, report): # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. # call.excinfo.exconly() returns the exception as a string. if call.excinfo and call.excinfo.typename != "AssertionError": - ERRORS.append(call.excinfo.exconly()) + ERRORS.append( + call.excinfo.exconly() + "\n Check Python Test Logs for more details." + ) + else: + ERRORS.append( + report.longreprtext + "\n Check Python Test Logs for more details." + ) def pytest_keyboard_interrupt(excinfo): @@ -79,8 +85,8 @@ def pytest_keyboard_interrupt(excinfo): Keyword arguments: excinfo -- the exception information of type ExceptionInfo. """ - # The function exconly() returns the exception as a string. - ERRORS.append(excinfo.exconly()) + # The function execonly() returns the exception as a string. + ERRORS.append(excinfo.exconly() + "\n Check Python Test Logs for more details.") class TestOutcome(Dict): @@ -120,7 +126,6 @@ class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): tests: Dict[str, TestOutcome] -collected_tests = testRunResultDict() IS_DISCOVERY = False @@ -130,6 +135,9 @@ def pytest_load_initial_conftests(early_config, parser, args): IS_DISCOVERY = True +collected_tests_so_far = list() + + def pytest_report_teststatus(report, config): """ A pytest hook that is called when a test is called. It is called 3 times per test, @@ -138,6 +146,7 @@ def pytest_report_teststatus(report, config): report -- the report on the test setup, call, and teardown. config -- configuration object. """ + cwd = pathlib.Path.cwd() if report.when == "call": traceback = None @@ -148,13 +157,22 @@ def pytest_report_teststatus(report, config): elif report.failed: report_value = "failure" message = report.longreprtext - item_result = create_test_outcome( - report.nodeid, - report_value, - message, - traceback, - ) - collected_tests[report.nodeid] = item_result + node_id = str(report.nodeid) + if node_id not in collected_tests_so_far: + collected_tests_so_far.append(node_id) + item_result = create_test_outcome( + node_id, + report_value, + message, + traceback, + ) + collected_test = testRunResultDict() + collected_test[node_id] = item_result + execution_post( + os.fsdecode(cwd), + "success", + collected_test if collected_test else None, + ) ERROR_MESSAGE_CONST = { @@ -187,6 +205,15 @@ def pytest_sessionfinish(session, exitstatus): ) cwd = pathlib.Path.cwd() if IS_DISCOVERY: + if not (exitstatus == 0 or exitstatus == 1 or exitstatus == 5): + errorNode: TestNode = { + "name": "", + "path": cwd, + "type_": "error", + "children": [], + "id_": "", + } + post_response(os.fsdecode(cwd), errorNode) try: session_node: Union[TestNode, None] = build_test_tree(session) if not session_node: @@ -196,7 +223,9 @@ def pytest_sessionfinish(session, exitstatus): ) post_response(os.fsdecode(cwd), session_node) except Exception as e: - f"Error Occurred, description: {e.args[0] if e.args and e.args[0] else ''} traceback: {(traceback.format_exc() if e.__traceback__ else '')}" + ERRORS.append( + f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" + ) errorNode: TestNode = { "name": "", "path": cwd, @@ -213,11 +242,12 @@ def pytest_sessionfinish(session, exitstatus): f"Pytest exited with error status: {exitstatus}, {ERROR_MESSAGE_CONST[exitstatus]}" ) exitstatus_bool = "error" - execution_post( - os.fsdecode(cwd), - exitstatus_bool, - collected_tests if collected_tests else None, - ) + + execution_post( + os.fsdecode(cwd), + exitstatus_bool, + None, + ) def build_test_tree(session: pytest.Session) -> TestNode: diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 49243390ad0f..9b2ad6abdf78 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -24,6 +24,8 @@ export class PythonResultResolver implements ITestResultResolver { public vsIdToRunId: Map; + public subTestStats: Map = new Map(); + constructor(testController: TestController, testProvider: TestProvider, private workspaceUri: Uri) { this.testController = testController; this.testProvider = testProvider; @@ -47,13 +49,13 @@ export class PythonResultResolver implements ITestResultResolver { if (rawTestData.status === 'error') { const testingErrorConst = this.testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; - const { errors } = rawTestData; - traceError(testingErrorConst, '\r\n', errors!.join('\r\n\r\n')); + const { error } = rawTestData; + traceError(testingErrorConst, '\r\n', error?.join('\r\n\r\n') ?? ''); let errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`); const message = util.format( `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, - errors!.join('\r\n\r\n'), + error?.join('\r\n\r\n') ?? '', ); if (errorNode === undefined) { @@ -88,7 +90,6 @@ export class PythonResultResolver implements ITestResultResolver { const rawTestExecData = payload; if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { // Map which holds the subtest information for each test item. - const subTestStats: Map = new Map(); // iterate through payload and update the UI accordingly. for (const keyTemp of Object.keys(rawTestExecData.result)) { @@ -163,11 +164,11 @@ export class PythonResultResolver implements ITestResultResolver { const data = rawTestExecData.result[keyTemp]; // find the subtest's parent test item if (parentTestItem) { - const subtestStats = subTestStats.get(parentTestCaseId); + const subtestStats = this.subTestStats.get(parentTestCaseId); if (subtestStats) { subtestStats.failed += 1; } else { - subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); + this.subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); @@ -200,11 +201,11 @@ export class PythonResultResolver implements ITestResultResolver { // find the subtest's parent test item if (parentTestItem) { - const subtestStats = subTestStats.get(parentTestCaseId); + const subtestStats = this.subTestStats.get(parentTestCaseId); if (subtestStats) { subtestStats.passed += 1; } else { - subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); + this.subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); @@ -229,5 +230,3 @@ export class PythonResultResolver implements ITestResultResolver { return Promise.resolve(); } } - -// had to switch the order of the original parameter since required param cannot follow optional. diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index cb7fda797c4a..d4e54951bfd7 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -231,7 +231,7 @@ export type DiscoveredTestPayload = { cwd: string; tests?: DiscoveredTestNode; status: 'success' | 'error'; - errors?: string[]; + error?: string[]; }; export type ExecutionTestPayload = { diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 4378c68b534c..211c322d67f1 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -8,7 +8,7 @@ import { SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; +import { createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { traceError, traceVerbose } from '../../../logging'; import { @@ -23,8 +23,6 @@ import { * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied */ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { - private promiseMap: Map> = new Map(); - constructor( public testServer: ITestServer, public configSettings: IConfigurationService, @@ -55,7 +53,6 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); const uuid = this.testServer.createUUID(uri.fsPath); - this.promiseMap.set(uuid, deferred); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 1bf032b9b594..b68b80945ef3 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -4,7 +4,7 @@ import { TestRun, Uri } from 'vscode'; import * as path from 'path'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; +import { createDeferred } from '../../../common/utils/async'; import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, @@ -31,8 +31,6 @@ import { startTestIdServer } from '../common/utils'; */ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { - private promiseMap: Map> = new Map(); - constructor( public testServer: ITestServer, public configSettings: IConfigurationService, @@ -48,6 +46,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, ): Promise { + const uuid = this.testServer.createUUID(uri.fsPath); traceVerbose(uri, testIds, debugBool); const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { @@ -55,18 +54,26 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } }); try { - await this.runTestsNew(uri, testIds, debugBool, executionFactory, debugLauncher); + await this.runTestsNew(uri, testIds, uuid, debugBool, executionFactory, debugLauncher); } finally { + this.testServer.deleteUUID(uuid); disposable.dispose(); // confirm with testing that this gets called (it must clean this up) } - const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; + // placeholder until after the rewrite is adopted + // TODO: remove after adoption. + const executionPayload: ExecutionTestPayload = { + cwd: uri.fsPath, + status: 'success', + error: '', + }; return executionPayload; } private async runTestsNew( uri: Uri, testIds: string[], + uuid: string, debugBool?: boolean, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, @@ -75,8 +82,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); this.configSettings.isTestExecution(); - const uuid = this.testServer.createUUID(uri.fsPath); - this.promiseMap.set(uuid, deferred); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 8d393a8da18d..69438e341f14 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -4,7 +4,6 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { DataReceivedEvent, @@ -20,8 +19,6 @@ import { * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. */ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { - private promiseMap: Map> = new Map(); - private cwd: string | undefined; constructor( @@ -32,7 +29,6 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { ) {} public async discoverTests(uri: Uri): Promise { - const deferred = createDeferred(); const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -49,18 +45,21 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { outChannel: this.outputChannel, }; - this.promiseMap.set(uuid, deferred); - const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); }); try { await this.callSendCommand(options); } finally { + this.testServer.deleteUUID(uuid); disposable.dispose(); - // confirm with testing that this gets called (it must clean this up) } - const discoveryPayload: DiscoveredTestPayload = { cwd: uri.fsPath, status: 'success' }; + // placeholder until after the rewrite is adopted + // TODO: remove after adoption. + const discoveryPayload: DiscoveredTestPayload = { + cwd: uri.fsPath, + status: 'success', + }; return discoveryPayload; } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index ca88d3871706..bf3038817f8b 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { TestRun, Uri } from 'vscode'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { Deferred, createDeferred } from '../../../common/utils/async'; +import { createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { DataReceivedEvent, @@ -23,8 +23,6 @@ import { startTestIdServer } from '../common/utils'; */ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { - private promiseMap: Map> = new Map(); - private cwd: string | undefined; constructor( @@ -40,14 +38,16 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { debugBool?: boolean, runInstance?: TestRun, ): Promise { + const uuid = this.testServer.createUUID(uri.fsPath); const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); try { - await this.runTestsNew(uri, testIds, debugBool); + await this.runTestsNew(uri, testIds, uuid, debugBool); } finally { + this.testServer.deleteUUID(uuid); disposable.dispose(); // confirm with testing that this gets called (it must clean this up) } @@ -55,13 +55,17 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { return executionPayload; } - private async runTestsNew(uri: Uri, testIds: string[], debugBool?: boolean): Promise { + private async runTestsNew( + uri: Uri, + testIds: string[], + uuid: string, + debugBool?: boolean, + ): Promise { const settings = this.configSettings.getSettings(uri); const { cwd, unittestArgs } = settings.testing; const command = buildExecutionCommand(unittestArgs); this.cwd = cwd || uri.fsPath; - const uuid = this.testServer.createUUID(uri.fsPath); const options: TestCommandOptions = { workspaceFolder: uri, @@ -74,16 +78,15 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { }; const deferred = createDeferred(); - this.promiseMap.set(uuid, deferred); traceLog(`Running UNITTEST execution for the following test ids: ${testIds}`); const runTestIdsPort = await startTestIdServer(testIds); await this.testServer.sendCommand(options, runTestIdsPort.toString(), () => { - // disposable.dispose(); deferred.resolve(); }); - // return deferred.promise; + // placeholder until after the rewrite is adopted + // TODO: remove after adoption. const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; return executionPayload; } diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts index 57b321c2e36c..94ec1cc10a9a 100644 --- a/src/test/testing/testController/resultResolver.unit.test.ts +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -113,7 +113,7 @@ suite('Result Resolver tests', () => { const payload: DiscoveredTestPayload = { cwd: workspaceUri.fsPath, status: 'error', - errors: [errorMessage], + error: [errorMessage], }; const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { id: 'id', From c1ff6c3ceda1b06d5471837d1d9d74a2d1a52e9e Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 23 Jun 2023 16:58:56 -0700 Subject: [PATCH 0047/1136] add to payload to allow error parsing on TS (#21483) partial fix for https://github.com/microsoft/vscode-python/issues/21476, make sure error display correctly. two part fix: fix "errors" to "error" in unittest payload to align with naming everywhere else add "result" and "test" to payload in run and discovery respectively even on error to havae resultResolver handle correctly. --- .../tests/unittestadapter/test_discovery.py | 8 +++--- .../tests/unittestadapter/test_execution.py | 12 ++++----- pythonFiles/unittestadapter/discovery.py | 25 ++++++++++--------- pythonFiles/unittestadapter/execution.py | 5 ++-- pythonFiles/unittestadapter/utils.py | 6 ++--- 5 files changed, 29 insertions(+), 27 deletions(-) diff --git a/pythonFiles/tests/unittestadapter/test_discovery.py b/pythonFiles/tests/unittestadapter/test_discovery.py index 30ccb7ef4079..28dc51f55dcd 100644 --- a/pythonFiles/tests/unittestadapter/test_discovery.py +++ b/pythonFiles/tests/unittestadapter/test_discovery.py @@ -132,7 +132,7 @@ def test_simple_discovery() -> None: assert actual["status"] == "success" assert is_same_tree(actual.get("tests"), expected) - assert "errors" not in actual + assert "error" not in actual def test_empty_discovery() -> None: @@ -146,8 +146,8 @@ def test_empty_discovery() -> None: actual = discover_tests(start_dir, pattern, None, uuid) assert actual["status"] == "success" - assert "tests" not in actual - assert "errors" not in actual + assert "tests" in actual + assert "error" not in actual def test_error_discovery() -> None: @@ -213,4 +213,4 @@ def test_error_discovery() -> None: assert actual["status"] == "error" assert is_same_tree(expected, actual.get("tests")) - assert len(actual.get("errors", [])) == 1 + assert len(actual.get("error", [])) == 1 diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py index c7a5cb0eef61..a822636bf479 100644 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -65,7 +65,7 @@ def test_no_ids_run() -> None: assert all(item in actual for item in ("cwd", "status")) assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - if "result" in actual: + if actual["result"] is not None: assert len(actual["result"]) == 0 else: raise AssertionError("actual['result'] is None") @@ -85,7 +85,7 @@ def test_single_ids_run() -> None: assert all(item in actual for item in ("cwd", "status")) assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert "result" in actual + assert actual["result"] is not None result = actual["result"] assert len(result) == 1 assert id in result @@ -117,7 +117,7 @@ def test_subtest_run() -> None: assert all(item in actual for item in ("cwd", "status")) assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert "result" in actual + assert actual["result"] is not None result = actual["result"] assert len(result) == 6 for id in subtests_ids: @@ -205,7 +205,7 @@ def test_multiple_ids_run(test_ids, pattern, cwd, expected_outcome) -> None: assert all(item in actual for item in ("cwd", "status")) assert actual["status"] == "success" assert actual["cwd"] == cwd - assert "result" in actual + assert actual["result"] is not None result = actual["result"] assert len(result) == len(test_ids) for test_id in test_ids: @@ -230,7 +230,7 @@ def test_failed_tests(): assert all(item in actual for item in ("cwd", "status")) assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert "result" in actual + assert actual["result"] is not None result = actual["result"] assert len(result) == len(test_ids) for test_id in test_ids: @@ -255,7 +255,7 @@ def test_unknown_id(): assert all(item in actual for item in ("cwd", "status")) assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert "result" in actual + assert actual["result"] is not None result = actual["result"] assert len(result) == len(test_ids) assert "unittest.loader._FailedTest.unknown_id" in result diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index 02490024b217..ffcd2671addb 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -53,8 +53,8 @@ def parse_discovery_cli_args(args: List[str]) -> Tuple[int, Union[str, None]]: class PayloadDict(TypedDict): cwd: str status: Literal["success", "error"] - tests: NotRequired[TestNode] - errors: NotRequired[List[str]] + tests: Optional[TestNode] + error: NotRequired[List[str]] def discover_tests( @@ -68,7 +68,7 @@ def discover_tests( - uuid: UUID sent by the caller of the Python script, that needs to be sent back as an integrity check; - status: Test discovery status, can be "success" or "error"; - tests: Discoverered tests if any, not present otherwise. Note that the status can be "error" but the payload can still contain tests; - - errors: Discovery errors if any, not present otherwise. + - error: Discovery error if any, not present otherwise. Payload format for a successful discovery: { @@ -86,30 +86,31 @@ def discover_tests( Payload format when there are errors: { "cwd": - "errors": [list of errors] + "": [list of errors] "status": "error", } """ cwd = os.path.abspath(start_dir) - payload: PayloadDict = {"cwd": cwd, "status": "success"} + payload: PayloadDict = {"cwd": cwd, "status": "success", "tests": None} tests = None - errors: List[str] = [] + error: List[str] = [] try: loader = unittest.TestLoader() suite = loader.discover(start_dir, pattern, top_level_dir) - tests, errors = build_test_tree(suite, cwd) # test tree built succesfully here. + tests, error = build_test_tree(suite, cwd) # test tree built succesfully here. except Exception: - errors.append(traceback.format_exc()) + error.append(traceback.format_exc()) - if tests is not None: - payload["tests"] = tests + # Still include the tests in the payload even if there are errors so that the TS + # side can determine if it is from run or discovery. + payload["tests"] = tests if tests is not None else None - if len(errors): + if len(error): payload["status"] = "error" - payload["errors"] = errors + payload["error"] = error return payload diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index d9d5f41d624b..aa50cbc5d09f 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -165,7 +165,7 @@ class TestExecutionStatus(str, enum.Enum): class PayloadDict(TypedDict): cwd: str status: TestExecutionStatus - result: NotRequired[TestResultTypeAlias] + result: Optional[TestResultTypeAlias] not_found: NotRequired[List[str]] error: NotRequired[str] @@ -185,7 +185,7 @@ def run_tests( cwd = os.path.abspath(start_dir) status = TestExecutionStatus.error error = None - payload: PayloadDict = {"cwd": cwd, "status": status} + payload: PayloadDict = {"cwd": cwd, "status": status, "result": None} try: # If it's a file, split path and file name. @@ -312,4 +312,5 @@ def send_run_data(raw_data, port, uuid): "cwd": cwd, "status": status, "error": "No test ids received from buffer", + "result": None, } diff --git a/pythonFiles/unittestadapter/utils.py b/pythonFiles/unittestadapter/utils.py index 9c8b896a8d6e..a461baf7d870 100644 --- a/pythonFiles/unittestadapter/utils.py +++ b/pythonFiles/unittestadapter/utils.py @@ -151,14 +151,14 @@ def build_test_tree( "id_": } """ - errors = [] + error = [] directory_path = pathlib.PurePath(test_directory) root = build_test_node(test_directory, directory_path.name, TestNodeTypeEnum.folder) for test_case in get_test_case(suite): test_id = test_case.id() if test_id.startswith("unittest.loader._FailedTest"): - errors.append(str(test_case._exception)) # type: ignore + error.append(str(test_case._exception)) # type: ignore else: # Get the static test path components: filename, class name and function name. components = test_id.split(".") @@ -206,7 +206,7 @@ def build_test_tree( if not root["children"]: root = None - return root, errors + return root, error def parse_unittest_args(args: List[str]) -> Tuple[str, str, Union[str, None]]: From 9c0af357e6e402742a764531d444f040ba4b9f97 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 26 Jun 2023 09:05:12 -0700 Subject: [PATCH 0048/1136] Let pylance use SharedArrayBuffer cancellation in the browser (#21482) Dirk implemented a new cancellation method for the browser in the LSP library. About 6 months ago. We use this in the browser to provide cancellation. Since we moved creation of the pylance language client to Python, this is picking up the LSP library currently in the Python extension and it was before this change by Dirk. This change moves the LSP library up to the latest. --- package-lock.json | 114 +++++++++--------- package.json | 10 +- .../lspInteractiveWindowMiddlewareAddon.ts | 2 +- 3 files changed, 60 insertions(+), 66 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49376d6e6cab..f41a215a083e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,9 +38,9 @@ "vscode-debugadapter": "^1.28.0", "vscode-debugprotocol": "^1.28.0", "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageclient": "8.0.2-next.5", - "vscode-languageserver": "8.0.2-next.5", - "vscode-languageserver-protocol": "3.17.2-next.6", + "vscode-languageclient": "^8.1.0", + "vscode-languageserver": "^8.1.0", + "vscode-languageserver-protocol": "^3.17.3", "vscode-tas-client": "^0.1.63", "which": "^2.0.2", "winreg": "^1.2.4", @@ -14318,29 +14318,18 @@ } }, "node_modules/vscode-languageclient": { - "version": "8.0.2-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2-next.5.tgz", - "integrity": "sha512-g87RJLHz0XlRyk6DOTbAk4JHcj8CKggXy4JiFL7OlhETkcYzTOR8d+Qdb4GqZr37PDs1Cl21omtTNK5LyR/RQg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", + "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", "dependencies": { - "minimatch": "^3.0.4", - "semver": "^7.3.5", - "vscode-languageserver-protocol": "3.17.2-next.6" + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.3" }, "engines": { "vscode": "^1.67.0" } }, - "node_modules/vscode-languageclient/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/vscode-languageclient/node_modules/semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -14356,29 +14345,37 @@ } }, "node_modules/vscode-languageserver": { - "version": "8.0.2-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.0.2-next.5.tgz", - "integrity": "sha512-2ZDb7O/4atS9mJKufPPz637z+51kCyZfgnobFW5eSrUdS3c0UB/nMS4Ng1EavYTX84GVaVMKCrmP0f2ceLmR0A==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz", + "integrity": "sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==", "dependencies": { - "vscode-languageserver-protocol": "3.17.2-next.6" + "vscode-languageserver-protocol": "3.17.3" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.2-next.6", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2-next.6.tgz", - "integrity": "sha512-WtsebNOOkWyNn4oFYoAMPC8Q/ZDoJ/K7Ja53OzTixiitvrl/RpXZETrtzH79R8P5kqCyx6VFBPb6KQILJfkDkA==", + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", + "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", "dependencies": { - "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageserver-types": "3.17.2-next.2" + "vscode-jsonrpc": "8.1.0", + "vscode-languageserver-types": "3.17.3" } }, - "node_modules/vscode-languageserver-protocol/node_modules/vscode-languageserver-types": { - "version": "3.17.2-next.2", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2-next.2.tgz", - "integrity": "sha512-TiAkLABgqkVWdAlC3XlOfdhdjIAdVU4YntPUm9kKGbXr+MGwpVnKz2KZMNBcvG0CFx8Hi8qliL0iq+ndPB720w==" + "node_modules/vscode-languageserver-protocol/node_modules/vscode-jsonrpc": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", + "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", + "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" }, "node_modules/vscode-tas-client": { "version": "0.1.63", @@ -26397,23 +26394,15 @@ "integrity": "sha512-sbbvGSWja7NVBLHPGawtgezc8DHYJaP4qfr/AaJiyDapWcSFtHyPtm18+LnYMLTmB7bhOUW/lf5PeeuLpP6bKA==" }, "vscode-languageclient": { - "version": "8.0.2-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2-next.5.tgz", - "integrity": "sha512-g87RJLHz0XlRyk6DOTbAk4JHcj8CKggXy4JiFL7OlhETkcYzTOR8d+Qdb4GqZr37PDs1Cl21omtTNK5LyR/RQg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", + "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", "requires": { - "minimatch": "^3.0.4", - "semver": "^7.3.5", - "vscode-languageserver-protocol": "3.17.2-next.6" + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.3" }, "dependencies": { - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, "semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -26425,29 +26414,34 @@ } }, "vscode-languageserver": { - "version": "8.0.2-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.0.2-next.5.tgz", - "integrity": "sha512-2ZDb7O/4atS9mJKufPPz637z+51kCyZfgnobFW5eSrUdS3c0UB/nMS4Ng1EavYTX84GVaVMKCrmP0f2ceLmR0A==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz", + "integrity": "sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==", "requires": { - "vscode-languageserver-protocol": "3.17.2-next.6" + "vscode-languageserver-protocol": "3.17.3" } }, "vscode-languageserver-protocol": { - "version": "3.17.2-next.6", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2-next.6.tgz", - "integrity": "sha512-WtsebNOOkWyNn4oFYoAMPC8Q/ZDoJ/K7Ja53OzTixiitvrl/RpXZETrtzH79R8P5kqCyx6VFBPb6KQILJfkDkA==", + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", + "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", "requires": { - "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageserver-types": "3.17.2-next.2" + "vscode-jsonrpc": "8.1.0", + "vscode-languageserver-types": "3.17.3" }, "dependencies": { - "vscode-languageserver-types": { - "version": "3.17.2-next.2", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2-next.2.tgz", - "integrity": "sha512-TiAkLABgqkVWdAlC3XlOfdhdjIAdVU4YntPUm9kKGbXr+MGwpVnKz2KZMNBcvG0CFx8Hi8qliL0iq+ndPB720w==" + "vscode-jsonrpc": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", + "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==" } } }, + "vscode-languageserver-types": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", + "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" + }, "vscode-tas-client": { "version": "0.1.63", "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.63.tgz", diff --git a/package.json b/package.json index ce407ed9ee4d..85d4baa31e4b 100644 --- a/package.json +++ b/package.json @@ -2008,9 +2008,9 @@ "vscode-debugadapter": "^1.28.0", "vscode-debugprotocol": "^1.28.0", "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageclient": "8.0.2-next.5", - "vscode-languageserver": "8.0.2-next.5", - "vscode-languageserver-protocol": "3.17.2-next.6", + "vscode-languageclient": "^8.1.0", + "vscode-languageserver": "^8.1.0", + "vscode-languageserver-protocol": "^3.17.3", "vscode-tas-client": "^0.1.63", "which": "^2.0.2", "winreg": "^1.2.4", @@ -2038,13 +2038,13 @@ "@types/tmp": "^0.0.33", "@types/uuid": "^8.3.4", "@types/vscode": "^1.75.0", - "@vscode/vsce": "^2.18.0", "@types/which": "^2.0.1", "@types/winreg": "^1.2.30", "@types/xml2js": "^0.4.2", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", "@vscode/test-electron": "^2.1.3", + "@vscode/vsce": "^2.18.0", "bent": "^7.3.12", "chai": "^4.1.2", "chai-arrays": "^2.0.0", @@ -2097,4 +2097,4 @@ "webpack-require-from": "^1.8.6", "yargs": "^15.3.1" } -} \ No newline at end of file +} diff --git a/src/client/activation/node/lspInteractiveWindowMiddlewareAddon.ts b/src/client/activation/node/lspInteractiveWindowMiddlewareAddon.ts index c68ebfe5a59c..0a40915d98eb 100644 --- a/src/client/activation/node/lspInteractiveWindowMiddlewareAddon.ts +++ b/src/client/activation/node/lspInteractiveWindowMiddlewareAddon.ts @@ -117,7 +117,7 @@ export class LspInteractiveWindowMiddlewareAddon implements Middleware, Disposab } private static _asTextContentChange(event: TextDocumentChangeEvent, c2pConverter: Converter): TextContent { - const params = c2pConverter.asChangeTextDocumentParams(event); + const params = c2pConverter.asChangeTextDocumentParams(event, event.document.uri, event.document.version); return { document: params.textDocument, changes: params.contentChanges }; } From 304dffa119ba6e8c9d61d8f64a1027869f32da1c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 26 Jun 2023 13:30:55 -0700 Subject: [PATCH 0049/1136] add cwd to python path during unittest sendCommand (#21490) fixes https://github.com/microsoft/vscode-python/issues/21476 --- .../testing/testController/common/server.ts | 6 +- .../testController/server.unit.test.ts | 708 +++++++++--------- 2 files changed, 364 insertions(+), 350 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 32829e355ccb..f854371ffc35 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -4,6 +4,7 @@ import * as net from 'net'; import * as crypto from 'crypto'; import { Disposable, Event, EventEmitter } from 'vscode'; +import * as path from 'path'; import { ExecutionFactoryCreateWithEnvironmentOptions, IPythonExecutionFactory, @@ -141,12 +142,15 @@ export class PythonTestServer implements ITestServer, Disposable { async sendCommand(options: TestCommandOptions, runTestIdPort?: string, callback?: () => void): Promise { const { uuid } = options; + + const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [options.cwd, ...pythonPathParts].join(path.delimiter); const spawnOptions: SpawnOptions = { token: options.token, cwd: options.cwd, throwOnStdErr: true, outputChannel: options.outChannel, - extraVariables: {}, + extraVariables: { PYTHONPATH: pythonPathCommand }, }; if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 38b71992aefb..1131c26c6444 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -1,349 +1,359 @@ -// // Copyright (c) Microsoft Corporation. All rights reserved. -// // Licensed under the MIT License. - -// import * as assert from 'assert'; -// import * as net from 'net'; -// import * as sinon from 'sinon'; -// import * as crypto from 'crypto'; -// import { Uri } from 'vscode'; -// import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; -// import { PythonTestServer } from '../../../client/testing/testController/common/server'; -// import { ITestDebugLauncher } from '../../../client/testing/common/types'; -// import { createDeferred } from '../../../client/common/utils/async'; - -// suite('Python Test Server', () => { -// const fakeUuid = 'fake-uuid'; - -// let stubExecutionFactory: IPythonExecutionFactory; -// let stubExecutionService: IPythonExecutionService; -// let server: PythonTestServer; -// let sandbox: sinon.SinonSandbox; -// let execArgs: string[]; -// let v4Stub: sinon.SinonStub; -// let debugLauncher: ITestDebugLauncher; - -// setup(() => { -// sandbox = sinon.createSandbox(); -// v4Stub = sandbox.stub(crypto, 'randomUUID'); - -// v4Stub.returns(fakeUuid); -// stubExecutionService = ({ -// exec: (args: string[]) => { -// execArgs = args; -// return Promise.resolve({ stdout: '', stderr: '' }); -// }, -// } as unknown) as IPythonExecutionService; - -// stubExecutionFactory = ({ -// createActivatedEnvironment: () => Promise.resolve(stubExecutionService), -// } as unknown) as IPythonExecutionFactory; -// }); - -// teardown(() => { -// sandbox.restore(); -// execArgs = []; -// server.dispose(); -// }); - -// // test('sendCommand should add the port to the command being sent', async () => { -// // const options = { -// // command: { script: 'myscript', args: ['-foo', 'foo'] }, -// // workspaceFolder: Uri.file('/foo/bar'), -// // cwd: '/foo/bar', -// // uuid: fakeUuid, -// // }; - -// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// // await server.serverReady(); - -// // await server.sendCommand(options); -// // const port = server.getPort(); - -// // assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); -// // }); - -// // test('sendCommand should write to an output channel if it is provided as an option', async () => { -// // const output: string[] = []; -// // const outChannel = { -// // appendLine: (str: string) => { -// // output.push(str); -// // }, -// // } as OutputChannel; -// // const options = { -// // command: { script: 'myscript', args: ['-foo', 'foo'] }, -// // workspaceFolder: Uri.file('/foo/bar'), -// // cwd: '/foo/bar', -// // uuid: fakeUuid, -// // outChannel, -// // }; - -// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// // await server.serverReady(); - -// // await server.sendCommand(options); - -// // const port = server.getPort(); -// // const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); - -// // assert.deepStrictEqual(output, [expected]); -// // }); - -// // test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { -// // let eventData: { status: string; errors: string[] }; -// // stubExecutionService = ({ -// // exec: () => { -// // throw new Error('Failed to execute'); -// // }, -// // } as unknown) as IPythonExecutionService; - -// // const options = { -// // command: { script: 'myscript', args: ['-foo', 'foo'] }, -// // workspaceFolder: Uri.file('/foo/bar'), -// // cwd: '/foo/bar', -// // uuid: fakeUuid, -// // }; - -// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// // await server.serverReady(); - -// // server.onDataReceived(({ data }) => { -// // eventData = JSON.parse(data); -// // }); - -// // await server.sendCommand(options); - -// // assert.deepStrictEqual(eventData!.status, 'error'); -// // assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); -// // }); - -// // test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { -// // let eventData: string | undefined; -// // const client = new net.Socket(); -// // const deferred = createDeferred(); - -// // const options = { -// // command: { script: 'myscript', args: ['-foo', 'foo'] }, -// // workspaceFolder: Uri.file('/foo/bar'), -// // cwd: '/foo/bar', -// // uuid: fakeUuid, -// // }; - -// // stubExecutionService = ({ -// // exec: async () => { -// // client.connect(server.getPort()); -// // return Promise.resolve({ stdout: '', stderr: '' }); -// // }, -// // } as unknown) as IPythonExecutionService; - -// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// // await server.serverReady(); -// // server.onDataReceived(({ data }) => { -// // eventData = data; -// // deferred.resolve(); -// // }); - -// // client.on('connect', () => { -// // console.log('Socket connected, local port:', client.localPort); -// // client.write('malformed data'); -// // client.end(); -// // }); -// // client.on('error', (error) => { -// // console.log('Socket connection error:', error); -// // }); - -// // await server.sendCommand(options); -// // await deferred.promise; -// // assert.deepStrictEqual(eventData, ''); -// // }); - -// // test('If the server doesnt recognize the UUID it should ignore it', async () => { -// // let eventData: string | undefined; -// // const client = new net.Socket(); -// // const deferred = createDeferred(); - -// // const options = { -// // command: { script: 'myscript', args: ['-foo', 'foo'] }, -// // workspaceFolder: Uri.file('/foo/bar'), -// // cwd: '/foo/bar', -// // uuid: fakeUuid, -// // }; - -// // stubExecutionService = ({ -// // exec: async () => { -// // client.connect(server.getPort()); -// // return Promise.resolve({ stdout: '', stderr: '' }); -// // }, -// // } as unknown) as IPythonExecutionService; - -// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// // await server.serverReady(); -// // server.onDataReceived(({ data }) => { -// // eventData = data; -// // deferred.resolve(); -// // }); - -// // client.on('connect', () => { -// // console.log('Socket connected, local port:', client.localPort); -// // client.write('{"Request-uuid": "unknown-uuid"}'); -// // client.end(); -// // }); -// // client.on('error', (error) => { -// // console.log('Socket connection error:', error); -// // }); - -// // await server.sendCommand(options); -// // await deferred.promise; -// // assert.deepStrictEqual(eventData, ''); -// // }); - -// // required to have "tests" or "results" -// // the heading length not being equal and yes being equal -// // multiple payloads -// // test('Error if payload does not have a content length header', async () => { -// // let eventData: string | undefined; -// // const client = new net.Socket(); -// // const deferred = createDeferred(); - -// // const options = { -// // command: { script: 'myscript', args: ['-foo', 'foo'] }, -// // workspaceFolder: Uri.file('/foo/bar'), -// // cwd: '/foo/bar', -// // uuid: fakeUuid, -// // }; - -// // stubExecutionService = ({ -// // exec: async () => { -// // client.connect(server.getPort()); -// // return Promise.resolve({ stdout: '', stderr: '' }); -// // }, -// // } as unknown) as IPythonExecutionService; - -// // server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// // await server.serverReady(); -// // server.onDataReceived(({ data }) => { -// // eventData = data; -// // deferred.resolve(); -// // }); - -// // client.on('connect', () => { -// // console.log('Socket connected, local port:', client.localPort); -// // client.write('{"not content length": "5"}'); -// // client.end(); -// // }); -// // client.on('error', (error) => { -// // console.log('Socket connection error:', error); -// // }); - -// // await server.sendCommand(options); -// // await deferred.promise; -// // assert.deepStrictEqual(eventData, ''); -// // }); - -// const testData = [ -// { -// testName: 'fires discovery correctly on test payload', -// payload: `Content-Length: 52 -// Content-Type: application/json -// Request-uuid: UUID_HERE - -// {"cwd": "path", "status": "success", "tests": "xyz"}`, -// expectedResult: '{"cwd": "path", "status": "success", "tests": "xyz"}', -// }, -// // Add more test data as needed -// ]; - -// testData.forEach(({ testName, payload, expectedResult }) => { -// test(`test: ${testName}`, async () => { -// // Your test logic here -// let eventData: string | undefined; -// const client = new net.Socket(); -// const deferred = createDeferred(); - -// const options = { -// command: { script: 'myscript', args: ['-foo', 'foo'] }, -// workspaceFolder: Uri.file('/foo/bar'), -// cwd: '/foo/bar', -// uuid: fakeUuid, -// }; - -// stubExecutionService = ({ -// exec: async () => { -// client.connect(server.getPort()); -// return Promise.resolve({ stdout: '', stderr: '' }); -// }, -// } as unknown) as IPythonExecutionService; - -// server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// await server.serverReady(); -// const uuid = server.createUUID(); -// payload = payload.replace('UUID_HERE', uuid); -// server.onDiscoveryDataReceived(({ data }) => { -// eventData = data; -// deferred.resolve(); -// }); - -// client.on('connect', () => { -// console.log('Socket connected, local port:', client.localPort); -// client.write(payload); -// client.end(); -// }); -// client.on('error', (error) => { -// console.log('Socket connection error:', error); -// }); - -// await server.sendCommand(options); -// await deferred.promise; -// assert.deepStrictEqual(eventData, expectedResult); -// }); -// }); - -// test('Calls run resolver if the result header is in the payload', async () => { -// let eventData: string | undefined; -// const client = new net.Socket(); -// const deferred = createDeferred(); - -// const options = { -// command: { script: 'myscript', args: ['-foo', 'foo'] }, -// workspaceFolder: Uri.file('/foo/bar'), -// cwd: '/foo/bar', -// uuid: fakeUuid, -// }; - -// stubExecutionService = ({ -// exec: async () => { -// client.connect(server.getPort()); -// return Promise.resolve({ stdout: '', stderr: '' }); -// }, -// } as unknown) as IPythonExecutionService; - -// server = new PythonTestServer(stubExecutionFactory, debugLauncher); -// await server.serverReady(); -// const uuid = server.createUUID(); -// server.onRunDataReceived(({ data }) => { -// eventData = data; -// deferred.resolve(); -// }); - -// const payload = `Content-Length: 87 -// Content-Type: application/json -// Request-uuid: ${uuid} - -// {"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}`; - -// client.on('connect', () => { -// console.log('Socket connected, local port:', client.localPort); -// client.write(payload); -// client.end(); -// }); -// client.on('error', (error) => { -// console.log('Socket connection error:', error); -// }); - -// await server.sendCommand(options); -// await deferred.promise; -// console.log('event data', eventData); -// const expectedResult = -// '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; -// assert.deepStrictEqual(eventData, expectedResult); -// }); -// }); +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as net from 'net'; +import * as sinon from 'sinon'; +import * as crypto from 'crypto'; +import { OutputChannel, Uri } from 'vscode'; +import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions } from '../../../client/common/process/types'; +import { PythonTestServer } from '../../../client/testing/testController/common/server'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { createDeferred } from '../../../client/common/utils/async'; + +suite('Python Test Server', () => { + const fakeUuid = 'fake-uuid'; + + let stubExecutionFactory: IPythonExecutionFactory; + let stubExecutionService: IPythonExecutionService; + let server: PythonTestServer; + let sandbox: sinon.SinonSandbox; + let execArgs: string[]; + let spawnOptions: SpawnOptions; + let v4Stub: sinon.SinonStub; + let debugLauncher: ITestDebugLauncher; + + setup(() => { + sandbox = sinon.createSandbox(); + v4Stub = sandbox.stub(crypto, 'randomUUID'); + + v4Stub.returns(fakeUuid); + stubExecutionService = ({ + exec: (args: string[], spawnOptionsProvided: SpawnOptions) => { + execArgs = args; + spawnOptions = spawnOptionsProvided; + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; + + stubExecutionFactory = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService), + } as unknown) as IPythonExecutionFactory; + }); + + teardown(() => { + sandbox.restore(); + execArgs = []; + server.dispose(); + }); + + test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + }; + const expectedSpawnOptions = { + cwd: '/foo/bar', + outputChannel: undefined, + token: undefined, + throwOnStdErr: true, + extraVariables: { PYTHONPATH: '/foo/bar', RUN_TEST_IDS_PORT: '56789' }, + } as SpawnOptions; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + + await server.sendCommand(options, '56789'); + const port = server.getPort(); + + assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); + assert.deepStrictEqual(spawnOptions, expectedSpawnOptions); + }); + + test('sendCommand should write to an output channel if it is provided as an option', async () => { + const output: string[] = []; + const outChannel = { + appendLine: (str: string) => { + output.push(str); + }, + } as OutputChannel; + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + outChannel, + }; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + + await server.sendCommand(options); + + const port = server.getPort(); + const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); + + assert.deepStrictEqual(output, [expected]); + }); + + test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { + let eventData: { status: string; errors: string[] }; + stubExecutionService = ({ + exec: () => { + throw new Error('Failed to execute'); + }, + } as unknown) as IPythonExecutionService; + + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + }; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + + server.onDataReceived(({ data }) => { + eventData = JSON.parse(data); + }); + + await server.sendCommand(options); + + assert.deepStrictEqual(eventData!.status, 'error'); + assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); + }); + + test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { + let eventData: string | undefined; + const client = new net.Socket(); + const deferred = createDeferred(); + + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + }; + + stubExecutionService = ({ + exec: async () => { + client.connect(server.getPort()); + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + server.onDataReceived(({ data }) => { + eventData = data; + deferred.resolve(); + }); + + client.on('connect', () => { + console.log('Socket connected, local port:', client.localPort); + client.write('malformed data'); + client.end(); + }); + client.on('error', (error) => { + console.log('Socket connection error:', error); + }); + + await server.sendCommand(options); + await deferred.promise; + assert.deepStrictEqual(eventData, ''); + }); + + test('If the server doesnt recognize the UUID it should ignore it', async () => { + let eventData: string | undefined; + const client = new net.Socket(); + const deferred = createDeferred(); + + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + }; + + stubExecutionService = ({ + exec: async () => { + client.connect(server.getPort()); + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + server.onDataReceived(({ data }) => { + eventData = data; + deferred.resolve(); + }); + + client.on('connect', () => { + console.log('Socket connected, local port:', client.localPort); + client.write('{"Request-uuid": "unknown-uuid"}'); + client.end(); + }); + client.on('error', (error) => { + console.log('Socket connection error:', error); + }); + + await server.sendCommand(options); + await deferred.promise; + assert.deepStrictEqual(eventData, ''); + }); + + // required to have "tests" or "results" + // the heading length not being equal and yes being equal + // multiple payloads + test('Error if payload does not have a content length header', async () => { + let eventData: string | undefined; + const client = new net.Socket(); + const deferred = createDeferred(); + + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + }; + + stubExecutionService = ({ + exec: async () => { + client.connect(server.getPort()); + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + server.onDataReceived(({ data }) => { + eventData = data; + deferred.resolve(); + }); + + client.on('connect', () => { + console.log('Socket connected, local port:', client.localPort); + client.write('{"not content length": "5"}'); + client.end(); + }); + client.on('error', (error) => { + console.log('Socket connection error:', error); + }); + + await server.sendCommand(options); + await deferred.promise; + assert.deepStrictEqual(eventData, ''); + }); + + const testData = [ + { + testName: 'fires discovery correctly on test payload', + payload: `Content-Length: 52 +Content-Type: application/json +Request-uuid: UUID_HERE + +{"cwd": "path", "status": "success", "tests": "xyz"}`, + expectedResult: '{"cwd": "path", "status": "success", "tests": "xyz"}', + }, + // Add more test data as needed + ]; + + testData.forEach(({ testName, payload, expectedResult }) => { + test(`test: ${testName}`, async () => { + // Your test logic here + let eventData: string | undefined; + const client = new net.Socket(); + const deferred = createDeferred(); + + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + }; + + stubExecutionService = ({ + exec: async () => { + client.connect(server.getPort()); + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + const uuid = server.createUUID(); + payload = payload.replace('UUID_HERE', uuid); + server.onDiscoveryDataReceived(({ data }) => { + eventData = data; + deferred.resolve(); + }); + + client.on('connect', () => { + console.log('Socket connected, local port:', client.localPort); + client.write(payload); + client.end(); + }); + client.on('error', (error) => { + console.log('Socket connection error:', error); + }); + + await server.sendCommand(options); + await deferred.promise; + assert.deepStrictEqual(eventData, expectedResult); + }); + }); + + test('Calls run resolver if the result header is in the payload', async () => { + let eventData: string | undefined; + const client = new net.Socket(); + const deferred = createDeferred(); + + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: fakeUuid, + }; + + stubExecutionService = ({ + exec: async () => { + client.connect(server.getPort()); + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); + await server.serverReady(); + const uuid = server.createUUID(); + server.onRunDataReceived(({ data }) => { + eventData = data; + deferred.resolve(); + }); + + const payload = `Content-Length: 87 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}`; + + client.on('connect', () => { + console.log('Socket connected, local port:', client.localPort); + client.write(payload); + client.end(); + }); + client.on('error', (error) => { + console.log('Socket connection error:', error); + }); + + await server.sendCommand(options); + await deferred.promise; + console.log('event data', eventData); + const expectedResult = + '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; + assert.deepStrictEqual(eventData, expectedResult); + }); +}); From bd5b622a2456be181d215a1d758a0456412d7c72 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Mon, 26 Jun 2023 13:35:11 -0700 Subject: [PATCH 0050/1136] Dev Container for VS Code Python Extension (#21435) Contains dockerfile, devcontainer.json, post run script needed to create dev container. Base image used is the latest version of Fedora, and the docker file contains installation for: python version(s), node, npm, conda. --------- authored-by: Anthony Kim --- .devcontainer/Dockerfile | 26 ++++++++++++++++++++++++++ .devcontainer/devcontainer.json | 29 +++++++++++++++++++++++++++++ scripts/postCreateCommand.sh | 15 +++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 scripts/postCreateCommand.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000000..f5f49445b399 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,26 @@ +# This image will serve as a starting point for devcontainer.json. +# Get latest image of Fedora as the base image. +FROM docker.io/library/fedora:latest + +# Install supported python versions and nodejs. +RUN dnf -y --nodocs install /usr/bin/{python3.7,python3.8,python3.9,python3.10,python3.11,git,conda,clang} && \ + dnf clean all + +ENV NVM_VERSION=0.39.3 +ENV NODE_VERSION=16.17.1 +ENV NPM_VERSION=8.19.3 + +# Installation instructions from https://github.com/nvm-sh/nvm . +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v$NVM_VERSION/install.sh | bash +RUN export NVM_DIR="$HOME/.nvm" && \ + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ + [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" && \ + nvm install $NODE_VERSION && \ + npm install -g npm@$NPM_VERSION + +# For clean open source builds. +ENV DISABLE_TRANSLATIONS=true + + + + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000000..6435ba5bbda8 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,29 @@ +// For format details, see https://aka.ms/devcontainer.json. +{ + "name": "VS Code Python Dev Container", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "build": { + "dockerfile": "./Dockerfile", + "context": ".." + }, + "customizations": { + "vscode": { + "extensions": [ + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "ms-python.python", + "ms-python.black-formatter", + "ms-python.vscode-pylance", + "charliermarsh.ruff" + ] + } + }, + // Commands to execute on container creation,start. + "postCreateCommand": "bash scripts/postCreateCommand.sh", + // Environment variable placed inside containerEnv following: https://containers.dev/implementors/json_reference/#general-properties + "containerEnv": { + "CI_PYTHON_PATH": "/workspaces/vscode-python/.venv/bin/python" + } + +} diff --git a/scripts/postCreateCommand.sh b/scripts/postCreateCommand.sh new file mode 100644 index 000000000000..85462caf7fad --- /dev/null +++ b/scripts/postCreateCommand.sh @@ -0,0 +1,15 @@ +#!/bin/bash +npm ci +# Create Virutal environment. +python3.7 -m venv /workspaces/vscode-python/.venv + +# Activate Virtual environment. +source /workspaces/vscode-python/.venv/bin/activate + +# Install required Python libraries. +npx gulp installPythonLibs + +# Install testing requirement using python in .venv . +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/test-requirements.txt +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/smoke-test-requirements.txt +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/functional-test-requirements.txt From 0307e91f9943d660d5c03ae01f8c3c78961dde4e Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 26 Jun 2023 18:16:15 -0700 Subject: [PATCH 0051/1136] split subtest name to be unique second half (#21497) fixes: https://github.com/microsoft/vscode-python/issues/21461 --- .../testController/common/resultResolver.ts | 4 +- .../resultResolver.unit.test.ts | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 9b2ad6abdf78..8e08ffbeedf5 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -160,6 +160,7 @@ export class PythonResultResolver implements ITestResultResolver { } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { // split on " " since the subtest ID has the parent test ID in the first part of the ID. const parentTestCaseId = keyTemp.split(' ')[0]; + const subtestId = keyTemp.split(' ')[1]; const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); const data = rawTestExecData.result[keyTemp]; // find the subtest's parent test item @@ -173,7 +174,6 @@ export class PythonResultResolver implements ITestResultResolver { // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); } - const subtestId = keyTemp; const subTestItem = this.testController?.createTestItem(subtestId, subtestId); runInstance.appendOutput(fixLogLines(`${subtestId} Failed\r\n`)); // create a new test item for the subtest @@ -197,6 +197,7 @@ export class PythonResultResolver implements ITestResultResolver { } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { // split on " " since the subtest ID has the parent test ID in the first part of the ID. const parentTestCaseId = keyTemp.split(' ')[0]; + const subtestId = keyTemp.split(' ')[1]; const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); // find the subtest's parent test item @@ -210,7 +211,6 @@ export class PythonResultResolver implements ITestResultResolver { // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); } - const subtestId = keyTemp; const subTestItem = this.testController?.createTestItem(subtestId, subtestId); // create a new test item for the subtest if (subTestItem) { diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts index 94ec1cc10a9a..0fc7eb3ab353 100644 --- a/src/test/testing/testController/resultResolver.unit.test.ts +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -4,6 +4,7 @@ import { TestController, Uri, TestItem, CancellationToken, TestRun, TestItemCollection, Range } from 'vscode'; import * as typemoq from 'typemoq'; import * as sinon from 'sinon'; +import * as assert from 'assert'; import { TestProvider } from '../../../client/testing/types'; import { DiscoveredTestNode, @@ -205,6 +206,58 @@ suite('Result Resolver tests', () => { teardown(() => { sinon.restore(); }); + test('resolveExecution create correct subtest item for unittest', async () => { + // test specific constants used expected values + sinon.stub(testItemUtilities, 'clearAllChildren').callsFake(() => undefined); + testProvider = 'unittest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + const mockSubtestItem = createMockTestItem('parentTest subTest'); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + // creates a mock test item with a space which will be used to split the runId + resultResolver.runIdToVSid.set('parentTest subTest', 'parentTest subTest'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('parentTest', mockTestItem2); + resultResolver.runIdToTestItem.set('parentTest subTest', mockSubtestItem); + + let generatedId: string | undefined; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((id: string) => { + generatedId = id; + console.log('createTestItem function called with id:', id); + }) + .returns(() => ({ id: 'id_this', label: 'label_this', uri: workspaceUri } as TestItem)); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + 'parentTest subTest': { + test: 'test', + outcome: 'subtest-success', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + assert.ok(generatedId); + assert.strictEqual(generatedId, 'subTest'); + }); test('resolveExecution handles failed tests correctly', async () => { // test specific constants used expected values testProvider = 'pytest'; From 84f334f5a58e00377296168dc50197ac34bc73b2 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 26 Jun 2023 19:45:39 -0700 Subject: [PATCH 0052/1136] fix bug that caused dup in param test cases (#21498) fixes https://github.com/microsoft/vscode-python/issues/21491 --- .../.data/param_same_name/test_param1.py | 8 ++ .../.data/param_same_name/test_param2.py | 8 ++ .../expected_discovery_test_output.py | 111 ++++++++++++++++++ .../tests/pytestadapter/test_discovery.py | 4 + .../tests/pytestadapter/test_execution.py | 7 -- pythonFiles/vscode_pytest/__init__.py | 9 +- 6 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 pythonFiles/tests/pytestadapter/.data/param_same_name/test_param1.py create mode 100644 pythonFiles/tests/pytestadapter/.data/param_same_name/test_param2.py diff --git a/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param1.py b/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param1.py new file mode 100644 index 000000000000..a16d0f49f411 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param1.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest + + +@pytest.mark.parametrize("num", ["a", "b", "c"]) +def test_odd_even(num): + assert True diff --git a/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param2.py b/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param2.py new file mode 100644 index 000000000000..c0ea8010e359 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/param_same_name/test_param2.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest + + +@pytest.mark.parametrize("num", range(1, 4)) +def test_odd_even(num): + assert True diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 494d8794ca73..33440a6d42fa 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -474,3 +474,114 @@ ], "id_": TEST_DATA_PATH_STR, } + +# This is the expected output for the param_same_name tests. +# └── param_same_name +# └── test_param1.py +# └── test_odd_even +# └── [a] +# └── [b] +# └── [c] +# └── test_param2.py +# └── test_odd_even +# └── [1] +# └── [2] +# └── [3] +param1_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param1.py") +param2_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param2.py") +param_same_name_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "param_same_name", + "path": os.fspath(TEST_DATA_PATH / "param_same_name"), + "type_": "folder", + "id_": os.fspath(TEST_DATA_PATH / "param_same_name"), + "children": [ + { + "name": "test_param1.py", + "path": param1_path, + "type_": "file", + "id_": param1_path, + "children": [ + { + "name": "test_odd_even", + "path": param1_path, + "type_": "function", + "children": [ + { + "name": "[a]", + "path": param1_path, + "lineno": "6", + "type_": "test", + "id_": "param_same_name/test_param1.py::test_odd_even[a]", + "runID": "param_same_name/test_param1.py::test_odd_even[a]", + }, + { + "name": "[b]", + "path": param1_path, + "lineno": "6", + "type_": "test", + "id_": "param_same_name/test_param1.py::test_odd_even[b]", + "runID": "param_same_name/test_param1.py::test_odd_even[b]", + }, + { + "name": "[c]", + "path": param1_path, + "lineno": "6", + "type_": "test", + "id_": "param_same_name/test_param1.py::test_odd_even[c]", + "runID": "param_same_name/test_param1.py::test_odd_even[c]", + }, + ], + "id_": "param_same_name/test_param1.py::test_odd_even", + } + ], + }, + { + "name": "test_param2.py", + "path": param2_path, + "type_": "file", + "id_": param2_path, + "children": [ + { + "name": "test_odd_even", + "path": param2_path, + "type_": "function", + "children": [ + { + "name": "[1]", + "path": param2_path, + "lineno": "6", + "type_": "test", + "id_": "param_same_name/test_param2.py::test_odd_even[1]", + "runID": "param_same_name/test_param2.py::test_odd_even[1]", + }, + { + "name": "[2]", + "path": param2_path, + "lineno": "6", + "type_": "test", + "id_": "param_same_name/test_param2.py::test_odd_even[2]", + "runID": "param_same_name/test_param2.py::test_odd_even[2]", + }, + { + "name": "[3]", + "path": param2_path, + "lineno": "6", + "type_": "test", + "id_": "param_same_name/test_param2.py::test_odd_even[3]", + "runID": "param_same_name/test_param2.py::test_odd_even[3]", + }, + ], + "id_": "param_same_name/test_param2.py::test_odd_even", + } + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 7f2355129c65..02ea1ddcd871 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -87,6 +87,10 @@ def test_parameterized_error_collect(): @pytest.mark.parametrize( "file, expected_const", [ + ( + "param_same_name", + expected_discovery_test_output.param_same_name_expected_output, + ), ( "parametrize_tests.py", expected_discovery_test_output.parametrize_tests_expected_output, diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index d11766027a91..400ef9f883bc 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -125,12 +125,6 @@ def test_bad_id_error_execution(): ], expected_execution_test_output.doctest_pytest_expected_execution_output, ), - ( - [ - "", - ], - expected_execution_test_output.no_test_ids_pytest_execution_expected_output, - ), ], ) def test_pytest_execution(test_ids, expected_const): @@ -147,7 +141,6 @@ def test_pytest_execution(test_ids, expected_const): 8. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. 9. single_parametrize_tests_expected_execution_output: test run on single parametrize test. 10. doctest_pytest_expected_execution_output: test run on doctest file. - 11. no_test_ids_pytest_execution_expected_output: test run with no inputted test ids. Keyword arguments: diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 1178abdb93d6..2aa2f6052d79 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -288,11 +288,12 @@ def build_test_tree(session: pytest.Session) -> TestNode: elif hasattr(test_case, "callspec"): # This means it is a parameterized test. function_name: str = "" # parameterized test cases cut the repetitive part of the name off. - name_split = test_node["name"].split("[")[1] - test_node["name"] = "[" + name_split + name_split = test_node["name"].split("[") + test_node["name"] = "[" + name_split[1] + parent_path = os.fspath(test_case.path) try: function_name = test_case.originalname # type: ignore - function_test_case = function_nodes_dict[function_name] + function_test_case = function_nodes_dict[parent_path] except AttributeError: # actual error has occurred ERRORS.append( f"unable to find original name for {test_case.name} with parameterization detected." @@ -304,7 +305,7 @@ def build_test_tree(session: pytest.Session) -> TestNode: function_test_case: TestNode = create_parameterized_function_node( function_name, test_case.path, test_case.nodeid ) - function_nodes_dict[function_name] = function_test_case + function_nodes_dict[parent_path] = function_test_case function_test_case["children"].append(test_node) # Now, add the function node to file node. try: From 8e61977a710a95f00011beee89bcf351ec747400 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 27 Jun 2023 23:24:47 -0700 Subject: [PATCH 0053/1136] Fix filtering with QuickPick (#21524) Fixes https://github.com/microsoft/vscode-python/issues/21518 --- .../pythonEnvironments/creation/common/workspaceSelection.ts | 2 ++ src/client/pythonEnvironments/creation/provider/condaUtils.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts index d145e38134d1..14246eabd768 100644 --- a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts +++ b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts @@ -68,6 +68,8 @@ export async function pickWorkspaceFolder( placeHolder: CreateEnv.pickWorkspacePlaceholder, ignoreFocusOut: true, canPickMany: options?.allowMultiSelect, + matchOnDescription: true, + matchOnDetail: true, }, options?.token, ); diff --git a/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/src/client/pythonEnvironments/creation/provider/condaUtils.ts index 4c4e816f18f1..e00a1c8dca09 100644 --- a/src/client/pythonEnvironments/creation/provider/condaUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -47,6 +47,8 @@ export async function pickPythonVersion(token?: CancellationToken): Promise Date: Wed, 28 Jun 2023 14:45:33 -0700 Subject: [PATCH 0054/1136] Add GDPR tag to method property (#21522) --- src/client/telemetry/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 7881ada5653c..50d7ebb0f9b1 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1543,7 +1543,9 @@ export interface IEventNamePropertyMapping { * This event also has a measure, "resultLength", which records the number of completions provided. */ /* __GDPR__ - "jedi_language_server.request" : { "owner": "karthiknadig" } + "jedi_language_server.request" : { + "method": {"classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig"} + } */ [EventName.JEDI_LANGUAGE_SERVER_REQUEST]: unknown; /** From 26748427355bbf7cd0fd2528ea5627c137f6944f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 29 Jun 2023 13:15:00 -0700 Subject: [PATCH 0055/1136] rewrite should respect settings.testing.cwd (#21539) fixes https://github.com/microsoft/vscode-python/issues/21531 --- .../pytest/pytestDiscoveryAdapter.ts | 3 +- .../pytest/pytestExecutionAdapter.ts | 7 +-- .../unittest/testDiscoveryAdapter.ts | 8 ++- .../unittest/testExecutionAdapter.ts | 10 ++-- .../pytestDiscoveryAdapter.unit.test.ts | 5 +- .../pytestExecutionAdapter.unit.test.ts | 49 +++++++++++++++++++ .../testDiscoveryAdapter.unit.test.ts | 37 ++++++++++++++ .../testExecutionAdapter.unit.test.ts | 39 +++++++++++++++ 8 files changed, 141 insertions(+), 17 deletions(-) diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 211c322d67f1..b83224d4161b 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -55,12 +55,13 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const uuid = this.testServer.createUUID(uri.fsPath); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); const spawnOptions: SpawnOptions = { - cwd: uri.fsPath, + cwd, throwOnStdErr: true, extraVariables: { PYTHONPATH: pythonPathCommand, diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index b68b80945ef3..a75a6089627c 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -84,12 +84,13 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { this.configSettings.isTestExecution(); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); const spawnOptions: SpawnOptions = { - cwd: uri.fsPath, + cwd, throwOnStdErr: true, extraVariables: { PYTHONPATH: pythonPathCommand, @@ -131,7 +132,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const pytestPort = this.testServer.getPort().toString(); const pytestUUID = uuid.toString(); const launchOptions: LaunchOptions = { - cwd: uri.fsPath, + cwd, args: testArgs, token: spawnOptions.token, testProvider: PYTEST_PROVIDER, @@ -156,7 +157,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { return Promise.reject(ex); } - const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; + const executionPayload: ExecutionTestPayload = { cwd, status: 'success', error: '' }; return executionPayload; } } diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 69438e341f14..6deca55117ea 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -19,8 +19,6 @@ import { * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. */ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { - private cwd: string | undefined; - constructor( public testServer: ITestServer, public configSettings: IConfigurationService, @@ -31,16 +29,16 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { public async discoverTests(uri: Uri): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const command = buildDiscoveryCommand(unittestArgs); - this.cwd = uri.fsPath; const uuid = this.testServer.createUUID(uri.fsPath); const options: TestCommandOptions = { workspaceFolder: uri, command, - cwd: this.cwd, + cwd, uuid, outChannel: this.outputChannel, }; @@ -57,7 +55,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // placeholder until after the rewrite is adopted // TODO: remove after adoption. const discoveryPayload: DiscoveredTestPayload = { - cwd: uri.fsPath, + cwd, status: 'success', }; return discoveryPayload; diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index bf3038817f8b..4cab941c2608 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -23,8 +23,6 @@ import { startTestIdServer } from '../common/utils'; */ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { - private cwd: string | undefined; - constructor( public testServer: ITestServer, public configSettings: IConfigurationService, @@ -62,15 +60,15 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { debugBool?: boolean, ): Promise { const settings = this.configSettings.getSettings(uri); - const { cwd, unittestArgs } = settings.testing; + const { unittestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const command = buildExecutionCommand(unittestArgs); - this.cwd = cwd || uri.fsPath; const options: TestCommandOptions = { workspaceFolder: uri, command, - cwd: this.cwd, + cwd, uuid, debugBool, testIds, @@ -87,7 +85,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. - const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; + const executionPayload: ExecutionTestPayload = { cwd, status: 'success', error: '' }; return executionPayload; } } diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 0286235be1bf..18212b2d1032 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -105,9 +105,10 @@ suite('pytest test discovery adapter', () => { }); test('Test discovery correctly pulls pytest args from config service settings', async () => { // set up a config service with different pytest args + const expectedPathNew = path.join('other', 'path'); const configServiceNew: IConfigurationService = ({ getSettings: () => ({ - testing: { pytestArgs: ['.', 'abc', 'xyz'] }, + testing: { pytestArgs: ['.', 'abc', 'xyz'], cwd: expectedPathNew }, }), } as unknown) as IConfigurationService; @@ -120,7 +121,7 @@ suite('pytest test discovery adapter', () => { expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); - assert.equal(options.cwd, expectedPath); + assert.equal(options.cwd, expectedPathNew); assert.equal(options.throwOnStdErr, true); return true; }), diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 9f4c41ba8309..44116fd753b0 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -138,6 +138,55 @@ suite('pytest test execution adapter', () => { typeMoq.Times.once(), ); }); + test('pytest execution respects settings.testing.cwd when present', async () => { + const newCwd = path.join('new', 'path'); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'], cwd: newCwd }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + const uri = Uri.file(myTestPath); + const uuid = 'uuid123'; + testServer + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); + const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); + adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); + await adapter.runTests(uri, [], false, testRun.object, execFactory.object); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); + const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); + const expectedArgs = [pathToPythonScript, '--rootdir', myTestPath]; + const expectedExtraVariables = { + PYTHONPATH: pathToPythonFiles, + TEST_UUID: 'uuid123', + TEST_PORT: '12345', + }; + + execService.verify( + (x) => + x.exec( + expectedArgs, + typeMoq.It.is((options) => { + assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.extraVariables?.TEST_UUID, expectedExtraVariables.TEST_UUID); + assert.equal(options.extraVariables?.TEST_PORT, expectedExtraVariables.TEST_PORT); + assert.equal(options.extraVariables?.RUN_TEST_IDS_PORT, '54321'); + assert.equal(options.cwd, newCwd); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); test('Debug launched correctly for pytest', async () => { const uri = Uri.file(myTestPath); const uuid = 'uuid123'; diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index ef21655e93e4..dc883afdf441 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -54,4 +54,41 @@ suite('Unittest test discovery adapter', () => { uuid: '123456789', }); }); + test('DiscoverTests should respect settings.testings.cwd when present', async () => { + let options: TestCommandOptions | undefined; + stubConfigSettings = ({ + getSettings: () => ({ + testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'], cwd: '/foo' }, + }), + } as unknown) as IConfigurationService; + + const stubTestServer = ({ + sendCommand(opt: TestCommandOptions): Promise { + delete opt.outChannel; + options = opt; + return Promise.resolve(); + }, + onDiscoveryDataReceived: () => { + // no body + }, + createUUID: () => '123456789', + } as unknown) as ITestServer; + + const uri = Uri.file('/foo/bar'); + const newCwd = '/foo'; + const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'discovery.py'); + + const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); + adapter.discoverTests(uri); + + assert.deepStrictEqual(options, { + workspaceFolder: uri, + cwd: newCwd, + command: { + script, + args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'], + }, + uuid: '123456789', + }); + }); }); diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index 88126225a177..4d4a8d0ebee4 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -62,4 +62,43 @@ suite('Unittest test execution adapter', () => { assert.deepStrictEqual(options, expectedOptions); }); }); + test('runTests should respect settings.testing.cwd when present', async () => { + stubConfigSettings = ({ + getSettings: () => ({ + testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'], cwd: '/foo' }, + }), + } as unknown) as IConfigurationService; + let options: TestCommandOptions | undefined; + + const stubTestServer = ({ + sendCommand(opt: TestCommandOptions, runTestIdPort?: string): Promise { + delete opt.outChannel; + options = opt; + assert(runTestIdPort !== undefined); + return Promise.resolve(); + }, + onRunDataReceived: () => { + // no body + }, + createUUID: () => '123456789', + } as unknown) as ITestServer; + + const newCwd = '/foo'; + const uri = Uri.file('/foo/bar'); + const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); + + const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); + const testIds = ['test1id', 'test2id']; + adapter.runTests(uri, testIds, false).then(() => { + const expectedOptions: TestCommandOptions = { + workspaceFolder: uri, + command: { script, args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'] }, + cwd: newCwd, + uuid: '123456789', + debugBool: false, + testIds, + }; + assert.deepStrictEqual(options, expectedOptions); + }); + }); }); From b1931a3e7fbe3379ef1a820bf71e33af7db90168 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 29 Jun 2023 15:31:06 -0700 Subject: [PATCH 0056/1136] fix bug which merges all parametrize test classes (#21542) fixes https://github.com/microsoft/vscode-python/issues/21541 --- .../pytestadapter/.data/parametrize_tests.py | 7 ++++++ .../expected_discovery_test_output.py | 24 +++++++++++++++++++ pythonFiles/vscode_pytest/__init__.py | 2 +- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py index 9421e0cc0691..6911c9aec7f0 100644 --- a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py +++ b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py @@ -8,3 +8,10 @@ ) def test_adding(actual, expected): assert eval(actual) == expected + + +# Testing pytest with parametrized tests. All three pass. +# The tests ids are parametrize_tests.py::test_under_ten[1] and so on. +@pytest.mark.parametrize("num", range(1, 3)) # test_marker--test_under_ten +def test_under_ten(num): + assert num < 10 diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 33440a6d42fa..0f733c94d141 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -438,6 +438,30 @@ }, ], }, + { + "name": "test_under_ten", + "path": parameterize_tests_path, + "type_": "function", + "children": [ + { + "name": "[1]", + "path": parameterize_tests_path, + "lineno": "15", + "type_": "test", + "id_": "parametrize_tests.py::test_under_ten[1]", + "runID": "parametrize_tests.py::test_under_ten[1]", + }, + { + "name": "[2]", + "path": parameterize_tests_path, + "lineno": "15", + "type_": "test", + "id_": "parametrize_tests.py::test_under_ten[2]", + "runID": "parametrize_tests.py::test_under_ten[2]", + }, + ], + "id_": "parametrize_tests.py::test_under_ten", + }, ], }, ], diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 2aa2f6052d79..3f3aca6a2a64 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -290,7 +290,7 @@ def build_test_tree(session: pytest.Session) -> TestNode: # parameterized test cases cut the repetitive part of the name off. name_split = test_node["name"].split("[") test_node["name"] = "[" + name_split[1] - parent_path = os.fspath(test_case.path) + parent_path = os.fspath(test_case.path) + "::" + name_split[0] try: function_name = test_case.originalname # type: ignore function_test_case = function_nodes_dict[parent_path] From 79bb94f310162df6148141d05d27e1079db1237a Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 29 Jun 2023 15:41:36 -0700 Subject: [PATCH 0057/1136] fix bug where session path is undefined (#21544) fixes https://github.com/microsoft/vscode-python/issues/21540 --- pythonFiles/vscode_pytest/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 3f3aca6a2a64..bdbe16ff8523 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -399,9 +399,10 @@ def create_session_node(session: pytest.Session) -> TestNode: Keyword arguments: session -- the pytest session. """ + session_path = session.path if session.path else pathlib.Path.cwd() return { "name": session.name, - "path": session.path, + "path": session_path, "type_": "folder", "children": [], "id_": os.fspath(session.path), From 1d58cefc1b04252fa3c382dbd40e7ddc09175bb1 Mon Sep 17 00:00:00 2001 From: paulacamargo25 Date: Thu, 29 Jun 2023 17:52:43 -0700 Subject: [PATCH 0058/1136] Fix syspath in discovery unittest (#21546) --- pythonFiles/unittestadapter/discovery.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index ffcd2671addb..cbad40ad1838 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -12,23 +12,14 @@ script_dir = pathlib.Path(__file__).parent.parent sys.path.append(os.fspath(script_dir)) -sys.path.append(os.fspath(script_dir / "lib" / "python")) - -from typing_extensions import Literal - -# Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. -PYTHON_FILES = pathlib.Path(__file__).parent.parent -sys.path.insert(0, os.fspath(PYTHON_FILES)) +sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) from testing_tools import socket_manager # If I use from utils then there will be an import error in test_discovery.py. from unittestadapter.utils import TestNode, build_test_tree, parse_unittest_args -# Add the lib path to sys.path to find the typing_extensions module. -sys.path.insert(0, os.path.join(PYTHON_FILES, "lib", "python")) - -from typing_extensions import NotRequired, TypedDict +from typing_extensions import NotRequired, TypedDict, Literal DEFAULT_PORT = "45454" From af7eb568b0ed9272a9f7e3274571c379c7a89a83 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 29 Jun 2023 23:08:38 -0700 Subject: [PATCH 0059/1136] update to align with sys path appends (#21547) changes to align with https://github.com/microsoft/vscode-python/pull/21546 --- pythonFiles/tests/unittestadapter/test_execution.py | 4 ++-- pythonFiles/unittestadapter/execution.py | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py index a822636bf479..057f64d7396a 100644 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -8,9 +8,9 @@ import pytest -PYTHON_FILES = pathlib.Path(__file__).parent.parent +script_dir = pathlib.Path(__file__).parent.parent +sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -sys.path.insert(0, os.fspath(PYTHON_FILES)) from unittestadapter.execution import parse_execution_cli_args, run_tests TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index aa50cbc5d09f..dfb6928a2074 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -13,13 +13,10 @@ from types import TracebackType from typing import Dict, List, Optional, Tuple, Type, Union -directory_path = pathlib.Path(__file__).parent.parent / "lib" / "python" -# Add the path to pythonFiles to sys.path to find testing_tools.socket_manager. -PYTHON_FILES = pathlib.Path(__file__).parent.parent +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -sys.path.insert(0, os.fspath(PYTHON_FILES)) -# Add the lib path to sys.path to find the typing_extensions module. -sys.path.insert(0, os.path.join(PYTHON_FILES, "lib", "python")) from testing_tools import process_json_util, socket_manager from typing_extensions import NotRequired, TypeAlias, TypedDict from unittestadapter.utils import parse_unittest_args From f37f88367bedaae035c95d6d45763d9218bed933 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 30 Jun 2023 07:41:59 -0700 Subject: [PATCH 0060/1136] Fix path for pytest Nodes (#21548) Fixes https://github.com/microsoft/vscode-python/issues/21540 --- pythonFiles/vscode_pytest/__init__.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index bdbe16ff8523..b880b26fd5b8 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -290,7 +290,7 @@ def build_test_tree(session: pytest.Session) -> TestNode: # parameterized test cases cut the repetitive part of the name off. name_split = test_node["name"].split("[") test_node["name"] = "[" + name_split[1] - parent_path = os.fspath(test_case.path) + "::" + name_split[0] + parent_path = os.fspath(get_node_path(test_case)) + "::" + name_split[0] try: function_name = test_case.originalname # type: ignore function_test_case = function_nodes_dict[parent_path] @@ -303,7 +303,7 @@ def build_test_tree(session: pytest.Session) -> TestNode: ) except KeyError: function_test_case: TestNode = create_parameterized_function_node( - function_name, test_case.path, test_case.nodeid + function_name, get_node_path(test_case), test_case.nodeid ) function_nodes_dict[parent_path] = function_test_case function_test_case["children"].append(test_node) @@ -354,7 +354,7 @@ def build_nested_folders( # Begin the iterator_path one level above the current file. iterator_path = file_node["path"].parent - while iterator_path != session.path: + while iterator_path != get_node_path(session): curr_folder_name = iterator_path.name try: curr_folder_node: TestNode = created_files_folders_dict[ @@ -385,7 +385,7 @@ def create_test_node( ) return { "name": test_case.name, - "path": test_case.path, + "path": get_node_path(test_case), "lineno": test_case_loc, "type_": "test", "id_": test_case.nodeid, @@ -399,13 +399,13 @@ def create_session_node(session: pytest.Session) -> TestNode: Keyword arguments: session -- the pytest session. """ - session_path = session.path if session.path else pathlib.Path.cwd() + node_path = get_node_path(session) return { "name": session.name, - "path": session_path, + "path": node_path, "type_": "folder", "children": [], - "id_": os.fspath(session.path), + "id_": os.fspath(node_path), } @@ -417,7 +417,7 @@ def create_class_node(class_module: pytest.Class) -> TestNode: """ return { "name": class_module.name, - "path": class_module.path, + "path": get_node_path(class_module), "type_": "class", "children": [], "id_": class_module.nodeid, @@ -451,11 +451,12 @@ def create_file_node(file_module: Any) -> TestNode: Keyword arguments: file_module -- the pytest file module. """ + node_path = get_node_path(file_module) return { - "name": file_module.path.name, - "path": file_module.path, + "name": node_path.name, + "path": node_path, "type_": "file", - "id_": os.fspath(file_module.path), + "id_": os.fspath(node_path), "children": [], } @@ -497,6 +498,10 @@ class ExecutionPayloadDict(Dict): error: Union[str, None] # Currently unused need to check +def get_node_path(node: Any) -> pathlib.Path: + return getattr(node, "path", pathlib.Path(node.fspath)) + + def execution_post( cwd: str, status: Literal["success", "error"], From a5ef6a26bcaa823a16203eb2b6ac1974a5fee8c0 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 5 Jul 2023 10:36:03 -0700 Subject: [PATCH 0061/1136] Update version for release candidate (#21556) --- package-lock.json | 2625 ++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 1399 insertions(+), 1228 deletions(-) diff --git a/package-lock.json b/package-lock.json index f41a215a083e..937531ade78f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.11.0-dev", + "version": "2023.12.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.11.0-dev", + "version": "2023.12.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", @@ -131,6 +131,28 @@ "vscode": "^1.79.0-20230526" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@azure/abort-controller": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", @@ -283,22 +305,260 @@ "@babel/highlight": "^7.10.4" } }, + "node_modules/@babel/compat-data": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", + "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.6.tgz", + "integrity": "sha512-HPIyDa6n+HKw5dEuway3vVAhBboYCtREBMp+IWeseZy6TFtzn6MHkCH2KKYUOC/vKKwgSMHQW4htBOrmuRPXfw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.6", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", + "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-validator-option": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", - "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.15.7", + "@babel/helper-validator-identifier": "^7.22.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -306,6 +566,18 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/parser": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.6.tgz", + "integrity": "sha512-EIQu22vNkceq3LbjAq7knDf/UmtI2qbcNI8GRBlijez6TpQLvSodJPYfydQmNA5buwkxxxa/PVI44jjYZ+/cLw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.17.8", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", @@ -328,6 +600,96 @@ "regenerator-runtime": "^0.13.4" } }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.6.tgz", + "integrity": "sha512-53CijMvKlLIDlOTrdWiHileRddlIiwUIyCKqYa7lYnnPldXCG5dUSN38uT0cA6i7rHWNKJLH0VU/Kxdr1GzB3w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.6", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cspotcode/source-map-consumer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", @@ -585,13 +947,13 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", - "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, "node_modules/@microsoft/1ds-core-js": { @@ -719,6 +1081,15 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, + "node_modules/@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1194,9 +1565,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1318,9 +1689,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1927,13 +2298,6 @@ } } }, - "node_modules/aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "optional": true - }, "node_modules/arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -1980,17 +2344,6 @@ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, - "node_modules/are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "dev": true, - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "node_modules/arg": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", @@ -2769,9 +3122,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.20.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", - "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", "dev": true, "funding": [ { @@ -2781,14 +3134,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001317", - "electron-to-chromium": "^1.4.84", - "escalade": "^3.1.1", - "node-releases": "^2.0.2", - "picocolors": "^1.0.0" + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" }, "bin": { "browserslist": "cli.js" @@ -2976,9 +3332,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001320", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001320.tgz", - "integrity": "sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA==", + "version": "1.0.30001512", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001512.tgz", + "integrity": "sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw==", "dev": true, "funding": [ { @@ -2988,6 +3344,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -3401,9 +3761,9 @@ } }, "node_modules/chownr": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true, "optional": true }, @@ -3475,7 +3835,7 @@ "node_modules/cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", "dev": true, "dependencies": { "string-width": "^1.0.1", @@ -3486,7 +3846,7 @@ "node_modules/cliui/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -3495,7 +3855,7 @@ "node_modules/cliui/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "dependencies": { "ansi-regex": "^2.0.0" @@ -3578,7 +3938,7 @@ "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -3720,13 +4080,6 @@ "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", "dev": true }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true - }, "node_modules/constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -3755,13 +4108,10 @@ } }, "node_modules/convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.1" - } + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "node_modules/copy-descriptor": { "version": "0.1.1", @@ -4357,13 +4707,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true - }, "node_modules/des.js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", @@ -4384,16 +4727,13 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "dev": true, "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/diagnostic-channel": { @@ -4572,9 +4912,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.92", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.92.tgz", - "integrity": "sha512-YAVbvQIcDE/IJ/vzDMjD484/hsRbFPW2qXJPaYTfOhtligmfYEYOep+5QojpaEU9kq6bMvNeC2aG7arYvTHYsA==", + "version": "1.4.450", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.450.tgz", + "integrity": "sha512-BLG5HxSELlrMx7dJ2s+8SFlsCtJp37Zpk2VAxyC6CZtbc+9AJeZHfYHbrlSgdXp6saQ8StMqOTEDaBKgA7u1sw==", "dev": true }, "node_modules/elliptic": { @@ -4694,12 +5034,6 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/error-ex/node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, "node_modules/es-abstract": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.2.tgz", @@ -5412,19 +5746,6 @@ "node": ">= 4" } }, - "node_modules/eslint/node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5437,23 +5758,6 @@ "node": "*" } }, - "node_modules/eslint/node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5463,15 +5767,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -5482,9 +5777,9 @@ } }, "node_modules/eslint/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -5529,18 +5824,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -6216,6 +6499,23 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -6566,44 +6866,13 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, - "node_modules/gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "optional": true, - "dependencies": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "node_modules/gauge/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, - "optional": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=6.9.0" } }, "node_modules/get-caller-file": { @@ -6693,7 +6962,7 @@ "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, "optional": true }, @@ -7029,64 +7298,7 @@ "node": ">= 0.10" } }, - "node_modules/gulp-typescript": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-5.0.1.tgz", - "integrity": "sha512-YuMMlylyJtUSHG1/wuSVTrZp60k1dMEFKYOvDf7OvbAJWrDtxxD4oZon4ancdWwzjj30ztiidhe4VXJniF0pIQ==", - "dev": true, - "dependencies": { - "ansi-colors": "^3.0.5", - "plugin-error": "^1.0.1", - "source-map": "^0.7.3", - "through2": "^3.0.0", - "vinyl": "^2.1.0", - "vinyl-fs": "^3.0.3" - }, - "engines": { - "node": ">= 8" - }, - "peerDependencies": { - "typescript": "~2.7.1 || >=2.8.0-dev || >=2.9.0-dev || ~3.0.0 || >=3.0.0-dev || >=3.1.0-dev || >= 3.2.0-dev || >= 3.3.0-dev" - } - }, - "node_modules/gulp-typescript/node_modules/ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/gulp-typescript/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/gulp-typescript/node_modules/through2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" - } - }, - "node_modules/gulp/node_modules/camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp/node_modules/gulp-cli": { + "node_modules/gulp-cli": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", @@ -7118,28 +7330,25 @@ "node": ">= 0.10" } }, - "node_modules/gulp/node_modules/v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "node_modules/gulp-cli/node_modules/camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" } }, - "node_modules/gulp/node_modules/y18n": { + "node_modules/gulp-cli/node_modules/y18n": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", "dev": true }, - "node_modules/gulp/node_modules/yargs": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz", - "integrity": "sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==", + "node_modules/gulp-cli/node_modules/yargs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", + "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", "dev": true, "dependencies": { "camelcase": "^3.0.0", @@ -7154,19 +7363,67 @@ "string-width": "^1.0.2", "which-module": "^1.0.0", "y18n": "^3.2.1", - "yargs-parser": "5.0.0-security.0" + "yargs-parser": "^5.0.1" } }, - "node_modules/gulp/node_modules/yargs-parser": { - "version": "5.0.0-security.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz", - "integrity": "sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==", + "node_modules/gulp-cli/node_modules/yargs-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", + "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", "dev": true, "dependencies": { "camelcase": "^3.0.0", "object.assign": "^4.1.0" } }, + "node_modules/gulp-typescript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-5.0.1.tgz", + "integrity": "sha512-YuMMlylyJtUSHG1/wuSVTrZp60k1dMEFKYOvDf7OvbAJWrDtxxD4oZon4ancdWwzjj30ztiidhe4VXJniF0pIQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^3.0.5", + "plugin-error": "^1.0.1", + "source-map": "^0.7.3", + "through2": "^3.0.0", + "vinyl": "^2.1.0", + "vinyl-fs": "^3.0.3" + }, + "engines": { + "node": ">= 8" + }, + "peerDependencies": { + "typescript": "~2.7.1 || >=2.8.0-dev || >=2.9.0-dev || ~3.0.0 || >=3.0.0-dev || >=3.1.0-dev || >= 3.2.0-dev || >= 3.3.0-dev" + } + }, + "node_modules/gulp-typescript/node_modules/ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/gulp-typescript/node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/gulp-typescript/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, "node_modules/gulplog": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", @@ -7272,13 +7529,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true - }, "node_modules/has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -7652,7 +7902,7 @@ "node_modules/invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", "dev": true, "engines": { "node": ">=0.10.0" @@ -7711,6 +7961,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "node_modules/is-bigint": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", @@ -7844,7 +8100,7 @@ "node_modules/is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", "dev": true, "dependencies": { "number-is-nan": "^1.0.0" @@ -8127,258 +8383,95 @@ } }, "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "node_modules/is-valid-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-hook": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", - "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", - "dev": true, - "dependencies": { - "append-transform": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.0.tgz", - "integrity": "sha512-Nm4wVHdo7ZXSG30KjZ2Wl5SU/Bw7bDx1PdaiIFzEStdjs0H12mOTncn1GVYuqQSaZxpg87VGBRsVRPGD2cD1AQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/core": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.7.tgz", - "integrity": "sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.7", - "@babel/helpers": "^7.7.4", - "@babel/parser": "^7.7.7", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/core/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/generator": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.7.tgz", - "integrity": "sha512-/AOIBpHh/JU1l0ZFS4kiRCBnLi6OTHzh0RPk3h9isBxkkqELtQNFi1Vr/tiG9p1yfoUdKVwISuXWQR+hwwM4VQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.7.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-function-name": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", - "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", - "dev": true, - "dependencies": { - "@babel/helper-get-function-arity": "^7.7.4", - "@babel/template": "^7.7.4", - "@babel/types": "^7.7.4" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-get-function-arity": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", - "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.7.4" - } + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/helper-split-export-declaration": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", - "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", "dev": true, - "dependencies": { - "@babel/types": "^7.7.4" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/helpers": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.4.tgz", - "integrity": "sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg==", + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", "dev": true, "dependencies": { - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4" + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/parser": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", - "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, "engines": { - "node": ">=6.0.0" + "node": ">=0.10.0" } }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/template": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", - "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4" - } + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/traverse": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", - "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true, - "dependencies": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.4", - "@babel/helper-function-name": "^7.7.4", - "@babel/helper-split-export-declaration": "^7.7.4", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/istanbul-lib-instrument/node_modules/@babel/types": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", - "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", "dev": true, - "dependencies": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/istanbul-lib-instrument/node_modules/convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", "dev": true, "dependencies": { - "safe-buffer": "~5.1.1" + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/istanbul-lib-instrument/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "dependencies": { - "ms": "^2.1.1" + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { @@ -8390,37 +8483,27 @@ "semver": "bin/semver.js" } }, - "node_modules/istanbul-lib-instrument/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/istanbul-lib-processinfo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", - "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, "dependencies": { "archy": "^1.0.0", - "cross-spawn": "^7.0.0", - "istanbul-lib-coverage": "^3.0.0-alpha.1", - "make-dir": "^3.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", - "uuid": "^3.3.3" + "uuid": "^8.3.2" }, "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-processinfo/node_modules/cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -8473,16 +8556,6 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-processinfo/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, "node_modules/istanbul-lib-report": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", @@ -8543,9 +8616,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -8723,15 +8796,15 @@ "dev": true }, "node_modules/keytar": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz", - "integrity": "sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, "hasInstallScript": true, "optional": true, "dependencies": { - "node-addon-api": "^3.0.0", - "prebuild-install": "^6.0.0" + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" } }, "node_modules/keyv": { @@ -8795,7 +8868,7 @@ "node_modules/lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", "dev": true, "dependencies": { "invert-kv": "^1.0.0" @@ -8825,6 +8898,19 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -8862,7 +8948,7 @@ "node_modules/load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", "dev": true, "dependencies": { "graceful-fs": "^4.1.2", @@ -8875,22 +8961,10 @@ "node": ">=0.10.0" } }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "dependencies": { - "error-ex": "^1.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/load-json-file/node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, "engines": { "node": ">=0.10.0" @@ -9995,19 +10069,38 @@ } }, "node_modules/node-abi": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", - "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", + "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", "dev": true, "optional": true, "dependencies": { - "semver": "^5.4.1" + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "dev": true, "optional": true }, @@ -10253,9 +10346,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", "dev": true }, "node_modules/node-stream-zip": { @@ -10350,19 +10443,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "optional": true, - "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "node_modules/nth-check": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", @@ -10378,7 +10458,7 @@ "node_modules/number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", "dev": true, "engines": { "node": ">=0.10.0" @@ -10425,32 +10505,6 @@ "node": ">=8.9" } }, - "node_modules/nyc/node_modules/convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/nyc/node_modules/find-cache-dir": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.2.0.tgz", - "integrity": "sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.0", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, "node_modules/nyc/node_modules/p-map": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", @@ -10764,6 +10818,23 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", @@ -10782,7 +10853,7 @@ "node_modules/os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", "dev": true, "dependencies": { "lcid": "^1.0.0" @@ -10965,6 +11036,18 @@ "node": ">=0.8" } }, + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -10986,7 +11069,7 @@ "node_modules/parse-semver": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha1-mkr9bfBj3Egm+T+6SpnPIj9mbLg=", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "dependencies": { "semver": "^5.1.0" @@ -11231,23 +11314,22 @@ } }, "node_modules/prebuild-install": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", - "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dev": true, "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", - "node-abi": "^2.21.0", - "npmlog": "^4.0.1", + "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", - "simple-get": "^3.0.3", + "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, @@ -11255,7 +11337,7 @@ "prebuild-install": "bin.js" }, "engines": { - "node": ">=6" + "node": ">=10" } }, "node_modules/prebuild-install/node_modules/pump": { @@ -11269,6 +11351,15 @@ "once": "^1.3.1" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", @@ -11490,7 +11581,7 @@ "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "optional": true, "engines": { @@ -11518,7 +11609,7 @@ "node_modules/read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", "dev": true, "dependencies": { "load-json-file": "^1.0.0", @@ -11532,7 +11623,7 @@ "node_modules/read-pkg-up": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", "dev": true, "dependencies": { "find-up": "^1.0.0", @@ -11545,7 +11636,7 @@ "node_modules/read-pkg-up/node_modules/find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", "dev": true, "dependencies": { "path-exists": "^2.0.0", @@ -11558,7 +11649,7 @@ "node_modules/read-pkg-up/node_modules/path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", "dev": true, "dependencies": { "pinkie-promise": "^2.0.0" @@ -11570,7 +11661,7 @@ "node_modules/read-pkg/node_modules/path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", "dev": true, "dependencies": { "graceful-fs": "^4.1.2", @@ -11584,7 +11675,7 @@ "node_modules/read-pkg/node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, "engines": { "node": ">=0.10.0" @@ -11797,7 +11888,7 @@ "node_modules/require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", "dev": true }, "node_modules/resolve": { @@ -12214,38 +12305,55 @@ "optional": true }, "node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "optional": true, "dependencies": { - "decompress-response": "^4.2.0", + "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "node_modules/simple-get/node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "optional": true, "dependencies": { - "mimic-response": "^2.0.0" + "mimic-response": "^3.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/simple-get/node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "optional": true, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -12612,9 +12720,9 @@ } }, "node_modules/spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -12622,15 +12730,15 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true }, "node_modules/spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "dependencies": { "spdx-exceptions": "^2.1.0", @@ -12638,9 +12746,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true }, "node_modules/split-string": { @@ -12775,7 +12883,7 @@ "node_modules/string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", "dev": true, "dependencies": { "code-point-at": "^1.0.0", @@ -12789,7 +12897,7 @@ "node_modules/string-width/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -12798,7 +12906,7 @@ "node_modules/string-width/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "dependencies": { "ansi-regex": "^2.0.0" @@ -12867,7 +12975,7 @@ "node_modules/strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", "dev": true, "dependencies": { "is-utf8": "^0.2.0" @@ -13064,9 +13172,9 @@ } }, "node_modules/tar-fs/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "optional": true, "dependencies": { @@ -13300,7 +13408,7 @@ "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true, "engines": { "node": ">=4" @@ -13524,9 +13632,9 @@ } }, "node_modules/ts-loader/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -13766,7 +13874,7 @@ "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "optional": true, "dependencies": { @@ -13782,6 +13890,18 @@ "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==", "dev": true }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -14073,6 +14193,36 @@ "yarn": "*" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -14183,6 +14333,18 @@ "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==", "dev": true }, + "node_modules/v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -14331,9 +14493,9 @@ } }, "node_modules/vscode-languageclient/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -14734,7 +14896,7 @@ "node_modules/which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", "dev": true }, "node_modules/which-typed-array": { @@ -14757,16 +14919,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "optional": true, - "dependencies": { - "string-width": "^1.0.2 || 2" - } - }, "node_modules/wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", @@ -14793,15 +14945,6 @@ "wipe-node-cache": "^2.1.0" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/workerpool": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", @@ -14811,7 +14954,7 @@ "node_modules/wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", "dev": true, "dependencies": { "string-width": "^1.0.1", @@ -14824,7 +14967,7 @@ "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -14833,7 +14976,7 @@ "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "dependencies": { "ansi-regex": "^2.0.0" @@ -15163,6 +15306,22 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "@azure/abort-controller": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", @@ -15295,23 +15454,211 @@ "@babel/highlight": "^7.10.4" } }, + "@babel/compat-data": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", + "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", + "dev": true + }, + "@babel/core": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.6.tgz", + "integrity": "sha512-HPIyDa6n+HKw5dEuway3vVAhBboYCtREBMp+IWeseZy6TFtzn6MHkCH2KKYUOC/vKKwgSMHQW4htBOrmuRPXfw==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.6", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.5" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } + } + }, + "@babel/generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", + "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-validator-option": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, "@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", "dev": true }, + "@babel/helpers": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5" + } + }, "@babel/highlight": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", - "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.15.7", + "@babel/helper-validator-identifier": "^7.22.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, + "@babel/parser": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.6.tgz", + "integrity": "sha512-EIQu22vNkceq3LbjAq7knDf/UmtI2qbcNI8GRBlijez6TpQLvSodJPYfydQmNA5buwkxxxa/PVI44jjYZ+/cLw==", + "dev": true + }, "@babel/runtime": { "version": "7.17.8", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", @@ -15331,6 +15678,77 @@ "regenerator-runtime": "^0.13.4" } }, + "@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.5" + } + } + } + }, + "@babel/traverse": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.6.tgz", + "integrity": "sha512-53CijMvKlLIDlOTrdWiHileRddlIiwUIyCKqYa7lYnnPldXCG5dUSN38uT0cA6i7rHWNKJLH0VU/Kxdr1GzB3w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.6", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.5" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } + } + }, + "@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + } + }, "@cspotcode/source-map-consumer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", @@ -15519,13 +15937,13 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", - "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, "@microsoft/1ds-core-js": { @@ -15638,6 +16056,12 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, + "@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -16050,9 +16474,9 @@ } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -16118,9 +16542,9 @@ } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -16611,13 +17035,6 @@ "diagnostic-channel-publishers": "1.0.5" } }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "optional": true - }, "arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -16646,17 +17063,6 @@ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, - "are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "arg": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", @@ -17296,16 +17702,15 @@ } }, "browserslist": { - "version": "4.20.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", - "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001317", - "electron-to-chromium": "^1.4.84", - "escalade": "^3.1.1", - "node-releases": "^2.0.2", - "picocolors": "^1.0.0" + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" } }, "buffer": { @@ -17451,9 +17856,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001320", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001320.tgz", - "integrity": "sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA==", + "version": "1.0.30001512", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001512.tgz", + "integrity": "sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw==", "dev": true }, "caseless": { @@ -17755,9 +18160,9 @@ } }, "chownr": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true, "optional": true }, @@ -17818,7 +18223,7 @@ "cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", "dev": true, "requires": { "string-width": "^1.0.1", @@ -17829,13 +18234,13 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "requires": { "ansi-regex": "^2.0.0" @@ -17905,7 +18310,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", "dev": true }, "collection-map": { @@ -18028,13 +18433,6 @@ "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", "dev": true }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true - }, "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -18060,13 +18458,10 @@ } }, "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "copy-descriptor": { "version": "0.1.1", @@ -18557,13 +18952,6 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true - }, "des.js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", @@ -18581,9 +18969,9 @@ "dev": true }, "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "dev": true, "optional": true }, @@ -18741,9 +19129,9 @@ } }, "electron-to-chromium": { - "version": "1.4.92", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.92.tgz", - "integrity": "sha512-YAVbvQIcDE/IJ/vzDMjD484/hsRbFPW2qXJPaYTfOhtligmfYEYOep+5QojpaEU9kq6bMvNeC2aG7arYvTHYsA==", + "version": "1.4.450", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.450.tgz", + "integrity": "sha512-BLG5HxSELlrMx7dJ2s+8SFlsCtJp37Zpk2VAxyC6CZtbc+9AJeZHfYHbrlSgdXp6saQ8StMqOTEDaBKgA7u1sw==", "dev": true }, "elliptic": { @@ -18844,14 +19232,6 @@ "dev": true, "requires": { "is-arrayish": "^0.2.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - } } }, "es-abstract": { @@ -19129,16 +19509,6 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -19148,32 +19518,12 @@ "brace-expansion": "^1.1.7" } }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -19181,9 +19531,9 @@ "dev": true }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -19213,15 +19563,6 @@ "has-flag": "^4.0.0" } }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -20034,6 +20375,17 @@ "integrity": "sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg==", "dev": true }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -20292,59 +20644,29 @@ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, "requires": { - "glob": "^7.1.3" - } - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" + "glob": "^7.1.3" } } } }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -20405,7 +20727,7 @@ "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, "optional": true }, @@ -20675,49 +20997,40 @@ "gulp-cli": "^2.2.0", "undertaker": "^1.2.1", "vinyl-fs": "^3.0.0" + } + }, + "gulp-cli": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", + "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "archy": "^1.0.0", + "array-sort": "^1.0.0", + "color-support": "^1.1.3", + "concat-stream": "^1.6.0", + "copy-props": "^2.0.1", + "fancy-log": "^1.3.2", + "gulplog": "^1.0.0", + "interpret": "^1.4.0", + "isobject": "^3.0.1", + "liftoff": "^3.1.0", + "matchdep": "^2.0.0", + "mute-stdout": "^1.0.0", + "pretty-hrtime": "^1.0.0", + "replace-homedir": "^1.0.0", + "semver-greatest-satisfied-range": "^1.1.0", + "v8flags": "^3.2.0", + "yargs": "^7.1.0" }, "dependencies": { "camelcase": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", "dev": true }, - "gulp-cli": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", - "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", - "dev": true, - "requires": { - "ansi-colors": "^1.0.1", - "archy": "^1.0.0", - "array-sort": "^1.0.0", - "color-support": "^1.1.3", - "concat-stream": "^1.6.0", - "copy-props": "^2.0.1", - "fancy-log": "^1.3.2", - "gulplog": "^1.0.0", - "interpret": "^1.4.0", - "isobject": "^3.0.1", - "liftoff": "^3.1.0", - "matchdep": "^2.0.0", - "mute-stdout": "^1.0.0", - "pretty-hrtime": "^1.0.0", - "replace-homedir": "^1.0.0", - "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.2.0", - "yargs": "^7.1.0" - } - }, - "v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, "y18n": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", @@ -20725,9 +21038,9 @@ "dev": true }, "yargs": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz", - "integrity": "sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", + "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", "dev": true, "requires": { "camelcase": "^3.0.0", @@ -20742,13 +21055,13 @@ "string-width": "^1.0.2", "which-module": "^1.0.0", "y18n": "^3.2.1", - "yargs-parser": "5.0.0-security.0" + "yargs-parser": "^5.0.1" } }, "yargs-parser": { - "version": "5.0.0-security.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz", - "integrity": "sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", + "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", "dev": true, "requires": { "camelcase": "^3.0.0", @@ -20864,13 +21177,6 @@ "has-symbols": "^1.0.2" } }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true - }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -21152,7 +21458,7 @@ "invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", "dev": true }, "is-absolute": { @@ -21195,6 +21501,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "is-bigint": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", @@ -21293,7 +21605,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", "dev": true, "requires": { "number-is-nan": "^1.0.0" @@ -21532,9 +21844,9 @@ "dev": true }, "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", "dev": true }, "istanbul-lib-hook": { @@ -21547,198 +21859,43 @@ } }, "istanbul-lib-instrument": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.0.tgz", - "integrity": "sha512-Nm4wVHdo7ZXSG30KjZ2Wl5SU/Bw7bDx1PdaiIFzEStdjs0H12mOTncn1GVYuqQSaZxpg87VGBRsVRPGD2cD1AQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "requires": { "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.0.0", "semver": "^6.3.0" }, "dependencies": { - "@babel/core": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.7.tgz", - "integrity": "sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.7", - "@babel/helpers": "^7.7.4", - "@babel/parser": "^7.7.7", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.7.tgz", - "integrity": "sha512-/AOIBpHh/JU1l0ZFS4kiRCBnLi6OTHzh0RPk3h9isBxkkqELtQNFi1Vr/tiG9p1yfoUdKVwISuXWQR+hwwM4VQ==", - "dev": true, - "requires": { - "@babel/types": "^7.7.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", - "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.7.4", - "@babel/template": "^7.7.4", - "@babel/types": "^7.7.4" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", - "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", - "dev": true, - "requires": { - "@babel/types": "^7.7.4" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", - "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", - "dev": true, - "requires": { - "@babel/types": "^7.7.4" - } - }, - "@babel/helpers": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.4.tgz", - "integrity": "sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg==", - "dev": true, - "requires": { - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4" - } - }, - "@babel/parser": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", - "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", - "dev": true - }, - "@babel/template": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", - "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4" - } - }, - "@babel/traverse": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", - "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.4", - "@babel/helper-function-name": "^7.7.4", - "@babel/helper-split-export-declaration": "^7.7.4", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", - "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true } } }, "istanbul-lib-processinfo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", - "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, "requires": { "archy": "^1.0.0", - "cross-spawn": "^7.0.0", - "istanbul-lib-coverage": "^3.0.0-alpha.1", - "make-dir": "^3.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", "rimraf": "^3.0.0", - "uuid": "^3.3.3" + "uuid": "^8.3.2" }, "dependencies": { "cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -21775,12 +21932,6 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true } } }, @@ -21835,9 +21986,9 @@ } }, "istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -21976,14 +22127,14 @@ "dev": true }, "keytar": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz", - "integrity": "sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, "optional": true, "requires": { - "node-addon-api": "^3.0.0", - "prebuild-install": "^6.0.0" + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" } }, "keyv": { @@ -22038,7 +22189,7 @@ "lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", "dev": true, "requires": { "invert-kv": "^1.0.0" @@ -22059,6 +22210,16 @@ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, "liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -22093,7 +22254,7 @@ "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -22103,19 +22264,10 @@ "strip-bom": "^2.0.0" }, "dependencies": { - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true } } @@ -22986,19 +23138,31 @@ } }, "node-abi": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", - "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", + "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", "dev": true, "optional": true, "requires": { - "semver": "^5.4.1" + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } + } } }, "node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "dev": true, "optional": true }, @@ -23202,9 +23366,9 @@ } }, "node-releases": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", "dev": true }, "node-stream-zip": { @@ -23278,19 +23442,6 @@ } } }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "nth-check": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", @@ -23303,7 +23454,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", "dev": true }, "nyc": { @@ -23341,26 +23492,6 @@ "yargs": "^15.0.2" }, "dependencies": { - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "find-cache-dir": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.2.0.tgz", - "integrity": "sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.0", - "pkg-dir": "^4.1.0" - } - }, "p-map": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", @@ -23597,6 +23728,20 @@ "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", "dev": true }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, "ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", @@ -23615,7 +23760,7 @@ "os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", "dev": true, "requires": { "lcid": "^1.0.0" @@ -23749,6 +23894,15 @@ "path-root": "^0.1.1" } }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, "parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -23764,7 +23918,7 @@ "parse-semver": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha1-mkr9bfBj3Egm+T+6SpnPIj9mbLg=", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "requires": { "semver": "^5.1.0" @@ -23958,23 +24112,22 @@ "dev": true }, "prebuild-install": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", - "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dev": true, "optional": true, "requires": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", - "node-abi": "^2.21.0", - "npmlog": "^4.0.1", + "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", - "simple-get": "^3.0.3", + "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, @@ -23992,6 +24145,12 @@ } } }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, "prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", @@ -24159,7 +24318,7 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "optional": true } @@ -24183,7 +24342,7 @@ "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", "dev": true, "requires": { "load-json-file": "^1.0.0", @@ -24194,7 +24353,7 @@ "path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -24205,7 +24364,7 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true } } @@ -24213,7 +24372,7 @@ "read-pkg-up": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", "dev": true, "requires": { "find-up": "^1.0.0", @@ -24223,7 +24382,7 @@ "find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", "dev": true, "requires": { "path-exists": "^2.0.0", @@ -24233,7 +24392,7 @@ "path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", "dev": true, "requires": { "pinkie-promise": "^2.0.0" @@ -24402,7 +24561,7 @@ "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", "dev": true }, "resolve": { @@ -24719,31 +24878,31 @@ "optional": true }, "simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, "optional": true, "requires": { - "decompress-response": "^4.2.0", + "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" }, "dependencies": { "decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "optional": true, "requires": { - "mimic-response": "^2.0.0" + "mimic-response": "^3.1.0" } }, "mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "optional": true } @@ -25033,9 +25192,9 @@ } }, "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -25043,15 +25202,15 @@ } }, "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true }, "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "requires": { "spdx-exceptions": "^2.1.0", @@ -25059,9 +25218,9 @@ } }, "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true }, "split-string": { @@ -25171,7 +25330,7 @@ "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", "dev": true, "requires": { "code-point-at": "^1.0.0", @@ -25182,13 +25341,13 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "requires": { "ansi-regex": "^2.0.0" @@ -25244,7 +25403,7 @@ "strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", "dev": true, "requires": { "is-utf8": "^0.2.0" @@ -25403,9 +25562,9 @@ } }, "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "optional": true, "requires": { @@ -25586,7 +25745,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true }, "to-object-path": { @@ -25748,9 +25907,9 @@ } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -25926,7 +26085,7 @@ "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "optional": true, "requires": { @@ -25939,6 +26098,15 @@ "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==", "dev": true }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -26177,6 +26345,16 @@ "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true }, + "update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -26278,6 +26456,15 @@ "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==", "dev": true }, + "v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -26404,9 +26591,9 @@ }, "dependencies": { "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "requires": { "lru-cache": "^6.0.0" } @@ -26690,7 +26877,7 @@ "which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", "dev": true }, "which-typed-array": { @@ -26707,16 +26894,6 @@ "is-typed-array": "^1.1.7" } }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, "wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", @@ -26743,12 +26920,6 @@ "wipe-node-cache": "^2.1.0" } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, "workerpool": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", @@ -26758,7 +26929,7 @@ "wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", "dev": true, "requires": { "string-width": "^1.0.1", @@ -26768,13 +26939,13 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "requires": { "ansi-regex": "^2.0.0" diff --git a/package.json b/package.json index 85d4baa31e4b..769728ac1549 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2023.11.0-dev", + "version": "2023.12.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From 66270f0a44cb8693766cea5ff1f184dc24ad146c Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 5 Jul 2023 11:02:08 -0700 Subject: [PATCH 0062/1136] Update main to next pre-release (#21559) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 937531ade78f..b042f0d35cfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.12.0-rc", + "version": "2023.13.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.12.0-rc", + "version": "2023.13.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 769728ac1549..93db9d7263e8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", - "version": "2023.12.0-rc", + "version": "2023.13.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 4f15a504f7c4467b9c6e727c0afbdff4fe5e1c75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Jul 2023 11:02:58 -0700 Subject: [PATCH 0063/1136] Bump semver from 5.7.1 to 7.5.2 (#21489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 7.5.2.

Release notes

Sourced from semver's releases.

v7.5.2

7.5.2 (2023-06-15)

Bug Fixes

v7.5.1

7.5.1 (2023-05-12)

Bug Fixes

v7.5.0

7.5.0 (2023-04-17)

Features

Bug Fixes

v7.4.0

7.4.0 (2023-04-10)

Features

Bug Fixes

Documentation

... (truncated)

Changelog

Sourced from semver's changelog.

7.5.2 (2023-06-15)

Bug Fixes

7.5.1 (2023-05-12)

Bug Fixes

7.5.0 (2023-04-17)

Features

Bug Fixes

7.4.0 (2023-04-10)

Features

Bug Fixes

Documentation

7.3.8 (2022-10-04)

Bug Fixes

... (truncated)

Commits
  • e7b78de chore: release 7.5.2
  • 58c791f fix: diff when detecting major change from prerelease (#566)
  • 5c8efbc fix: preserve build in raw after inc (#565)
  • 717534e fix: better handling of whitespace (#564)
  • 2f738e9 chore: bump @​npmcli/template-oss from 4.14.1 to 4.15.1 (#558)
  • aa016a6 chore: release 7.5.1
  • d30d25a fix: show type on invalid semver error (#559)
  • 09c69e2 chore: bump @​npmcli/template-oss from 4.13.0 to 4.14.1 (#555)
  • 5b02ad7 chore: release 7.5.0
  • e219bb4 fix: throw on bad version with correct error message (#552)
  • Additional commits viewable in compare view
Maintainer changes

This version was pushed to npm by npm-cli-ops, a new releaser for semver since your current version.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=semver&package-manager=npm_and_yarn&previous-version=5.7.1&new-version=7.5.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 164 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 2 +- 2 files changed, 157 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index b042f0d35cfe..e6042d7638d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "reflect-metadata": "^0.1.12", "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", - "semver": "^5.5.0", + "semver": "^7.5.2", "stack-trace": "0.0.10", "sudo-prompt": "^9.2.1", "tmp": "^0.0.33", @@ -1850,6 +1850,15 @@ "node": "*" } }, + "node_modules/@vscode/vsce/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/@vscode/vsce/node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -2684,6 +2693,14 @@ "node": "<=0.11.8 || >0.11.10" } }, + "node_modules/async-listener/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/async-settle": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", @@ -3935,6 +3952,14 @@ "node": "^4.7 || >=6.9 || >=7.3 || >=8.2.1" } }, + "node_modules/cls-hooked/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -4249,6 +4274,15 @@ "node": ">=4.8" } }, + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/cross-spawn/node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -4752,6 +4786,14 @@ "diagnostic-channel": "*" } }, + "node_modules/diagnostic-channel/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -4868,6 +4910,15 @@ "once": "^1.3.1" } }, + "node_modules/download/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -10068,6 +10119,15 @@ "ms": "^2.1.1" } }, + "node_modules/nock/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/node-abi": { "version": "3.45.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", @@ -10375,6 +10435,15 @@ "validate-npm-package-license": "^3.0.1" } }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -11075,6 +11144,15 @@ "semver": "^5.1.0" } }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", @@ -12137,11 +12215,17 @@ } }, "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { - "semver": "bin/semver" + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/semver-greatest-satisfied-range": { @@ -16666,6 +16750,12 @@ "brace-expansion": "^1.1.7" } }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -17328,6 +17418,13 @@ "requires": { "semver": "^5.3.0", "shimmer": "^1.1.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } } }, "async-settle": { @@ -18305,6 +18402,13 @@ "async-hook-jl": "^1.7.6", "emitter-listener": "^1.0.1", "semver": "^5.4.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } } }, "code-point-at": { @@ -18580,6 +18684,12 @@ "which": "^1.2.9" }, "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -18981,6 +19091,13 @@ "integrity": "sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ==", "requires": { "semver": "^5.3.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } } }, "diagnostic-channel-publishers": { @@ -19082,6 +19199,12 @@ "end-of-stream": "^1.1.0", "once": "^1.3.1" } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, @@ -23134,6 +23257,12 @@ "requires": { "ms": "^2.1.1" } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, @@ -23386,6 +23515,14 @@ "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "normalize-path": { @@ -23922,6 +24059,14 @@ "dev": true, "requires": { "semver": "^5.1.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "parse5-htmlparser2-tree-adapter": { @@ -24749,9 +24894,12 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "requires": { + "lru-cache": "^6.0.0" + } }, "semver-greatest-satisfied-range": { "version": "1.1.0", diff --git a/package.json b/package.json index 93db9d7263e8..715179858e24 100644 --- a/package.json +++ b/package.json @@ -1998,7 +1998,7 @@ "reflect-metadata": "^0.1.12", "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", - "semver": "^5.5.0", + "semver": "^7.5.2", "stack-trace": "0.0.10", "sudo-prompt": "^9.2.1", "tmp": "^0.0.33", From 724a0fd6625cc36ac38abb8720b2fdbad96f2c73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Jul 2023 11:06:52 -0700 Subject: [PATCH 0064/1136] Bump typing-extensions from 4.6.3 to 4.7.1 (#21552) Bumps [typing-extensions](https://github.com/python/typing_extensions) from 4.6.3 to 4.7.1.
Release notes

Sourced from typing-extensions's releases.

4.7.1

  • Fix support for TypedDict, NamedTuple and is_protocol on PyPy-3.7 and PyPy-3.8. Patch by Alex Waygood. Note that PyPy-3.7 and PyPy-3.8 are unsupported by the PyPy project. The next feature release of typing-extensions will drop support for PyPy-3.7 and may also drop support for PyPy-3.8.

4.7.0

This is a feature release. Major changes include:

  • All non-deprecated names from typing are now re-exported by typing_extensions for convenience
  • Add typing_extensions.get_protocol_members and typing_extensions.is_protocol
  • Declare support for Python 3.12
  • This will be the last feature release to support Python 3.7, which recently reached its end-of-life

Full changelog of versions 4.7.0 and 4.7.0rc1:

Release 4.7.0 (June 28, 2023)

  • This is expected to be the last feature release supporting Python 3.7, which reaches its end of life on June 27, 2023. Version 4.8.0 will support only Python 3.8.0 and up.
  • Fix bug where a typing_extensions.Protocol class that had one or more non-callable members would raise TypeError when issubclass() was called against it, even if it defined a custom __subclasshook__ method. The correct behaviour -- which has now been restored -- is not to raise TypeError in these situations if a custom __subclasshook__ method is defined. Patch by Alex Waygood (backporting python/cpython#105976).

Release 4.7.0rc1 (June 21, 2023)

  • Add typing_extensions.get_protocol_members and typing_extensions.is_protocol (backport of CPython PR #104878). Patch by Jelle Zijlstra.
  • typing_extensions now re-exports all names in the standard library's typing module, except the deprecated ByteString. Patch by Jelle Zijlstra.
  • Due to changes in the implementation of typing_extensions.Protocol, typing.runtime_checkable can now be used on typing_extensions.Protocol (previously, users had to use typing_extensions.runtime_checkable if they were using typing_extensions.Protocol).
  • Align the implementation of TypedDict with the implementation in the standard library on Python 3.9 and higher. typing_extensions.TypedDict is now a function instead of a class. The private functions _check_fails, _dict_new, and _typeddict_new have been removed. is_typeddict now returns False when called with TypedDict itself as the argument. Patch by Jelle Zijlstra.
  • Declare support for Python 3.12. Patch by Jelle Zijlstra.
  • Fix tests on Python 3.13, which removes support for creating TypedDict classes through the keyword-argument syntax. Patch by

... (truncated)

Changelog

Sourced from typing-extensions's changelog.

Release 4.7.1 (July 2, 2023)

  • Fix support for TypedDict, NamedTuple and is_protocol on PyPy-3.7 and PyPy-3.8. Patch by Alex Waygood. Note that PyPy-3.7 and PyPy-3.8 are unsupported by the PyPy project. The next feature release of typing-extensions will drop support for PyPy-3.7 and may also drop support for PyPy-3.8.

Release 4.7.0 (June 28, 2023)

  • This is expected to be the last feature release supporting Python 3.7, which reaches its end of life on June 27, 2023. Version 4.8.0 will support only Python 3.8.0 and up.
  • Fix bug where a typing_extensions.Protocol class that had one or more non-callable members would raise TypeError when issubclass() was called against it, even if it defined a custom __subclasshook__ method. The correct behaviour -- which has now been restored -- is not to raise TypeError in these situations if a custom __subclasshook__ method is defined. Patch by Alex Waygood (backporting python/cpython#105976).

Release 4.7.0rc1 (June 21, 2023)

  • Add typing_extensions.get_protocol_members and typing_extensions.is_protocol (backport of CPython PR #104878). Patch by Jelle Zijlstra.
  • typing_extensions now re-exports all names in the standard library's typing module, except the deprecated ByteString. Patch by Jelle Zijlstra.
  • Due to changes in the implementation of typing_extensions.Protocol, typing.runtime_checkable can now be used on typing_extensions.Protocol (previously, users had to use typing_extensions.runtime_checkable if they were using typing_extensions.Protocol).
  • Align the implementation of TypedDict with the implementation in the standard library on Python 3.9 and higher. typing_extensions.TypedDict is now a function instead of a class. The private functions _check_fails, _dict_new, and _typeddict_new have been removed. is_typeddict now returns False when called with TypedDict itself as the argument. Patch by Jelle Zijlstra.
  • Declare support for Python 3.12. Patch by Jelle Zijlstra.
  • Fix tests on Python 3.13, which removes support for creating TypedDict classes through the keyword-argument syntax. Patch by Jelle Zijlstra.
  • Fix a regression introduced in v4.6.3 that meant that issubclass(object, typing_extensions.Protocol) would erroneously raise TypeError. Patch by Alex Waygood (backporting the CPython PR python/cpython#105239).
  • Allow Protocol classes to inherit from typing_extensions.Buffer or collections.abc.Buffer. Patch by Alex Waygood (backporting python/cpython#104827, by Jelle Zijlstra).
  • Allow classes to inherit from both typing.Protocol and typing_extensions.Protocol

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typing-extensions&package-manager=pip&previous-version=4.6.3&new-version=4.7.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.in | 2 +- requirements.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.in b/requirements.in index 37e15028bedd..6701a1ef1b77 100644 --- a/requirements.in +++ b/requirements.in @@ -4,7 +4,7 @@ # 2) pip-compile --generate-hashes requirements.in # Unittest test adapter -typing-extensions==4.6.3 +typing-extensions==4.7.1 # Fallback env creator for debian microvenv diff --git a/requirements.txt b/requirements.txt index 2436fe08810f..2747490fdba8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,9 +20,9 @@ tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f # via -r requirements.in -typing-extensions==4.6.3 \ - --hash=sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26 \ - --hash=sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5 +typing-extensions==4.7.1 \ + --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ + --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 # via -r requirements.in zipp==3.15.0 \ --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ From b1c3837e11d62a1593ee8eba15d89fe60d6398f0 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 5 Jul 2023 13:32:11 -0700 Subject: [PATCH 0065/1136] Correct PATH env var name for non-Windows when applying env var collection for microvenv (#21564) Closes https://github.com/microsoft/vscode-python/issues/21422 This is likely the cause for: ``` ## Extension: ms-python.python - `PATH=/path/to/my/python/bin:undefined` ``` --- .../interpreter/activation/terminalEnvVarCollectionService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index dbf77e72379a..85b393425836 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -32,6 +32,7 @@ import { IInterpreterService } from '../contracts'; import { defaultShells } from './service'; import { IEnvironmentActivationService } from './types'; import { EnvironmentType } from '../../pythonEnvironments/info'; +import { getSearchPathEnvVarNames } from '../../common/utils/exec'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService { @@ -172,9 +173,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ const activatePath = path.join(path.dirname(interpreter.path), 'activate'); if (!(await pathExists(activatePath))) { const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder); + const pathVarName = getSearchPathEnvVarNames()[0]; envVarCollection.replace( 'PATH', - `${path.dirname(interpreter.path)}${path.delimiter}${process.env.Path}`, + `${path.dirname(interpreter.path)}${path.delimiter}${process.env[pathVarName]}`, { applyAtShellIntegration: true }, ); return; From 27ad70ab5361142348e9d448987e2c0e3e504111 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 5 Jul 2023 14:08:08 -0700 Subject: [PATCH 0066/1136] Disable creating gh tags for pre-releases (#21563) --- build/azure-pipeline.pre-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index f2e4c2461a6d..7e8c106c2aec 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -28,6 +28,7 @@ extends: template: azure-pipelines/extension/pre-release.yml@templates parameters: publishExtension: ${{ parameters.publishExtension }} + ghCreateTag: false l10nSourcePaths: ./src/client buildSteps: - task: NodeTool@0 From cfee3ddcdbce4d7ce05db554ddad03f2090df625 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 6 Jul 2023 13:36:48 -0700 Subject: [PATCH 0067/1136] Remove needs labels (#21572) closes https://github.com/microsoft/vscode-python/issues/21366 --- .github/workflows/remove-needs-labels.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/remove-needs-labels.yml diff --git a/.github/workflows/remove-needs-labels.yml b/.github/workflows/remove-needs-labels.yml new file mode 100644 index 000000000000..fea7eafd2152 --- /dev/null +++ b/.github/workflows/remove-needs-labels.yml @@ -0,0 +1,22 @@ +name: 'Remove Needs Label' +on: + issues: + types: [closed] + +jobs: + classify: + name: 'Remove needs labels on issue closing' + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v3 + with: + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions + + - name: Install Actions + run: npm install --production --prefix ./actions + + - name: Remove Any Needs Labels + uses: ./actions/python-remove-needs-label From 202463600c712853e56bb650e99e4334d1937818 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 6 Jul 2023 15:24:30 -0700 Subject: [PATCH 0068/1136] Info needed closer (#21575) closes https://github.com/microsoft/vscode-python/issues/21574 --- .github/workflows/info-needed-closer.yml | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/info-needed-closer.yml diff --git a/.github/workflows/info-needed-closer.yml b/.github/workflows/info-needed-closer.yml new file mode 100644 index 000000000000..07104d6bc4be --- /dev/null +++ b/.github/workflows/info-needed-closer.yml @@ -0,0 +1,28 @@ +name: Info-Needed Closer +on: + schedule: + - cron: 20 12 * * * # 5:20am Redmond + repository_dispatch: + types: [trigger-needs-more-info] + workflow_dispatch: + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v2 + with: + repository: 'microsoft/vscode-github-triage-actions' + path: ./actions + ref: stable + - name: Install Actions + run: npm install --production --prefix ./actions + - name: Run info-needed Closer + uses: ./actions/needs-more-info-closer + with: + label: info-needed + closeDays: 30 + closeComment: "Because we have not heard back with the information we requested, we are closing this issue for now. If you are able to provide the info later on, then we will be happy to re-open this issue to pick up where we left off. \n\nHappy Coding!" + pingDays: 30 + pingComment: "Hey @${assignee}, this issue might need further attention.\n\n@${author}, you can help us out by closing this issue if the problem no longer exists, or adding more information." From abcb5cee20fdeac9ba1fdcdc2b6abe55a4fdfc63 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 7 Jul 2023 08:16:02 -0700 Subject: [PATCH 0069/1136] Move lock issues (#21580) move logic for lock issue workflow back to python repo to avoid extra work of checking out and cloning vscode-github-triage-actions repo. Should reduce time it takes to run github actions. --- .github/workflows/lock-issues.yml | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/.github/workflows/lock-issues.yml b/.github/workflows/lock-issues.yml index 8c828ff766cb..bcd9ea267f9f 100644 --- a/.github/workflows/lock-issues.yml +++ b/.github/workflows/lock-issues.yml @@ -15,17 +15,10 @@ jobs: lock-issues: runs-on: ubuntu-latest steps: - - name: Checkout Actions - uses: actions/checkout@v3 - with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions - - - name: Install Actions - run: npm install --production --prefix ./actions - - name: 'Lock Issues' - uses: ./actions/python-lock-issues + uses: dessant/lock-threads@v4 with: - token: ${{ github.token }} + github-token: ${{ github.token }} + issue-inactive-days: '30' + process-only: 'issues' + log-output: true From 674459018770b77824fff2038be6c40e47634f29 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 10 Jul 2023 08:19:11 -0700 Subject: [PATCH 0070/1136] Move more workflows to python (#21581) move logic for lock issue workflow back to python repo to avoid extra work of checking out and cloning vscode-github-triage-actions repo. Should reduce time it takes to run github actions. --- .../community-feedback-auto-comment.yml | 23 +++++++------ .github/workflows/pr-file-check.yml | 33 ++++++++++++++----- .github/workflows/pr-labels.yml | 16 +++------ .github/workflows/remove-needs-labels.yml | 18 ++++------ 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/.github/workflows/community-feedback-auto-comment.yml b/.github/workflows/community-feedback-auto-comment.yml index 57bbd97bf430..1bb8ca9b10da 100644 --- a/.github/workflows/community-feedback-auto-comment.yml +++ b/.github/workflows/community-feedback-auto-comment.yml @@ -8,22 +8,21 @@ jobs: add-comment: if: github.event.label.name == 'needs community feedback' runs-on: ubuntu-latest - permissions: issues: write - steps: - - name: Checkout Actions - uses: actions/checkout@v3 + - name: Check For Existing Comment + uses: peter-evans/find-comment@v2 + id: finder with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions - - - name: Install Actions - run: npm install --production --prefix ./actions + issue-number: ${{ github.event.issue.number }} + comment-author: 'github-actions[bot]' + body-includes: 'Thanks for the feature request! We are going to give the community' - - name: Add Community Feedback Comment if applicable - uses: ./actions/python-community-feedback-auto-comment + - name: Add Community Feedback Comment + if: steps.finder.outputs.comment-id == '' + uses: peter-evans/create-or-update-comment@v3 with: issue-number: ${{ github.event.issue.number }} + body: | + Thanks for the feature request! We are going to give the community 60 days from when this issue was created to provide 7 👍 upvotes on the opening comment to gauge general interest in this idea. If there's enough upvotes then we will consider this feature request in our future planning. If there's unfortunately not enough upvotes then we will close this issue. diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index 258e07daace7..c0bf09f2cd24 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -1,4 +1,5 @@ name: PR files + on: pull_request: types: @@ -15,15 +16,29 @@ jobs: name: 'Check for changed files' runs-on: ubuntu-latest steps: - - name: Checkout Actions - uses: actions/checkout@v3 + - name: 'package-lock.json matches package.json' + uses: brettcannon/check-for-changed-files@v1.1.0 with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions + prereq-pattern: 'package.json' + file-pattern: 'package-lock.json' + skip-label: 'skip package*.json' + failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - - name: Install Actions - run: npm install --production --prefix ./actions + - name: 'package.json matches package-lock.json' + uses: brettcannon/check-for-changed-files@v1.1.0 + with: + prereq-pattern: 'package-lock.json' + file-pattern: 'package.json' + skip-label: 'skip package*.json' + failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - - name: Check for changed files - uses: ./actions/python-pr-file-check + - name: 'Tests' + uses: brettcannon/check-for-changed-files@v1.1.0 + with: + prereq-pattern: src/**/*.ts + file-pattern: | + src/**/*.test.ts + src/**/*.testvirtualenvs.ts + .github/test_plan.md + skip-label: 'skip tests' + failure-message: 'TypeScript code was edited without also editing a ${file-pattern} file; see the Testing page in our wiki on testing guidelines (the ${skip-label} label can be used to pass this check)' diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 7ddb781e2a85..e953f62d2011 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -13,15 +13,9 @@ jobs: name: 'Classify PR' runs-on: ubuntu-latest steps: - - name: Checkout Actions - uses: actions/checkout@v3 + - name: 'PR impact specified' + uses: mheap/github-action-required-labels@v4 with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions - - - name: Install Actions - run: npm install --production --prefix ./actions - - - name: Classify PR - uses: ./actions/python-pr-labels + mode: exactly + count: 1 + labels: 'bug, debt, feature-request, no-changelog' diff --git a/.github/workflows/remove-needs-labels.yml b/.github/workflows/remove-needs-labels.yml index fea7eafd2152..3d218e297a11 100644 --- a/.github/workflows/remove-needs-labels.yml +++ b/.github/workflows/remove-needs-labels.yml @@ -8,15 +8,11 @@ jobs: name: 'Remove needs labels on issue closing' runs-on: ubuntu-latest steps: - - name: Checkout Actions - uses: actions/checkout@v3 + - name: 'Removes needs labels on issue close' + uses: actions-ecosystem/action-remove-labels@v1 with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions - - - name: Install Actions - run: npm install --production --prefix ./actions - - - name: Remove Any Needs Labels - uses: ./actions/python-remove-needs-label + labels: | + needs PR + needs spike + needs community feedback + needs proposal From 523b1f69ebb3bd2a897b8aa12436ced8e9cd107c Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 10 Jul 2023 08:25:34 -0700 Subject: [PATCH 0071/1136] Update list of repo labels (#21597) For https://github.com/microsoft/vscode-python/issues/20697 --- .github/workflows/issue-labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index 98ac4eaca81d..ec2c5eb002fd 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -6,7 +6,7 @@ on: env: # To update the list of labels, see `getLabels.js`. - REPO_LABELS: '["area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' + REPO_LABELS: '["area-api","area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd"]' permissions: From 1308730eea2485ded7e8492ff97bc6a217d8776b Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Mon, 10 Jul 2023 09:40:43 -0700 Subject: [PATCH 0072/1136] Add pythonwelcome2 walkthrough to activation reason (#21588) pythonWelcomeWithDS no longer exists, but we're currently rolling out a second version of the walkthrough so it'd be nice to get the Python extension to auto activate like it does with pythonWelcome so we can compare apples to apples --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 715179858e24..dfd6a640b3c4 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "onDebugDynamicConfigurations:python", "onDebugResolve:python", "onWalkthrough:pythonWelcome", - "onWalkthrough:pythonWelcomeWithDS", + "onWalkthrough:pythonWelcome2", "onWalkthrough:pythonDataScienceWelcome", "workspaceContains:mspythonconfig.json", "workspaceContains:pyproject.toml", From f77a0117435504a2db7b76512572da74c4b71941 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 10 Jul 2023 15:34:17 -0700 Subject: [PATCH 0073/1136] fix duplicate class names processed as single node in build test tree pytest (#21601) fixes https://github.com/microsoft/vscode-python/issues/21578 --- .../.data/unittest_folder/test_add.py | 8 ++++ .../.data/unittest_folder/test_subtract.py | 8 ++++ .../expected_discovery_test_output.py | 47 ++++++++++++++++++- pythonFiles/vscode_pytest/__init__.py | 4 +- 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py index a96c7f2fa392..e9bdda0ad2ad 100644 --- a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py +++ b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_add.py @@ -19,3 +19,11 @@ def test_add_positive_numbers(self): # test_marker--test_add_positive_numbers def test_add_negative_numbers(self): # test_marker--test_add_negative_numbers result = add(-2, -3) self.assertEqual(result, -5) + + +class TestDuplicateFunction(unittest.TestCase): + # This test's id is unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_a. It has the same class name as + # another test, but it's in a different file, so it should not be confused. + # This test passes. + def test_dup_a(self): # test_marker--test_dup_a + self.assertEqual(1, 1) diff --git a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py index 087e5140def4..634a6d81f9eb 100644 --- a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py +++ b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py @@ -24,3 +24,11 @@ def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbe result = subtract(-2, -3) # This is intentional to test assertion failures self.assertEqual(result, 100000) + + +class TestDuplicateFunction(unittest.TestCase): + # This test's id is unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s. It has the same class name as + # another test, but it's in a different file, so it should not be confused. + # This test passes. + def test_dup_s(self): # test_marker--test_dup_s + self.assertEqual(1, 1) diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 0f733c94d141..b05ed5e9f00c 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -106,10 +106,14 @@ # │ └── TestAddFunction # │ ├── test_add_negative_numbers # │ └── test_add_positive_numbers +# │ └── TestDuplicateFunction +# │ └── test_dup_a # └── test_subtract.py # └── TestSubtractFunction # ├── test_subtract_negative_numbers # └── test_subtract_positive_numbers +# │ └── TestDuplicateFunction +# │ └── test_dup_s unittest_folder_path = os.fspath(TEST_DATA_PATH / "unittest_folder") test_add_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_add.py") test_subtract_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_subtract.py") @@ -159,7 +163,26 @@ }, ], "id_": "unittest_folder/test_add.py::TestAddFunction", - } + }, + { + "name": "TestDuplicateFunction", + "path": test_add_path, + "type_": "class", + "children": [ + { + "name": "test_dup_a", + "path": test_add_path, + "lineno": find_test_line_number( + "test_dup_a", + test_add_path, + ), + "type_": "test", + "id_": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + "runID": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + }, + ], + "id_": "unittest_folder/test_add.py::TestDuplicateFunction", + }, ], }, { @@ -197,7 +220,26 @@ }, ], "id_": "unittest_folder/test_subtract.py::TestSubtractFunction", - } + }, + { + "name": "TestDuplicateFunction", + "path": test_subtract_path, + "type_": "class", + "children": [ + { + "name": "test_dup_s", + "path": test_subtract_path, + "lineno": find_test_line_number( + "test_dup_s", + test_subtract_path, + ), + "type_": "test", + "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + "runID": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + }, + ], + "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction", + }, ], }, ], @@ -206,6 +248,7 @@ "id_": TEST_DATA_PATH_STR, } + # This is the expected output for the dual_level_nested_folder tests # └── dual_level_nested_folder # └── test_top_folder.py diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index b880b26fd5b8..d5d7d0e6a9f2 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -266,10 +266,10 @@ def build_test_tree(session: pytest.Session) -> TestNode: test_node = create_test_node(test_case) if isinstance(test_case.parent, pytest.Class): try: - test_class_node = class_nodes_dict[test_case.parent.name] + test_class_node = class_nodes_dict[test_case.parent.nodeid] except KeyError: test_class_node = create_class_node(test_case.parent) - class_nodes_dict[test_case.parent.name] = test_class_node + class_nodes_dict[test_case.parent.nodeid] = test_class_node test_class_node["children"].append(test_node) if test_case.parent.parent: parent_module = test_case.parent.parent From 049ca8b50960473e339ec8b14955ebe340eafc6f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 10 Jul 2023 15:34:33 -0700 Subject: [PATCH 0074/1136] Return exceptions & skips correctly pytest (#21603) fixes https://github.com/microsoft/vscode-python/issues/21579 --- .../.data/error_raise_exception.py | 14 ++++ .../pytestadapter/.data/parametrize_tests.py | 3 + .../tests/pytestadapter/.data/skip_tests.py | 30 +++++++++ .../expected_discovery_test_output.py | 10 ++- .../expected_execution_test_output.py | 51 ++++++++++++++ .../tests/pytestadapter/test_execution.py | 15 +++++ pythonFiles/vscode_pytest/__init__.py | 66 +++++++++++++++++-- 7 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 pythonFiles/tests/pytestadapter/.data/error_raise_exception.py create mode 100644 pythonFiles/tests/pytestadapter/.data/skip_tests.py diff --git a/pythonFiles/tests/pytestadapter/.data/error_raise_exception.py b/pythonFiles/tests/pytestadapter/.data/error_raise_exception.py new file mode 100644 index 000000000000..2506089abe07 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/error_raise_exception.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + + +@pytest.fixture +def raise_fixture(): + raise Exception("Dummy exception") + + +class TestSomething: + def test_a(self, raise_fixture): + assert True diff --git a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py index 6911c9aec7f0..a39b7c26de9f 100644 --- a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py +++ b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + import pytest diff --git a/pythonFiles/tests/pytestadapter/.data/skip_tests.py b/pythonFiles/tests/pytestadapter/.data/skip_tests.py new file mode 100644 index 000000000000..113e3506932a --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/skip_tests.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + +# Testing pytest with skipped tests. The first passes, the second three are skipped. + + +def test_something(): # test_marker--test_something + # This tests passes successfully. + assert 1 + 1 == 2 + + +def test_another_thing(): # test_marker--test_another_thing + # Skip this test with a reason. + pytest.skip("Skipping this test for now") + + +@pytest.mark.skip( + reason="Skipping this test as it requires additional setup" # test_marker--test_complex_thing +) +def test_decorator_thing(): + # Skip this test as well, with a reason. This one uses a decorator. + assert True + + +@pytest.mark.skipif(1 < 5, reason="is always true") # test_marker--test_complex_thing_2 +def test_decorator_thing_2(): + # Skip this test as well, with a reason. This one uses a decorator with a condition. + assert True diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index b05ed5e9f00c..fb8234350fb4 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -489,7 +489,10 @@ { "name": "[1]", "path": parameterize_tests_path, - "lineno": "15", + "lineno": find_test_line_number( + "test_under_ten[1]", + parameterize_tests_path, + ), "type_": "test", "id_": "parametrize_tests.py::test_under_ten[1]", "runID": "parametrize_tests.py::test_under_ten[1]", @@ -497,7 +500,10 @@ { "name": "[2]", "path": parameterize_tests_path, - "lineno": "15", + "lineno": find_test_line_number( + "test_under_ten[2]", + parameterize_tests_path, + ), "type_": "test", "id_": "parametrize_tests.py::test_under_ten[2]", "runID": "parametrize_tests.py::test_under_ten[2]", diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index abe27ffc79ce..3d24f036fe2a 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -150,6 +150,57 @@ }, } +# This is the expected output for the error_raised_exception.py file. +# └── error_raise_exception.py +# ├── TestSomething +# │ └── test_a: failure +error_raised_exception_execution_expected_output = { + "error_raise_exception.py::TestSomething::test_a": { + "test": "error_raise_exception.py::TestSomething::test_a", + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": "TRACEBACK", + "subtest": None, + } +} + +# This is the expected output for the skip_tests.py file. +# └── test_something: success +# └── test_another_thing: skipped +# └── test_decorator_thing: skipped +# └── test_decorator_thing_2: skipped +skip_tests_execution_expected_output = { + "skip_tests.py::test_something": { + "test": "skip_tests.py::test_something", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "skip_tests.py::test_another_thing": { + "test": "skip_tests.py::test_another_thing", + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, + "skip_tests.py::test_decorator_thing": { + "test": "skip_tests.py::test_decorator_thing", + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, + "skip_tests.py::test_decorator_thing_2": { + "test": "skip_tests.py::test_decorator_thing_2", + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, +} + + # This is the expected output for the dual_level_nested_folder.py tests # └── dual_level_nested_folder # └── test_top_folder.py diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index 400ef9f883bc..f147a0462f38 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -56,6 +56,19 @@ def test_bad_id_error_execution(): @pytest.mark.parametrize( "test_ids, expected_const", [ + ( + [ + "skip_tests.py::test_something", + "skip_tests.py::test_another_thing", + "skip_tests.py::test_decorator_thing", + "skip_tests.py::test_decorator_thing_2", + ], + expected_execution_test_output.skip_tests_execution_expected_output, + ), + ( + ["error_raise_exception.py::TestSomething::test_a"], + expected_execution_test_output.error_raised_exception_execution_expected_output, + ), ( [ "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", @@ -161,4 +174,6 @@ def test_pytest_execution(test_ids, expected_const): for key in actual_result_dict: if actual_result_dict[key]["outcome"] == "failure": actual_result_dict[key]["message"] = "ERROR MESSAGE" + if actual_result_dict[key]["traceback"] != None: + actual_result_dict[key]["traceback"] = "TRACEBACK" assert actual_result_dict == expected_const diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index d5d7d0e6a9f2..b14a79aef7fd 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -69,14 +69,37 @@ def pytest_exception_interact(node, call, report): """ # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. # call.excinfo.exconly() returns the exception as a string. - if call.excinfo and call.excinfo.typename != "AssertionError": - ERRORS.append( - call.excinfo.exconly() + "\n Check Python Test Logs for more details." - ) + # See if it is during discovery or execution. + # if discovery, then add the error to error logs. + if type(report) == pytest.CollectReport: + if call.excinfo and call.excinfo.typename != "AssertionError": + ERRORS.append( + call.excinfo.exconly() + "\n Check Python Test Logs for more details." + ) + else: + ERRORS.append( + report.longreprtext + "\n Check Python Test Logs for more details." + ) else: - ERRORS.append( - report.longreprtext + "\n Check Python Test Logs for more details." - ) + # if execution, send this data that the given node failed. + report_value = "failure" + node_id = str(node.nodeid) + if node_id not in collected_tests_so_far: + collected_tests_so_far.append(node_id) + item_result = create_test_outcome( + node_id, + report_value, + "Test failed with exception", + report.longreprtext, + ) + collected_test = testRunResultDict() + collected_test[node_id] = item_result + cwd = pathlib.Path.cwd() + execution_post( + os.fsdecode(cwd), + "success", + collected_test if collected_test else None, + ) def pytest_keyboard_interrupt(excinfo): @@ -183,6 +206,35 @@ def pytest_report_teststatus(report, config): } +def pytest_runtest_protocol(item, nextitem): + if item.own_markers: + for marker in item.own_markers: + # If the test is marked with skip then it will not hit the pytest_report_teststatus hook, + # therefore we need to handle it as skipped here. + skip_condition = False + if marker.name == "skipif": + skip_condition = any(marker.args) + if marker.name == "skip" or skip_condition: + node_id = str(item.nodeid) + report_value = "skipped" + cwd = pathlib.Path.cwd() + if node_id not in collected_tests_so_far: + collected_tests_so_far.append(node_id) + item_result = create_test_outcome( + node_id, + report_value, + None, + None, + ) + collected_test = testRunResultDict() + collected_test[node_id] = item_result + execution_post( + os.fsdecode(cwd), + "success", + collected_test if collected_test else None, + ) + + def pytest_sessionfinish(session, exitstatus): """A pytest hook that is called after pytest has fulled finished. From d3b89852716808d7de6e3f711bf125a221052271 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 10 Jul 2023 16:39:45 -0700 Subject: [PATCH 0075/1136] Refactor API code in a separate module (#21604) For https://github.com/microsoft/vscode-python/issues/20949 We plan to publish the "api" module as an npm package. Inspired from https://insiders.vscode.dev/github/microsoft/vscode-wasi/blob/main/wasm-wasi/src/api/main.ts#L408. --- src/client/api.ts | 8 +-- src/client/{apiTypes.ts => api/main.ts} | 65 +++++++++++++++++-- src/client/api/package-lock.json | 63 ++++++++++++++++++ src/client/api/package.json | 26 ++++++++ src/client/deprecatedProposedApiTypes.ts | 2 +- src/client/environmentApi.ts | 8 +-- src/client/extension.ts | 8 +-- .../creation/proposed.createEnvApis.ts | 2 +- src/test/api.test.ts | 4 +- src/test/common.ts | 4 +- src/test/environmentApi.unit.test.ts | 6 +- src/test/initialize.ts | 4 +- 12 files changed, 172 insertions(+), 28 deletions(-) rename src/client/{apiTypes.ts => api/main.ts} (87%) create mode 100644 src/client/api/package-lock.json create mode 100644 src/client/api/package.json diff --git a/src/client/api.ts b/src/client/api.ts index 7bc3fc81373b..7cf580d9f78f 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -10,7 +10,7 @@ import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient import { LanguageClient } from 'vscode-languageclient/node'; import { PYLANCE_NAME } from './activation/node/languageClientFactory'; import { ILanguageServerOutputChannel } from './activation/types'; -import { IExtensionApi } from './apiTypes'; +import { PythonExtension } from './api/main'; import { isTestExecution, PYTHON_LANGUAGE } from './common/constants'; import { IConfigurationService, Resource } from './common/types'; import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extension/adapter/remoteLaunchers'; @@ -29,14 +29,14 @@ export function buildApi( serviceManager: IServiceManager, serviceContainer: IServiceContainer, discoveryApi: IDiscoveryAPI, -): IExtensionApi { +): PythonExtension { const configurationService = serviceContainer.get(IConfigurationService); const interpreterService = serviceContainer.get(IInterpreterService); serviceManager.addSingleton(JupyterExtensionIntegration, JupyterExtensionIntegration); const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); const outputChannel = serviceContainer.get(ILanguageServerOutputChannel); - const api: IExtensionApi & { + const api: PythonExtension & { /** * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an * iteration or two. @@ -44,7 +44,7 @@ export function buildApi( pylance: ApiForPylance; } & { /** - * @deprecated Use IExtensionApi.environments API instead. + * @deprecated Use PythonExtension.environments API instead. * * Return internal settings within the extension which are stored in VSCode storage */ diff --git a/src/client/apiTypes.ts b/src/client/api/main.ts similarity index 87% rename from src/client/apiTypes.ts rename to src/client/api/main.ts index d30a81582a7e..b9266a732826 100644 --- a/src/client/apiTypes.ts +++ b/src/client/api/main.ts @@ -1,19 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, Event, Uri, WorkspaceFolder } from 'vscode'; -import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types'; +import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem, extensions } from 'vscode'; /* * Do not introduce any breaking changes to this API. * This is the public API for other extensions to interact with this extension. */ - -export interface IExtensionApi { +export interface PythonExtension { /** * Promise indicating whether all parts of the extension have completed loading or not. * @type {Promise} - * @memberof IExtensionApi */ ready: Promise; jupyter: { @@ -128,6 +125,47 @@ export interface IExtensionApi { }; } +interface IJupyterServerUri { + baseUrl: string; + token: string; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + authorizationHeader: any; // JSON object for authorization header. + expiration?: Date; // Date/time when header expires and should be refreshed. + displayName: string; +} + +type JupyterServerUriHandle = string; + +export interface IJupyterUriProvider { + readonly id: string; // Should be a unique string (like a guid) + getQuickPickEntryItems(): QuickPickItem[]; + handleQuickPick(item: QuickPickItem, backEnabled: boolean): Promise; + getServerUri(handle: JupyterServerUriHandle): Promise; +} + +interface IDataFrameInfo { + columns?: { key: string; type: ColumnType }[]; + indexColumn?: string; + rowCount?: number; +} + +export interface IDataViewerDataProvider { + dispose(): void; + getDataFrameInfo(): Promise; + getAllRows(): Promise; + getRows(start: number, end: number): Promise; +} + +enum ColumnType { + String = 'string', + Number = 'number', + Bool = 'bool', +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type IRowsResponse = any[]; + export type RefreshOptions = { /** * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so @@ -349,3 +387,20 @@ export type EnvironmentVariablesChangeEvent = { */ readonly env: EnvironmentVariables; }; + +export const PVSC_EXTENSION_ID = 'ms-python.python'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PythonExtension { + export async function api(): Promise { + const extension = extensions.getExtension(PVSC_EXTENSION_ID); + if (extension === undefined) { + throw new Error(`Python extension is not installed or is disabled`); + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonApi: PythonExtension = extension.exports; + return pythonApi; + } +} diff --git a/src/client/api/package-lock.json b/src/client/api/package-lock.json new file mode 100644 index 000000000000..137262c6523b --- /dev/null +++ b/src/client/api/package-lock.json @@ -0,0 +1,63 @@ +{ + "name": "@vscode/python-extension", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@vscode/python-extension", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@types/vscode": "^1.78.0" + }, + "devDependencies": { + "@types/node": "^16.11.7", + "typescript": "^4.7.2" + } + }, + "node_modules/@types/node": { + "version": "16.18.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.27.tgz", + "integrity": "sha512-GFfndd/RINWD19W+xNJ9Qh/sOZ5ieTiOSagA86ER/12i/l+MEnQxsbldGRF23azWjRfe7zUlAldyrwN84a1E5w==", + "dev": true + }, + "node_modules/@types/vscode": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.78.0.tgz", + "integrity": "sha512-LJZIJpPvKJ0HVQDqfOy6W4sNKUBBwyDu1Bs8chHBZOe9MNuKTJtidgZ2bqjhmmWpUb0TIIqv47BFUcVmAsgaVA==" + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + } + }, + "dependencies": { + "@types/node": { + "version": "16.18.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.27.tgz", + "integrity": "sha512-GFfndd/RINWD19W+xNJ9Qh/sOZ5ieTiOSagA86ER/12i/l+MEnQxsbldGRF23azWjRfe7zUlAldyrwN84a1E5w==", + "dev": true + }, + "@types/vscode": { + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.78.0.tgz", + "integrity": "sha512-LJZIJpPvKJ0HVQDqfOy6W4sNKUBBwyDu1Bs8chHBZOe9MNuKTJtidgZ2bqjhmmWpUb0TIIqv47BFUcVmAsgaVA==" + }, + "typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true + } + } +} diff --git a/src/client/api/package.json b/src/client/api/package.json new file mode 100644 index 000000000000..901ab3f13cfd --- /dev/null +++ b/src/client/api/package.json @@ -0,0 +1,26 @@ +{ + "name": "@vscode/python-extension", + "description": "VSCode Python extension's public API", + "version": "1.0.0", + "publisher": "ms-python", + "author": { + "name": "Microsoft Corporation" + }, + "types": "./index.d.ts", + "license": "MIT", + "homepage": "https://github.com/Microsoft/vscode-python", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/vscode-python" + }, + "bugs": { + "url": "https://github.com/Microsoft/vscode-python/issues" + }, + "dependencies": { + "@types/vscode": "^1.78.0" + }, + "devDependencies": { + "@types/node": "^16.11.7", + "typescript": "^4.7.2" + } +} diff --git a/src/client/deprecatedProposedApiTypes.ts b/src/client/deprecatedProposedApiTypes.ts index 14cabe1d09ae..407cb1dab394 100644 --- a/src/client/deprecatedProposedApiTypes.ts +++ b/src/client/deprecatedProposedApiTypes.ts @@ -4,7 +4,7 @@ import { Uri, Event } from 'vscode'; import { PythonEnvKind, EnvPathType } from './pythonEnvironments/base/info'; import { ProgressNotificationEvent, GetRefreshEnvironmentsOptions } from './pythonEnvironments/base/locator'; -import { Resource } from './apiTypes'; +import { Resource } from './api/main'; export interface EnvironmentDetailsOptions { useCache: boolean; diff --git a/src/client/environmentApi.ts b/src/client/environmentApi.ts index 3e846eb2772f..ee0b28ae2ff4 100644 --- a/src/client/environmentApi.ts +++ b/src/client/environmentApi.ts @@ -26,11 +26,11 @@ import { EnvironmentTools, EnvironmentType, EnvironmentVariablesChangeEvent, - IExtensionApi, + PythonExtension, RefreshOptions, ResolvedEnvironment, Resource, -} from './apiTypes'; +} from './api/main'; import { buildEnvironmentCreationApi } from './pythonEnvironments/creation/createEnvApi'; type ActiveEnvironmentChangeEvent = { @@ -114,7 +114,7 @@ function filterUsingVSCodeContext(e: PythonEnvInfo) { export function buildEnvironmentApi( discoveryApi: IDiscoveryAPI, serviceContainer: IServiceContainer, -): IExtensionApi['environments'] { +): PythonExtension['environments'] { const interpreterPathService = serviceContainer.get(IInterpreterPathService); const configService = serviceContainer.get(IConfigurationService); const disposables = serviceContainer.get(IDisposableRegistry); @@ -180,7 +180,7 @@ export function buildEnvironmentApi( onEnvironmentVariablesChanged, ); - const environmentApi: IExtensionApi['environments'] = { + const environmentApi: PythonExtension['environments'] = { getEnvironmentVariables: (resource?: Resource) => { sendApiTelemetry('getEnvironmentVariables'); resource = resource && 'uri' in resource ? resource.uri : resource; diff --git a/src/client/extension.ts b/src/client/extension.ts index 5fcb63e2d322..89649d377c74 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -41,7 +41,7 @@ import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry'; import { IStartupDurations } from './types'; import { runAfterActivation } from './common/utils/runAfterActivation'; import { IInterpreterService } from './interpreter/contracts'; -import { IExtensionApi } from './apiTypes'; +import { PythonExtension } from './api/main'; import { WorkspaceService } from './common/application/workspace'; import { disposeAll } from './common/utils/resourceLifecycle'; import { ProposedExtensionAPI } from './proposedApiTypes'; @@ -58,8 +58,8 @@ let activatedServiceContainer: IServiceContainer | undefined; ///////////////////////////// // public functions -export async function activate(context: IExtensionContext): Promise { - let api: IExtensionApi; +export async function activate(context: IExtensionContext): Promise { + let api: PythonExtension; let ready: Promise; let serviceContainer: IServiceContainer; try { @@ -103,7 +103,7 @@ async function activateUnsafe( context: IExtensionContext, startupStopWatch: StopWatch, startupDurations: IStartupDurations, -): Promise<[IExtensionApi & ProposedExtensionAPI, Promise, IServiceContainer]> { +): Promise<[PythonExtension & ProposedExtensionAPI, Promise, IServiceContainer]> { // Add anything that we got from initializing logs to dispose. context.subscriptions.push(...logDispose); diff --git a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts index 52209a5a31d0..e0e79134fc56 100644 --- a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts +++ b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License import { Event, Disposable, WorkspaceFolder } from 'vscode'; -import { EnvironmentTools } from '../../apiTypes'; +import { EnvironmentTools } from '../../api/main'; export type CreateEnvironmentUserActions = 'Back' | 'Cancel'; export type EnvironmentProviderId = string; diff --git a/src/test/api.test.ts b/src/test/api.test.ts index 24eb78c11bf0..488ce79073c2 100644 --- a/src/test/api.test.ts +++ b/src/test/api.test.ts @@ -2,12 +2,12 @@ // Licensed under the MIT License. import { expect } from 'chai'; -import { IExtensionApi } from '../client/apiTypes'; +import { PythonExtension } from '../client/api/main'; import { ProposedExtensionAPI } from '../client/proposedApiTypes'; import { initialize } from './initialize'; suite('Python API tests', () => { - let api: IExtensionApi & ProposedExtensionAPI; + let api: PythonExtension & ProposedExtensionAPI; suiteSetup(async () => { api = await initialize(); }); diff --git a/src/test/common.ts b/src/test/common.ts index 0a76c495830a..0168ee47fc37 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -10,7 +10,7 @@ import * as glob from 'glob'; import * as path from 'path'; import { coerce, SemVer } from 'semver'; import { ConfigurationTarget, Event, TextDocument, Uri } from 'vscode'; -import type { IExtensionApi } from '../client/apiTypes'; +import type { PythonExtension } from '../client/api/main'; import { IProcessService } from '../client/common/process/types'; import { IDisposable } from '../client/common/types'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; @@ -438,7 +438,7 @@ export async function isPythonVersion(...versions: string[]): Promise { } } -export interface IExtensionTestApi extends IExtensionApi, ProposedExtensionAPI { +export interface IExtensionTestApi extends PythonExtension, ProposedExtensionAPI { serviceContainer: IServiceContainer; serviceManager: IServiceManager; } diff --git a/src/test/environmentApi.unit.test.ts b/src/test/environmentApi.unit.test.ts index a4ea73fb6c92..76441247db49 100644 --- a/src/test/environmentApi.unit.test.ts +++ b/src/test/environmentApi.unit.test.ts @@ -36,8 +36,8 @@ import { ActiveEnvironmentPathChangeEvent, EnvironmentVariablesChangeEvent, EnvironmentsChangeEvent, - IExtensionApi, -} from '../client/apiTypes'; + PythonExtension, +} from '../client/api/main'; suite('Python Environment API', () => { const workspacePath = 'path/to/workspace'; @@ -57,7 +57,7 @@ suite('Python Environment API', () => { let onDidChangeEnvironments: EventEmitter; let onDidChangeEnvironmentVariables: EventEmitter; - let environmentApi: IExtensionApi['environments']; + let environmentApi: PythonExtension['environments']; setup(() => { serviceContainer = typemoq.Mock.ofType(); diff --git a/src/test/initialize.ts b/src/test/initialize.ts index f4f37204da85..8a7abca0d91c 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import type { IExtensionApi } from '../client/apiTypes'; +import type { PythonExtension } from '../client/api/main'; import { clearPythonPathInWorkspaceFolder, IExtensionTestApi, @@ -42,7 +42,7 @@ export async function initialize(): Promise { return (api as any) as IExtensionTestApi; } export async function activateExtension() { - const extension = vscode.extensions.getExtension(PVSC_EXTENSION_ID_FOR_TESTS)!; + const extension = vscode.extensions.getExtension(PVSC_EXTENSION_ID_FOR_TESTS)!; const api = await extension.activate(); // Wait until its ready to use. await api.ready; From cd10e3898e591cfafc76f2ccc08935c3e3f5b265 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 10 Jul 2023 17:02:23 -0700 Subject: [PATCH 0076/1136] Download `get-pip.py` during build (#21589) --- .github/actions/build-vsix/action.yml | 7 +- .github/workflows/build.yml | 9 +- .github/workflows/pr-check.yml | 9 +- build/azure-pipeline.pre-release.yml | 5 +- build/azure-pipeline.stable.yml | 5 +- build/build-install-requirements.txt | 2 + build/debugger-install-requirements.txt | 2 - gulpfile.js | 14 +- pythonFiles/download_get_pip.py | 46 + pythonFiles/get-pip.py | 27086 ---------------- .../common/process/internal/scripts/index.ts | 3 - 11 files changed, 86 insertions(+), 27102 deletions(-) create mode 100644 build/build-install-requirements.txt delete mode 100644 build/debugger-install-requirements.txt create mode 100644 pythonFiles/download_get_pip.py delete mode 100644 pythonFiles/get-pip.py diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index b84d3e0871f0..6c4621c7eb9b 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -29,7 +29,7 @@ runs: cache: 'pip' cache-dependency-path: | requirements.txt - build/debugger-install-requirements.txt + build/build-install-requirements.txt pythonFiles/jedilsp_requirements/requirements.txt - name: Upgrade Pip @@ -46,10 +46,11 @@ runs: with: options: '-t ./pythonFiles/lib/python --implementation py' - - name: Install debugpy + - name: Install debugpy and get-pip run: | - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python -m pip --disable-pip-version-check install packaging python ./pythonFiles/install_debugpy.py + python ./pythonFiles/download_get_pip.py shell: bash - name: Install Jedi LSP diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f418a802ff8e..24d91b94da10 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -147,6 +147,13 @@ jobs: with: python-version: ${{ matrix.python }} + - name: Download get-pip.py + run: | + python -m pip install wheel + python -m pip install -r build/build-install-requirements.txt + python ./pythonFiles/download_get_pip.py + shell: bash + - name: Install debugpy run: | # We need to have debugpy so that tests relying on it keep passing, but we don't need install_debugpy's logic in the test phase. @@ -172,7 +179,7 @@ jobs: - name: Install debugpy wheels (Python ${{ matrix.python }}) run: | python -m pip install wheel - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python -m pip install -r build/build-install-requirements.txt python ./pythonFiles/install_debugpy.py shell: bash if: matrix.test-suite == 'debugger' diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 84903463e204..aa9cae2aa474 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -127,6 +127,13 @@ jobs: # We need to have debugpy so that tests relying on it keep passing, but we don't need install_debugpy's logic in the test phase. python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + - name: Download get-pip.py + run: | + python -m pip install wheel + python -m pip install -r build/build-install-requirements.txt + python ./pythonFiles/download_get_pip.py + shell: bash + - name: Install base Python requirements uses: brettcannon/pip-secure-install@v1 with: @@ -147,7 +154,7 @@ jobs: - name: Install debugpy wheels (Python ${{ matrix.python }}) run: | python -m pip install wheel - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python -m pip --disable-pip-version-check install -r build/build-install-requirements.txt python ./pythonFiles/install_debugpy.py shell: bash if: matrix.test-suite == 'debugger' diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index 7e8c106c2aec..d4fe0ac376fb 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -53,9 +53,10 @@ extends: displayName: Install wheel - script: | - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python -m pip --disable-pip-version-check install -r build/build-install-requirements.txt python ./pythonFiles/install_debugpy.py - displayName: Install debugpy + python ./pythonFiles/download_get_pip.py + displayName: Install debugpy and get-pip.py - script: | python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/python --implementation py -r ./requirements.txt diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 5c8444de9b2e..05f83aa81824 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -48,9 +48,10 @@ extends: displayName: Install wheel - script: | - python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python -m pip --disable-pip-version-check install -r build/build-install-requirements.txt python ./pythonFiles/install_debugpy.py - displayName: Install debugpy + python ./pythonFiles/download_get_pip.py + displayName: Install debugpy and get-pip.py - script: | python -m pip install --no-deps --require-hashes --only-binary :all: -t ./pythonFiles/lib/python --implementation py -r ./requirements.txt diff --git a/build/build-install-requirements.txt b/build/build-install-requirements.txt new file mode 100644 index 000000000000..8baaa59ded67 --- /dev/null +++ b/build/build-install-requirements.txt @@ -0,0 +1,2 @@ +# Requirements needed to run install_debugpy.py and download_get_pip.py +packaging diff --git a/build/debugger-install-requirements.txt b/build/debugger-install-requirements.txt deleted file mode 100644 index 6ee0765db4b3..000000000000 --- a/build/debugger-install-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Requirements needed to run install_debugpy.py -packaging diff --git a/gulpfile.js b/gulpfile.js index a344b165a6cc..0d47127f187e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -286,7 +286,7 @@ gulp.task('installDebugpy', async () => { '-t', './pythonFiles/lib/temp', '-r', - './build/debugger-install-requirements.txt', + './build/build-install-requirements.txt', ]; await spawnAsync(process.env.CI_PYTHON_PATH || 'python', depsArgs, undefined, true) .then(() => true) @@ -295,7 +295,7 @@ gulp.task('installDebugpy', async () => { return false; }); - // Install new DEBUGPY with wheels for python 3.7 + // Install new DEBUGPY with wheels for python const wheelsArgs = ['./pythonFiles/install_debugpy.py']; const wheelsEnv = { PYTHONPATH: './pythonFiles/lib/temp' }; await spawnAsync(process.env.CI_PYTHON_PATH || 'python', wheelsArgs, wheelsEnv, true) @@ -305,6 +305,16 @@ gulp.task('installDebugpy', async () => { return false; }); + // Download get-pip.py + const getPipArgs = ['./pythonFiles/download_get_pip.py']; + const getPipEnv = { PYTHONPATH: './pythonFiles/lib/temp' }; + await spawnAsync(process.env.CI_PYTHON_PATH || 'python', getPipArgs, getPipEnv, true) + .then(() => true) + .catch((ex) => { + console.error("Failed to download get-pip wheels using 'python'", ex); + return false; + }); + rmrf.sync('./pythonFiles/lib/temp'); }); diff --git a/pythonFiles/download_get_pip.py b/pythonFiles/download_get_pip.py new file mode 100644 index 000000000000..b8238d60f261 --- /dev/null +++ b/pythonFiles/download_get_pip.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import os +import pathlib +import urllib.request as url_lib +from packaging.version import parse as version_parser + +EXTENSION_ROOT = pathlib.Path(__file__).parent.parent +GET_PIP_DEST = EXTENSION_ROOT / "pythonFiles" +PIP_PACKAGE = "pip" +PIP_VERSION = "latest" # Can be "latest", or specific version "23.1.2" + + +def _get_package_data(): + json_uri = "https://pypi.org/pypi/{0}/json".format(PIP_PACKAGE) + # Response format: https://warehouse.readthedocs.io/api-reference/json/#project + # Release metadata format: https://github.com/pypa/interoperability-peps/blob/master/pep-0426-core-metadata.rst + with url_lib.urlopen(json_uri) as response: + return json.loads(response.read()) + + +def _download_and_save(root, version): + root = os.getcwd() if root is None or root == "." else root + url = f"https://raw.githubusercontent.com/pypa/get-pip/{version}/public/get-pip.py" + print(url) + with url_lib.urlopen(url) as response: + data = response.read() + get_pip_file = pathlib.Path(root) / "get-pip.py" + get_pip_file.write_bytes(data) + + +def main(root): + data = _get_package_data() + + if PIP_VERSION == "latest": + use_version = max(data["releases"].keys(), key=version_parser) + else: + use_version = PIP_VERSION + + _download_and_save(root, use_version) + + +if __name__ == "__main__": + main(GET_PIP_DEST) diff --git a/pythonFiles/get-pip.py b/pythonFiles/get-pip.py deleted file mode 100644 index 2c411ecf21e3..000000000000 --- a/pythonFiles/get-pip.py +++ /dev/null @@ -1,27086 +0,0 @@ -#!/usr/bin/env python -# -# Hi There! -# -# You may be wondering what this giant blob of binary data here is, you might -# even be worried that we're up to something nefarious (good for you for being -# paranoid!). This is a base85 encoding of a zip file, this zip file contains -# an entire copy of pip (version 21.3.1). -# -# Pip is a thing that installs packages, pip itself is a package that someone -# might want to install, especially if they're looking to run this get-pip.py -# script. Pip has a lot of code to deal with the security of installing -# packages, various edge cases on various platforms, and other such sort of -# "tribal knowledge" that has been encoded in its code base. Because of this -# we basically include an entire copy of pip inside this blob. We do this -# because the alternatives are attempt to implement a "minipip" that probably -# doesn't do things correctly and has weird edge cases, or compress pip itself -# down into a single file. -# -# If you're wondering how this is created, it is generated using -# `scripts/generate.py` in https://github.com/pypa/get-pip. - -import sys - -this_python = sys.version_info[:2] -min_version = (3, 6) -if this_python < min_version: - message_parts = [ - "This script does not work on Python {}.{}".format(*this_python), - "The minimum supported Python version is {}.{}.".format(*min_version), - "Please use https://bootstrap.pypa.io/pip/{}.{}/get-pip.py instead.".format( - *this_python - ), - ] - print("ERROR: " + " ".join(message_parts)) - sys.exit(1) - - -import os.path -import pkgutil -import shutil -import tempfile -from base64 import b85decode - - -def determine_pip_install_arguments(): - implicit_pip = True - implicit_setuptools = True - implicit_wheel = True - - # Check if the user has requested us not to install setuptools - if "--no-setuptools" in sys.argv or os.environ.get("PIP_NO_SETUPTOOLS"): - args = [x for x in sys.argv[1:] if x != "--no-setuptools"] - implicit_setuptools = False - else: - args = sys.argv[1:] - - # Check if the user has requested us not to install wheel - if "--no-wheel" in args or os.environ.get("PIP_NO_WHEEL"): - args = [x for x in args if x != "--no-wheel"] - implicit_wheel = False - - # We only want to implicitly install setuptools and wheel if they don't - # already exist on the target platform. - if implicit_setuptools: - try: - import setuptools # noqa - - implicit_setuptools = False - except ImportError: - pass - if implicit_wheel: - try: - import wheel # noqa - - implicit_wheel = False - except ImportError: - pass - - # Add any implicit installations to the end of our args - if implicit_pip: - args += ["pip"] - if implicit_setuptools: - args += ["setuptools"] - if implicit_wheel: - args += ["wheel"] - - return ["install", "--upgrade", "--force-reinstall"] + args - - -def monkeypatch_for_cert(tmpdir): - """Patches `pip install` to provide default certificate with the lowest priority. - - This ensures that the bundled certificates are used unless the user specifies a - custom cert via any of pip's option passing mechanisms (config, env-var, CLI). - - A monkeypatch is the easiest way to achieve this, without messing too much with - the rest of pip's internals. - """ - from pip._internal.commands.install import InstallCommand - - # We want to be using the internal certificates. - cert_path = os.path.join(tmpdir, "cacert.pem") - with open(cert_path, "wb") as cert: - cert.write(pkgutil.get_data("pip._vendor.certifi", "cacert.pem")) - - install_parse_args = InstallCommand.parse_args - - def cert_parse_args(self, args): - if not self.parser.get_default_values().cert: - # There are no user provided cert -- force use of bundled cert - self.parser.defaults["cert"] = cert_path # calculated above - return install_parse_args(self, args) - - InstallCommand.parse_args = cert_parse_args - - -def bootstrap(tmpdir): - monkeypatch_for_cert(tmpdir) - - # Execute the included pip and use it to install the latest pip and - # setuptools from PyPI - from pip._internal.cli.main import main as pip_entry_point - - args = determine_pip_install_arguments() - sys.exit(pip_entry_point(args)) - - -def main(): - tmpdir = None - try: - # Create a temporary working directory - tmpdir = tempfile.mkdtemp() - - # Unpack the zipfile into the temporary directory - pip_zip = os.path.join(tmpdir, "pip.zip") - with open(pip_zip, "wb") as fp: - fp.write(b85decode(DATA.replace(b"\n", b""))) - - # Add the zipfile to sys.path so that we can import it - sys.path.insert(0, pip_zip) - - # Run the bootstrap - bootstrap(tmpdir=tmpdir) - finally: - # Clean up our temporary working directory - if tmpdir: - shutil.rmtree(tmpdir, ignore_errors=True) - - -DATA = b""" -P)h>@6aWAK2mt$eR#TliG(h+O003nH000jF003}la4%n9X>MtBUtcb8c|B0UO2j}6z0X&KUUXrdvMRV -16ubz6s0VM$QfAw<4YV^ulDhQoop$MlK*;0ehKz6 -^g4|bOsV`^+*aO7_tw^Cd$4zs{Pl#j>6{|X*AaQ6!2wJ?w>%d+2&1X4Rc!^r6h-hMtH_d5{IF3D`nKTt~p1QY-O00;p4c~(;+8{@xi0ssK6 -1ONaJ0001RX>c!JUu|J&ZeL$6aCu!*!ET%|5WVviBXU@FMMw_qp;5O|rCxIBA*z%^Qy~|I#aghDZI*1 -mzHbcdCgFtbH*em&nbG}VT_EcdJ^%Uh<#$rfXmjvMazjtt+Y{4fL(0@tjn1(F!nz|6RBOjouLCQKB%tCsn -f_O;(TkT9D!5I2G1vZWcORK< -*}iONjWAr8Zm1&KuL0jC{@?djd+x5R}RGfYPBawx08>U(W?WmDk1T9S4?epCt{Z(ueTz)EC*E`5mT15 --&2~-DsS-6=uU3I|BmObEPJI*Sr)^2!Om@h-$wOJl_c@O>A_3OHg5wqIeD(E7`y@m0ou*N^~8Scf|wu -`N_HtL5`*k&gASg%W(oQp9a7<~IpnR_S}F8z9z|q{`1rb)-o!>My0eex)q(ByedFLGyO7=Ikq8}(HcH -6i;acy-%V$hD`fEosH@wgA+8z#{H{ToXOd_?&uMj~(yRVmD7BE?-`X6FU!78rkLs#HE1jqSOWnjp~Z3(}j4wN{#<0DmEaw -w2fbN$l@K=F!>KqO9KQH000080Q-4XQ_aV~HNXG>03HDV01N;C0B~t=FK~G-ba`-PWF?NVPQ@?`MfZN -i-B_Ob4-5=!Z$M%;t!XVKc9b}U{5?(?Egj!;iWEo#VY8e`cO+3psdiM#D?U$24DrcGE{QX%^A1rwho7 -bo%%^4nEOe11`ih5ds}r~C4-D(by*bnzy~VhcmspFPs+92he4iKm495?R6(6IB9*bzqWO6Z``e?dj4> -$ei>cuLo8^bh>J0qwmAsn45g@9MQ{TAMQ=}M~B1K+Woqz5;+g_LK&{q3XhT~awQHE!$j2T)4`1QY-O0 -0;p4c~(=FwQ!+P0RR9!0ssIR0001RX>c!JX>N37a&BR4FJE72ZfSI1UoLQYb&)Yo#4rqn_xuX$SgsPJ -3leY=j7%q3*bq8})@_Z_B-k#f{~ot+ASB2V>&bck^4xJALFYoL2O3Leg*}O$!hKQ7DMaVKUUslOCh)if@+itrPZeClT~ -1iR*^N=_&VilHX7ezR{Ys!P3i6v#8#CnCLX(r^h#(D9Q2`wcYz#AqB@vfzGIq$A8sk{)NWEK&TeAplO -P?6fq6Q1^6a*0l)grsP?n#H~**AHt%UnWjY1bq&q0|@WSC{?>xZeNm!(&pOOE&dqH}AXz$)6~;-HFq; -xJFdD4^T@31QY-O00;p4c~(c!JX>N37a&BR4FJg6RY-C?$ZgwtkdDU9 -obKAHPf7f4uG7lkBlGE!=dmYW`dihW;o~E`ZcCNi@JUohoY@8{Q1wh-1$NzhG@j(J4?IbgOIlV{(b{C -7?A9fc@1wrttV^vAk^$p`qy{EM#ouDPzHJmWfRJmkLP0Eh5`jUu}2}!od0gsCy2o?*rZyPR2(bSUO$% -<|5NYz|kB9(b;g#Fd#^2(tThkgbn-15A&&!1SkV-;QOc(aEUs)`n$97g+j+@~e@<#e6Bez$)8kE7$CVsa!Y&$ksdzhuK>@*WHllam(J%Bz^1 -QFuJ>TBK59wcM7qX?8>Fvf*h#xnw(L7rDKHEljCe&?`s#rJVk^W1OOEdeuJ+V^6W(P%hAYhU;hjIOt? -2vJB0fWh56koK;Ps{O-tR;9d?}OpA)80<2VnFw5Vxw9d@n9FLVJJhuS000yys;B?3CXqmx?Fhd=u2$L -Ckdn)rXm$@sB4hWuO=_IQ}D!OgUn}Uj7lOnIGY#4r=RnmQ%m5le`faf>hg931HhzU-^Y`1Ea-aQB*l>FFREh)$5jY2R>#sl -UWuDTJ2(W2$w`i9+Bh+a@^EZli~*{QY3)2@XMbNRCX=Qyv-{?{i!Xhm5ElvxeI#=`~ -D?LO(6baf!usZ{VAodthv$pdbX0 -|q%mA+?{9piAA{!!stmrt0q3V$EuC6g`A+nT|BM2+3s>qh=U=AFthLvH+k0!N|r9wJ!j!=fA$q`6UAS{-Ga -(eL*g?2>_1%t|33HNce3`{^o3NV21-EIponyvONX0_bnWq$thPGHCZ|R4{P7TcWCqn9dCn}ym+Ans=a ->N`DS#m=&*%-GN%tdJ~G8IL_C>r_Dv+ba=a! -VgIRWan$LZbsL6xMVoz~81qqTbPQPn$khB?V(*t%SlRv3Mo`_qk>@hbk}Cq^~|6y?>LfkAH@&35JAK5 -1H1mT%Gd{5bFm(8}bk^PW|M^=@6rHY?DanXt&!q{>@Tx$k!rQJLg}2s(t4ScUH=DGin8&9C__68M{q(n|SlK>S*QE*7Gx8RY1 -y_%fH?47QqMfUPB}6DyjinRLhBK%obEtt2A~Q7~W)A$hSzb)&uj}Tv)}7c^_QTFovq0k$sq=1AQ=^!BI_En|a4Ggg|)oU`+0c4al`EZ^4``D&Jo4JP3a?0bO$_CR%5e3 -;C?%8VZV>f;=0;gV_JE2j!!vqIu#XFj{2k`%(_hZtog5#Zd^}r!I6FFD4`YhLjqyVzYvPBOINd1HRAuH7*&S^3x&tM`*%12XpXMC8OX0OawPi)z@ur}DW+Ps43vA!#)8oaph8K7Qr=zY?W=&dW*ZMPZ18Dt|G;kK{ -AiVjxgnL587Vp4G7UWB8xZ(w71#7OpxuiK^#`{kx*0~-@h@ox+-R-gUCgZ+yuT3l!DoqOa7ypKZ>Yg> -z|kR2?e8i|`TDmVHU%*J>b1&?5-QBhwE>Opiex9vY;4iv*of}Mn21$91#TvwkZQj%szLUU)K5W{bCh( -2Yclp_+C7LKSr6XH=Z$l@y0|F&G?qQO;cJjb*=-vK(_jaq);Q-KvB1#&Vlm%a>)MdAlWL9EkQ4GqgQ3 -#cym3KhY)n&Bg7+YWJ#OscgtVdS0DZRpX()g-PD3 -%YgU&v7i+@)gc%pnw*b)O0bLkv_MU&s$$2iVCK{?YTI$3Ud-omnn$bD)cA_YTv0sIx$}Gdd -HE%@ukz>K$yxvWLX&7GC<|s~W~5iG3FtTNLAap0_*CC#LCUAJrYs>D8_w*_}zSS*WgOg}n3GpON#EHz -!Lt<@@G_s`eq-R!wn@Z((Y6c~b9xzD@s1MAzcYD$_XxN;c0jQirMpBeZU6GkcjOB60 -4Q`YS~WSoHZvD_R*%G~z9xkChTKxJ1E^rGw9VcCl1l(C164}0Jz$lMqMWYkNE*J~={u=?%UHGEOpX0q -^cAsRY|r%$zgMlp?`EP0w$iWxjR176^{_S`Zof(V1*vr;4qTY(QrNgTe60CC*NJ)h%*`!1Eywg(oQ}I -Pr?VR6({Xd?{0URA{RqlRR%j;=CEU}SaFrh&@c(BNS=wIUSH=(Q1dn=jzMl@*CZk0sr`CVmFM+YisCo -{Pgk555Ea`*%l%j5uPIzW86SMD~Otez{h#5(@Q2izPO;ch~?nv;iGy3%%Rt)Ri4qs&7(D(F)RuHScAK -vM`S-<-DlYcDGhPL|{Bsb303kw^4`BcY)HCSI|*hkQHnVZOsfv!E-gd539!X|u&fb&!9z5|JbT+#&cS!ES;s51 -gt5Rd=K63LopF(|!6rx;Y+xYW{ORIiT95)Y&of1ZPIJh=Szb)z;%J3Lu_uZv0WMh35$G&8jj}$R5XFj -T1gnbG*Ql2T1bk&U_VmURq)QZC5GxrMj-65NRU@P$SMpAP6EhtCjA%oeASnpPuM6+0U_`>XZ*DV;m~e -PGttebj=R^-C0J>mK5*~iYJo@Z>P6ALS_LMCiAsf$_-MMkt@tpFfx#M7mC&*5ZPP4P~m&b2jzCSr$XR -p^E&V!}?2T2$W4S>G2ZU2)Inpmx>A~WXiXY@CS5Y>w<>B@Y^zD_IeX?Tft+?=%I7ir;mAnISOy@Z-CX -52MBZ -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 -X7ZMnvG3z|!UA!`-^Wu5d2i+3vsuWNhOM$t& -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 -qRRFRsttoL!$qLRTXC^y|Cn)rYqqBhe~Gg! -|JAj|6W&(5+D&wTB-VlNwj-0!cxLyn=F@Az9ok3#@o$|<5m*L_yW*Z=p! -fR^uvdsnBPX$O7*V8o@Z;%Pa{hCRW8MbKHN?;|n8t&!POWMTno~uyF9$$>x>#3a@7?xFu($9Vs$ukTV -8h9c(8OAs-Rk&{XU(TV{!D~2M};#L$HgzOu-~muo#mC1>DDc-(mlB;T%F^VMqFviX|1Pll+HU5)}+UDWKz-0~m1VbZ5@qDU^QR;^0ds0tn&AqcDs`Xf#{AM`d -HN=+j$Z1uAt|}p3~_ScQc~T5NOfM>gAl5I(A6EFRDqYzz>~}C>rX_~4YNBsCac%VRVf&uO -Kb_fvsp5)Q4=?t?Ki~rDWY~<<~7z_XslQlx3r-nG`hDKd19MASG?|0PWX0VWHN>z=&BNvRdBzu%RM!zcHKn9T>KK~pwa{ -anP8<0>ntgtJoq(vJMw+ttHfJS&LM&O<|Q}&7^`w+fW18t -rtWWYjQGBvm#VT&nADF+S^x3{m7D`~6TT -VY?#IFmd|Rf5CL|hX3xoBGUAV{_db*_lo}>YwNYzlsP58)15F7eQp69peQ`KZw`-75K4c7*8mTG{I}| -9LN!)r4*utE+5ixFZak`O#WV>7GYKRy3AR4oTULK*7G#M23sE8 -sqJYlH;YsX*r|$jm8z<9NfJ(y8^^Jk>*YM5USQ!nmO*mFsF2cp&MKSFcXB&(2@{pEb6_>aPsXFO&QbozQ -u;Uv|h9WzJgM75br=uC!(}wN}RRI!o>)N+#0SOq@}Z5iV-&n8yWeJ?0C>u-F2UFPB*Y4?zlD275|0*5 -8Uzt{1r807$>8rX^caa=~SNQDz0DXSW$+vBBDAhE%DNS_Q@+y|~`U@ -8@5-ZkQtTP(_l_Yse;5NNW^{VsVy(n>!FNuEeq4%T_4^3bm>>a9~qsi4{^DNR5n+I$y?>7W7OzcKsD3 -pe=3WUWrmut^>>m=0G6ZwkxRaEP#7pKtXqzYeLR4T7*#pHOhv-!D1bh->a3X_#h#3+lf76 -)C%<)uAf9f2I9eIDOqTdkEHX?a&u>3k-3u?|Q18um$(R6SWf&k?wqd0p?aVgnm-;RoW?ay>qxGlsWe==QIT+& -q*l1VPlCNY!hi7?1g)Qqw&*^HrtkwrA4BD`bS|OY;5k)8Z7m)#)tX{g}>`7sF{vayYJfP8rs~y#*Rw3 -MN%}I14;4dtxX(1jXB^O&1{wd4dz!l4Z@3nqMR(Hl7j@-I<*BU!~?!&gm@l`TuYLItdezhk2e7wnNT}7C$*)3 -Cu!=Cw?FYQjPAC&sfO)rL$5;JRqCEYp4^n2g<>(fTGC`_({|fXUsL8D$SV|%ESRGSO;9#|qJFc{kZ?p -(YwD<2G+;aN#kR$rJs%|z(hBjoHK8`t_bc9&t^t+iH26?~yXOqDH|yGo-bd&W+}~sihF*D=Q0r&xbpR -*qrP4ndQwOR#+)E8sgk=U#Map`eJ^0_T -{q6HC_j&{dQ}RVfyvWD*zA{RYIp=5Zl+!zYQZ7K(xnDG!`LYm4PuDyhF_>RrnLo;o(aq8S}%S>PMc@9666OwsN*7nUgIbRDoN*B4T=fVkNw47cO27l%3r3$Dl4qZ3tM|I}*Ly6T9W5SJ__(5jbawvh*}ID`; ->}0#;p4SD%okyq_Rpn4#$ -w}V1L9VCdwM+$)t867IA?{Y&GLr9H4BpXu##6lg=?SF)nd_>vEt+O=F^xmRCX%oMcXkoYdL8UXk1o(| -l8@T<5Z#OrbhfVk{;%j%&&2jm-C4PoroiK8e`3eEEUT@gM-~t3N8RKe3SH=$FdA%63R^LHnMb`-Tq(={QHn+EJiR#frEY&o}o(aqVJ(X+(lQ8tM -f!}AfL?WVl)m7VE5(0n!5eo7T4V@mNQF@&)(E`#8q;JAlw9%^#zO@4l$dZ^u*3@+(fgMKmYY_r~r~w9 -sA!P=Y=1HjGun~^I!gY?06?A9NCzETK8ftC;I*#1HL#JJv$P_juU>qg{OV7MntL -wdsaTG*yXBfo#F@`N$g_eQYPa3oHF&1YhUcYh|q=neMuib*2~$ZOmxr1OPS8GW!mS`=$EKS-?QumUsHJHVN<(I9- -96BfB6RO^w521kd#WMAfbC4l4$$N4=-O>>i)!xFPg=iXegPV?;(97|G4|uYc&|V$WTHPg@kXg}VfG?4 -5y_uX>tEDwT#k)~H>W$c{@&ePqB7r}r=Tw7` -(E01=4Jl|H3#}1x?x)(x4WZP>QyJipm2BIY{?jPgO}F7+ZMR<0lqTS4syqM?V#|!f6QC#&)Ij#xA*eO -xVMGaa?Yp0^rioD>F&KQ+lvqFD~rM0g`Z=)y}nGcbc3rueJ6IvZjRkjn|~RROFecopMsNKs%rYP=`^a -ULoN^9F&eBB`_?nhBS#xHs?P_O;#ji+e0mJ2K1C=015ir?1QY-O00;p4c~(;=0Hdeq0000~0RR9M000 -1RX>c!JX>N37a&BR4FKuCIZZ2?nJ&?g_!!Qhn?|urA(+Zt^8Egw|$DPJ@*{zh~CQ2f3Y#}KddHcC3tq -@2^;@8JNNSVP_raS`8T*Tm$)b{YrMkUAOoa=FbIZ}RzGHQF@94?0kH8~#P4Zcdo9X!4RWosSOXqx6{B -88ePs3^bK!%zfD>Y*!HOG402h)uz!X!XeoYLpV35d;Sm%v~khP8MJf -%P)h>@6aWAK2mt$eR#Q9GlIB1O001u>000^Q003}la4%nJZggdGZeeUMaCvZYZ)#;@bS`jt)mh(<+qe -;a_g}#(54HhYNP!j&4ETyWT#7Cbw814n9~KLPmMEK9nN&$?H$t%gduN8EMEXav=*!`Z0Er~daAr93%{ -PoZb=o+l?W{5S#46pkqH9#n-aq)gwQE($a|k_R@%xP;T7+PCfBf*1t`kRxEi)cazEq0~VCxYbCnOi#ufbCrf72{SVo7PL~DQ5O8=0+reU<)XVYjF@*n;?o -s3+tBt1b(ArPr{F3WU>Lh%pP^$))+L}wG{_m4FF^{><78GVj5nXX9?dq^Eei@EHVo+@bRFM#cY+Wj-J -Lt4*9;it#Y!JiiB~9i1e@o$-)~*Fbp!feG!?yBj@&*$V{jwX|y8rOBaphPL4w^=@EEQ)*I+OTZE@Iu3 -WAz_A>&Z@=2hMBn_C++D+-Vj73C$L&i>(4M-B9Nm|U9MV_n6QH1j9a(T?jkO6SkO1rZ?5P0KTT0bR-; -dtN|sGpyBQ+$j0`@(81ENSCiC%8e+_n0yt2X};e2zzc=ai&5Ei3!H$u|Vda1s?O{nL{7wRb3WI@S?Sj;>pUiGeb+4!co)rbTtlg}vjmE;C@e1z!YvB=w)Wo -&FCtniHn)Tc)ac_ILV*TYgnq^{uegP%o_g!3Ktr*FrT-D360k)kfVvkbsdD^w4xX6EyOM;(Osu4EQgc-2kr;f0}fVIZ`kuuDB8peNsIaLBx_jxBs -rKER3xPdOO53F7ErPuwliP~4w|>S6in@Q3u -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 -M^b5T-)JY@5T}@J!w4M1dvG_RoM?OL7P~8 -B$&%Jfvx}7>Gk^uP`car|TcfvZDjR$+;M*OJ;IZAG%l%UT5?bD@2HtRIz7GQo6ax?)>ClnM;)Y}HZ!+lMSW5>e;O^s2n9Krh -J4lNpV`xD8c*Mbp`3018M;92M^q{&t7Lm+ndw5)wSyDB^Hj^X|%oIbGEOXlw}_$&lN -8k(dM}4l$%eZ=S}cy)8;>2q-kd}StL^?F^`ji|89w&*{i;GRy2|# -_=MSyk~@>l7+hYAGY7l63UdU6xeUh5lFf5Tuz3~|mL#y*W)iRTJeG;xsX|)umHq4pvicZkVZni;+^5npMd4>c3D+0|XQR000O8`*~JVvbSILlnej>*DnA79smFUaA|NaUuk -Z1WpZv|Y%h0cWo2w%Vs&Y3WMy(LaCzMtYmeJD^1FWpqacWcYIRM~$DIKeNYdmGplymI?R{7*1g*uj)s --bxly>75{qHw3d{dIWz1{;j1Dn_+XE-yQHyK6I+kU&}V(5#Z?b!|dU5`~=R?Uvx?>VmpyXo5ld(()as -Oxw9m$B;kfj5K5R#6nKR@I?v`+?Q%ZU;d6XDhO<820*S&-FL4ABU=55z^t<;XZ2Sd2>wJOW35iu6fGd -47``$zOBTNvbWt(wM|i{?8DgAd?itIRhQ*=yeZorHr(D8NJNHP2#t4JG;LFDi@N%i=S^_{jNZ^4?*(8 -!g-#lFNTsVeV -*SQ^D1_iC{kPi*`0e*i2;@g7DniakT8++>n&>7`Jo5R=~z}?oYgs-a=ik|muqup8t=JG9##X0$qJWp1 -uobPsk7a1i04YB+|AVGlxse{d;kZr!%Gg4NY6XGOy_u53lX>#pdF0|Ue#EX2^lQT2jn> -{YhQARe_BpJmVVX7qm#de=8ZMeLpcXlntnEEY+k*%1471Y0HDjTP`O>lq_VX|my58TOjc%Te&w+uQt_ -jwdhV`K;Oeaiy!Ngx*PdwRk`f)BTyGlwEUjFKFx_i9&|pOmkk{ApM|Z4aRF&BKN@0V;~-)lypwG%7ke -+k78g2Xy}3Wygo7uE)2Mm>Fc5v+}%n$3%e0aIASux_>o4Fk-B&jB#8I7RY&3eiAY&Fay;sy?s-ujfo+ -p-WKlYSMM^02c{9m8^_u)SDj%~d1JwfCmz$-mX$ShLoMDLaLq95vkJ)QVS@xA+T?@iX<#&+g7JP^xwR -bSV#NkjhC2O1ds4&EqrVSCBWR~1^AU;Jq_FMAW>(l(4GfuqCN5NHOo=|GVrZ}6kp%{=P7Iaa2tmeoBK{AG!s-sDSy5d5q667>Uv0PRSp;Ap1 ->YOqG57WRICxPJ0wt1=&1g4S(&lCZZN^*omX+rRMDf}qNqe`q#DfnjHQKNWj1QPjR0JR0hq*Ru(r?ma -k$^L&%K;Eg7=XD)#4wNa$DVsRsiUSv_u=~y&fK$iBH@J5?t7aSca$g)pKERS>i9RKX2U3WWe=01@^5j -U^XO2X@z}?*7N*WfWkCSHS_oh{3rt(I^~FUl$C@%$fr)7HWgCHh -W>$37P3xvo1b33m&e_XL&m;or4yyX(JmUBnzHFZ1|(v$z#{c9O1e;)VHT7B%)o)1Ii$IoO;Zj9^S%ho -EYO)>gEN5&8S+f{g_*G)AxjcKzkI3#$g) -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 -5!_C2C*<*dT0cht$Ix3%ykKM1Q9-_ -dHxTWSV0H<07jV=#c-2YA5&9+N&{t{j(ZIxB@g8{JBOW+h+z&=lOrUybQaVr*T?VyWxK8n-CnDk%y7d -+Z!e&y*lKAg+6gVCr>PkJPDqktaV<^rZ%*26UVU$EFfSBelTwe6vsS8m%TTTuKCTs?4Ln(Z8%4iLNAmw~hwb+SNvEiYFq4(aI)%B7}VfY&H;Xhe9)ni96XUVH&+42 -S?b0o>mKL6_Tt;))shp9Z=@=-_Ce0;eEAahKI)L2QJ!mUtk+t|irfW(VM*1~&^Cu4wmPF@QTzCzA_q1p_5ED;7p38rC -=QJ7=+9ZUi09jwWKJeB~_6?n-Q(^Lco1gBeH-)3OFKRb{at$y%zvo?ZU@=I2*q_44xa?InXczkGB3=K -SnOBY}N5Nfy>STnvx@#(LY;f#`i*YYP$n={|16(IJ`MrZk3^ld&MfwzZ{&ZOEVEquPr%mm -8Zl;qG$rABPc@)dR5~Z`-GBQNxh21EPP6Kp=%@`fayiB?Xc^+Wo3f*lWWK*W^G%p8$sQrb)a3HYlQ -pqs{&DFHcjM;tYMTi4ePuw^wiJlnsalfV$Z#*8)Yp}O>4A2R~$%IFk@CaVulb0Nf@4fAj&c1}ci8#27 -zwYtZ9DgOztq-4(b>lrVyPBnq2iKBhZz8g}1%>>Ym!0%19QKB%<%Z4ZK5NtUSPiogh1cNI{V1#O?|cM -aD4typlhIuE81^FnS=Wzc!F@4~oddS=9_-2EKB3)JXhvFJy!>gRixiv<(Cudm!D3_t-Lm->eYV(N{2S -7IcVO}_#;OEKQo~1zg*1&_)c@Q7hm~A1AGEW7 -2`^T0v)u^wsn4#1k9Z1BR~@X72mR`T -13l^ur4Q^$#F62^=o%AeL}73O3|S|&uF56@6M&=2d6pD%gnRMwLVf-0h1Dt~fN5%er?ZPUBMKyPa2jm -C8k7^h$&-wn8v{U;ag%IxNDz_IZJo?4sva$;N&G_BJ!&}ocR)9NLeY`pK_S{FahW_XaS)_WLk6)>g$C -7pxY`gqvMU0F3&O`fH^u6Tsbn*r1Znk_f*j4Eex7tCQxBdW?f6pfee2OZ{Cw;24XK9^HEKwqle{e7(b -;6HCSZ~mi%Fgp?;1v@#JK{Uj%{Yt<}(;&=@ByW$Jyf;2mV;5u#6Qlq+nvH65h%+D_Go#g<7|poP^yNX -)bAYWMVTmLXJu5bPX;o?CC8Sg}@bb(4&TURuHfLL7S3}m*CO;Iv)|_rmMfqwxwx64{S7j`jlpe34#M) -IJc*R_#@2`DMwSZ(~)iN$pM%a=8pM&e>4mF_52ETC}x&0wdDkik_o_M&@7;bGa_ -l7+-W?+KtWs6+0N_h>x-){%DgkV&%jt3ZWjk+4|^W`FWxVnL+1;ltZ)L}Gqa$>2=I4ez#64saiVW{1UiTo82>9PY8{Q30#@7#R3mm15`?J42NudG -MDL6bjW89p??7{gXP4bWO_u_=m;C2)WvDr3Ajtu1GH>v&IqJkFjpR&8M&g~M<4CFx(nv_g-Hymlb*vFq_N3t>Lld#a8;N0!3O2z3_ri}*lo~Fv!v(-DwWe-1oZMgZ;v3cDVDizw -n(j+exK%BSc?-d1i^_PGi+=Z*Yggoo5UL`dp_=RDsDibXnchpZDaCBEuRn}V|A8 -df=h!JAo(kpy7i8wd%3AFTIKs`fVWMuQ%g((fF>e`Il$U_mR8oVl~ -s}hwdtBy{to{m`Ps{v`(rq|cQ5}gJS|C=|JQiBGpz@B+A^)j(~sw0;u;eW45CEX8chz_U13%#->y5zo -gSD^GpbD|KJpAux`)Sr2IgKoKWyFW5bOt}yP^NL7=S|Vk!*OETgyoP)(rDqtbTA88@wnqfpJNd9GH6Re|IY9ZCs6*@-7mzONw>G*O`~%L%%|j(Yl57KN95 -T%K<^7Y@_O-2MEfFo5q}fEh{#HY4lRy@*hZJ%Wb3n7o2oPQ)r1=i1@H_{OvZdEYlfGp#z=73SAVmQ{s -x?FX#hKPf=t8rI2Ja=tEEne%_O{%ws*4yr4nPv0_Cf!yyaz~S>pn{2}gDKOo`&5B6BPN5!*vzXaB+q? -kc>n!tMDUSrkiZDNF|3h9R{XsqfT_y-P7eR1Xm-w++Un+I6k9r3i!eu0}*ItJwp4da9mxR$8cgO&ujB -m?nI^1n9~H{U<Gj1q>yLonNU&t`*U&8F4hbhozwD!0##0zPFDJa`fFUI+Uf;(Y|~^-P6HQ^k4Ml+MR3qc -S;QQ

dbPd3PYyX07AGvS`hkw%MWAP9FHbZJ8U?NrV}*BO=vurX56-OQbyUZwe&UJ@9U%M+MNNV3DBsaj`2hU1R<{GMf@y&HYN>c9(;ojoYn&m84F5`8d``ZoOj9SAeT*PyBfV>S3~hrd -(&>?e8x9tT-9h}9dp2WwbD&hbd~NxUt{-*+6|nTA-{u_Ci^Q0)fDR6wv)AD;XqSrwqY?X9xV48)s!#} -N^F29!fcU3$4Te~=X}l*8*v-@-3Pm|eA-{mb(~_gwQ+bnuanK-*;Hur -&}{O$&Y{2W9u)-_$KJUUelAx!-9BRUT^a696!Q&}byp${BXNfIogNc^Z2&v=qHK`-T7Jj3ymW4zF@oy -kF;;S2qRi@HoP@n1Yu=Qf~qfpg_+GzLxPaOXtKy^M~dydog+TXXEp=H{p1_;X?Jn&$zWO&OFdO2IBNz -I+D`jE*VcFk!sU3IhqGl&{oceh@g?AU0D*y{G}qz@X%NiUr!yz{FIq!9@n{8w;S;PDu<#Kd|6I#5h2m -t-v_Mdx5Ci=9tnbQ2Oq>jX6ki@#bRo^IOMOr?`piwQwG~*Q*j2owJN62Eq0scLBwNO3LDqJfH~~(s<- -zp#;7){@D6JzK@4LdoDCusqnDpM)RZHE*NYGAV&5E|69OeCKbmT-BK{Ciy^W8d6^W^w^JTj@{`xf3iz -2CayV*d?7$`BhD-4qo+#W}_es5II*t+-hy(RU?!+OGB#&bO^#(RCE%3esc -v!gYKG`*NLX)+&^**;-Ns$>Qr-PLt`S=uv*~C2#wL`X4{@`p!dVKsfzekDj$9l%T+zs;p_)Nf((Om+< -`UOYN9y>>!2e#Nl=ydHDz&QRg5Zyj9=LP0;!Ut{gt$9#MIcmh*IMSgcSSF{c5hJ&6jh*2>wag$z6$#X -dDufC5hHP9Mh%YH0?Ihy?GF98LaDw%8#^_xL3#O1@wC{Vk&q89CfWULC855Pjw(BWfVw1?f$JPOny#N -R(EXo;w{%Pw09tfig7Cx*AfdShBJ -`(vA|dS}QP_QRiJW8YwZA=d7p_jr%lJVG4iwouu7JH=^x_i&0SXP=O+9y)xf>VP~}0d`_n6d7W1_YE< -Y6{NkTpUfp13~_Z4V?%qnUM1-cHr6`1%K*yAX`0VlbjW&-TK6N28rSzh%Mf&5>#S;<)2G{}eo9HfPbGhIo)sAGNB -*)g#V#bj2qTqJwk;V*Rc3)9>@a2`j5x0FsPmwEU{Z%@{2fGGJic@c!-^a=2S~`u9wCTHe8M( -&SArdZm-Zvu9M>}VCwyaz6im<1GqU`slvR@6RGMWT}0@3{I$1&l3X#c3VsVrk#s&9(R4k4mAKq$iPiT -z1Ie5*MR*~YvQO|Q(sfoYGO2Y)3G; -X{mh0bhAx}CeA`30SIIlel6`bJ}Oe`V=Hu+tW(ek+tsMK==8MxtHKmyNQdF=B*Ho9Kv85Yj|gEB8(@t -Lg{tl&_wr? -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 -gJOd7pdJ|ItKGA0!%DmKq7n>FdD{`$bVZDcQ9s -g?_V+cRLHa7FDyd`IF%3yU`i7LTOaF*1t*y=3$;kq4-pr^iIjJ}B`08Y}sW&GQSxlIdSFc}x+Y+H-2@ -N-Cb#T&mf~T&`ZEA7G(3&YNMja`IYLqp$a7Y%uha1N}&n@#$(XL>zO-DVMQI7*b8Zwb_7)@f?x|gakE -e2~Ch%&3J5V0;VP`lFO_rWPVaaq`>YFZAvTL=#a&V2Mz@bFoQz(+zT3&(TG>snNk9y5wdIAN&p6Jofc -CLppSCquDk~`efch_+t -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 -IVSo)QX0P6Jyo>K6uN^?2wu{e_&p%%`!BTeuj -!^ls-72>6e&pNM!grx8=G+XankMC-yxi!Vq)6f?kBU!$+quLwXc2^z%gjm40u0GOKVm`Eb8qT;AVs@a -soN8IZ?&SOml}7z5F4&>X^B;D5noqW@K%yQM!iR>KjDC7#P5=+L;L8~md7h#Dkbq3Zvot@}npL -jw_4A^bSOid%{4LFbfRT*N5ZJ-B;XoHDUJPBbv7Xx(7t1>j8$qgu!tni>aoqf!f%avBt$5*8q7rZWLX -nLq%19!OVG%pAO=;eoL4M`$;mzp2CX8u(VALoRs{TanYsYUtepCRAlAjT|6c95Cjnzz{(d#~H#UaaZ=}d_9IWvZPE425U@ -Q|+z~uh(^78uoAKqQ2H<$0P-&}o6Z~lIL`3bXuY0en`@Ykz%Z_>*j{}xCnEq>pr9@@5_yV(|N!({`m5N;oDQ23!CI4E9uUgfw -FfGzqDuXmtv~@-G4mAXKP(r3#vinwaoJ`y1v}#K~RO~abT;IbssVR=x3G-mJ{{Sls*eXn~Gpvx|W~jh -{s4=!~#PC~J09+xBDTSI3xn;Omvj7NZV3Vj|V^)m8Ataf}!MK6jdJmAb*H(_ -_OUqJ7XJOP3^GM!;WOx4ktuJ*~;6mf91nbhQbDcy;T#3!V7r7cz~Kt?BAci$8l1r9xvvvFFYXPW9~bV -_v%4j#J34cI8C!;u^tQzZ=W(f}Ca&sRUb`SAG@qlJLfd4)#H-UGmAW|*c)HUh5Kqy?A+0K&@(;Iyc78 -H@UYR~M3sZxYF8=FKjR$B>w{c2lw -&@SW+r#umWf{$W`RPErtN%l$Jz+fn!-LYfmb$>zbP|hVO;^0T-sm6}Gx8w=`ci^QF501V9dH$HKmQ9P -|{OSW>N_1iZKGO!3AtdEa7%rS-GVjaykAFxWT~1^=*zUsR;ROx)&@UaC1sCn-vfMH0-Po$wEloi@{7< -O3oO&f-7|lzspu&{32R%G5$__*I83;WHJVD7y;4E88agLtMeRf2Yt6sH3RQe83$(tgN&(dz;V>Zlq8<@Lao=ZPZJ^*;ofCaY5$NDGDd(=(OQnr;0{HUf?Y%YkN2pfagI1IuixiYV##upM5YO4uzd^nHoTfgUAoeP+<~)2dKlAvmvB#zQhlK2_$92CTjRx(5 -rwls<2Io)5&S$*I~&fDHi1*5m7@l_5K#v16WZ<+n~DZR&2f0!`ZWwKj?>PycS#s=Wm?{*H2=_$q>I&(jf2E{7v1ZsE2Q=q>D3Tnzk-_I{A;MJ)Ce1faZaXCBQS@wkPiPH}0ujG2~yELGj&=vKIt!`ey56m^aZQa8N%m>J+=~aI@~AvnCYJtG7O=sxu9E0V(N^F^>a6nKEkVW6w4w=K -tITrpU`Yh(tU(Em^7YGOo$bnOAE34ehXVmWiC(>me>ZyWd6AGzza=(4MKB-iRIYIB(8uKBpY%L!t>Yj -ah!j4@g9lFn4&A3N+nhQYY3bj7CpYpX?vGVqS^;)J4r`z}Y9F8F3~@bizK^(6aXgXZc#a=%I(yhunQk -+?4!6tu&{53Va`Tg(y=m-I+PnX}EhRQDnd#68Lg2a4$#o{>r5&)U*eW*v#rOrUU -5mJ(BIhW6X8GYe%Inv<=?x%Sj$sO=wurkw-;;q1`ktuN|_N!|W=G$)oOR)d|LB6&(RPkaI0imMD44(> -2qPnZi?J#wtNmZR7;l2|fU56aiFJTvJQ$hu}h_@@KhKZN3?9m27t$Ruku_Rh%bg|f_6@47CKb3%QIik -!KrU!$?)1yJW-+->|Rk`q0JUy%wTl2MDcm#BOdJcj3LS**X@5 -ewt`{6Z=aT23*s-82UEE8n2xrzdbET#irqik1kNCpkfyt#sqGU%`8X$ZB;}acoUt1s*|OuOZ5=(y3ti -n9AGcQjO`C4GbS;paJU%Y2 -k0>FFH2NJgl5SW##}R&n%YV7o{soyD!ydrH71K_0STNSh90DLskvw87_!UMXxnBq -S5-Z{D|c8!UZN9quhtE&!EJnaI=aysq5ef1b)bshfAPVcJ>o~*L|=R>1Nw%i$KFegGqv}>*F8J-vZG1 -_%Q{x_3}2F|xF>!9DL)f4Glvf^mSE9f&?h9vG2f+=8a?J`;_{d1Kh9so>PRKb4Srs}EJRMF0oY?2mqa -Y`@3Y&LpHKA+KhsvfWqJnkexf0ZYmquEV71GO>D2C8t##xp3HleCf2G7>9z{}HZCPhxDdl|&Rk6 -fq4Q(w-AhP(BoH*|557G3i9U+#VO>nabPqZZ(z1?ZG -bFM)9K#^3v>f-9gBc)DrsZH-C`1!VOT7eHK(V<&y)bhxQ;mSO@d*!dmTpCrwI{cmZT(H^4^yY+>79|XZ;IZ=B>vCwDV -8VuWb}LVYLaVTiTBH8_PpmwSuRYHs-mQ}7H~ -B`|K;N9azA@Cq2^yP3yzFQOD$J&FmQCH$!TqC0**I+ZM0ZOTCl}pJQxg8L80{}OqEQ}-4QuW*~x(5FJ -n0|po|O?`It;!kaHy?&HZW04a1t97o}hY&Su$})=G~cxfYOf$<5k}n79`z76v+2=7bolzf@|}*DFd>% -Wqj!ebhH$+}f~!XSiJ~M$H*so93;+#nBb>3bm^_7g(BKe8B2Q_|a+EQk`EG>6+?b2k67;_4S{#Vaswv -&We(2o>C#U6Y`u?WkxMCGs6&~87@ZM7$SMfT1_A{aE0r>b>8yaI3^bg@o`F?Bu~HnAsjP4*)b_uL{q> -Oe>HTTdy5D+J%87>Vh?bT7@Mie_QXcId-N#U=_+{PAO?=NczqMzMhA=;-;wWvgk_NMiVDL9J$mr=HuY -LGXiogfsJ)7Df1gu0*=Xz-4atyv(}&4uOdj_<_ZWOhl8|9v#|_Wy+PS}t?L`nBV*7AZBWj^Fk;?W=?u -Lt|i2ThOx4gkqE6d1|g -jkU~qUnlq*@$tJs&vtPy>$NTxMIJn<0j|H-X5$4;<%&ex!O2;*YuVVN|u7k8)5`?>MBCBjHpUS@9sY{ -(D&V$U~&(K7m1b*yxJR! -`c2}E2m*w2L^rN)LjQ4^J>)F4f`1Wrauri?IPX-l3xAu*FVWWuww5_XJug)AHfv?G#l7%Qh#xRafBNU -oL@h6sMcpgg_hX8Ljt00QooVod-UQr* -eQdbSBjoJNuJyjH4oLIJxPB*{Jy@j1h&x-H?Pr13_D<`Y5z|8K9DnNme7(VT;^&te1x+enc-yDS+ySj -+eFj))4@eozWktG4*%;c6BWsaO&D!Ba(y_69RJZh_pjr7a!fyw1Q2z?Y(G#QgvNeM?BD8evY*icj4q0~{z -+pwriCM{<8`(6eOEoOg*q@H%gPO=u3W^LfESn41xkz{^Yd31lPHBJECKZx1xZ4KjGI!>En*kA*|1}@h -q(TB8$q#IGV5?11)TQ^;f`g8bTrCog^_`Z4c(er9|;DT2Ef%@a2YvYRoYjoaIk}QzKN_$7MB&N_|O3`!(U$+|Ux%s#&?#w^n_5EU)o1gEOai3$FeHq -X>{0!LbdSqvE-UXh6djqz2h`1}XH)>;=wvA*iCViQz%J4-7T#(OZW{s>AjvzmuU(OM788bCV5IvIIa* -9=3QP72_`41LQCPW6^m$)Ii3Diab|1eUNRVOZD~$svlZx-bAK@_w8<+>1ScYn`$iC7t%Y^PQ`Wt{uee3JTkOLw)xPM3mY2Ts9v34Nyx11QY-O00;p4c~(>XdH_p -U0001V0000X0001RX>c!JX>N37a&BR4FJo+JFLQKZbaiuIV{c?-b1rasJ&UmofG`Ze_FT~ups+GP8$< -;pC~3-=|G$6*Hp`aPQbN@*g$_`J<)t2scH*1-GZ9*mYV(2AoVfbRM)?f`T!O8zsV`QJ?77H)jX><@T+ -@d7A8~*OP)h>@6aWAK2mt$eR#W1KK|^)~005W{001HY003}la4%nJZggdGZeeUMV{dJ3VQyq|FJE72Z -fSI1UoLQYombzF<1`R{&tEYL545K?@LXw;?)DC`5^yVVa1X0h*G-(ps$&P+X)hrDcV_IQb=oWt`jTdT -e*VTDPtr6^9$9{1dk(o)jtM2y9+;HShz3P<%~WBN6zvjGH`+J|4=Hv@X>^S?Qu5pht!%FX#cE!-wvjx -TxUQk7z4oo@R`6crZUrA3@?$ayc9=5T3gx&#S(ZzY?U?1;9>w5)A6}EB|MQ?q4R=w}MH*?+6{NK;TFJ -K!bJYrR$*kRy^$Ki@cBV_0N%9qZs)U6?_@$r_3e7Dr*tIIJK$lQ)cI-fk($Qi{ZBQZ`(5-7)x4{5w_@ -LNMlGjm-!V_5(A}kRWxwcMr%YGTwM`#SUT={@6>ovuD$$X?w6$fn!Zb(%#hn(0;&O+EmvJOqr_`{Jaa -FuTN6+SqG)dH;+tYehwBwWy0TsEXvJoiOTF&5$}#g~=0EnC_J6s2YdT$JP11Le#LZeU>>o{LiCxLGu-4dlQAe0XPmSc_J8+>7=&?mLbl9A=cZ;dDG%nG71K4XRdOaOnWP`D~7(@m^jc%9c -zuPz(hRu{{MAnLUd7R>|H;P1F-rIAAe?}5*1d|QG@S -pz=K|@yqD>7cs^l2^v?- -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 -4I;qPN$QN;FSy0cK_PMGOzBA?MnoITYK9q -tawV&SJB?_I}7wP_}6oT;W>ww6R%FdElB0AgHWvUMTD5ba>jV1<>2*P`|pR>DIdrj8TP4C52xv$CiX$ -2|5|>U{3kYB(4lm_3rFwZqWo~}`m`@{fEvGP%{!Q}Ht*TSn`v5sw2g0A@H4)(w*ne;-pJugJO@x%yiL -2&c=c1k(Wo=ZSWZi>dPxJEEj5_F@&5o&O9KQH000080Q-4XQ|fU;s?`Gk0FDa)03-ka0B~t=FJEbHbY -*gGVQepBZ*6U1Ze(*WV{dJ6Y-Mz5Z*DGdd8Jn0Z{s!$e)nHNczLjQv2WR903Xn08Bnad1W9j)Lt7ZGE -vMQP+mqz>(jfo)D9Mh0=C*Zyu`H4w$&V6Ex-W7rK^%ld%~F!D@`Xo*mGXQKc_E943j&q&_lT857@0g2 -G~3mZ8!Dt_1O+YpkY9f+K0*MtZCl?d1UQN(!q^EgxPlQ$^9;~4mq!tETFBEyErOvd7^?vew~N -j7`gV&Bjuzl!W8P2pT)Ttn2@x-Ba2)q6IUdhT^(frUa((k9aA4x-&+zkV^Yb>aC&y#+dH4Op>14zGea -E`@X!V8C4<^m}AB@)5>+tH+?W*Q(8O&fZgC9St3`N -<=HrzJ#^{5$aX9lc)$KThEw_tibEx>A&A231T6?wl6`HF0#0?Y8O>C|U1;GBOw74+#<ak4 -BcW|0u86;=hSnxa#^-FdBk1=xWK!lCY>GKWV3r638Ud9&Tr-q+npnbTdCa}sj$S4r{xOtNQeQMbG#Mv -gZWq9h5i*`u9s1YxNQ-Us-YXJmfPQ*Hb4Uh4_JF>pFu+McJo-n6&-*xQNpc?)bIRcu&wA$uBnFR(|@@ -RxuB-IIF~MN(id_gKf)El@uZ%~B7`{qMQGj->Yb*3^bzZ_e!X|0brJLO-+gt(lFWy*afWmTPsB)AI@U -v1Jf0tL(xZ&&$I3VcN~#ZO%I{GkwmdP@VdDHQP)f;ye6O^UNJKh6jBUaL#Bh0$!{&HVi31K9NDld8)T -}6z>_BbXyHkZwS18!?D5tlN>~A9?+`wr9_K6tdb4y%)VyEtSGv{frliH0e2tH5HzjR^BY~QH1AZv3_I -wfp8ouVBqWpFQ=*=cuyN3GI$oy8kn3ZB*wG!;^n!X8bKGu&?tT@DRHrws)i@@^z`Uj=i=gAShs}vBd3 -K%|>utgW=_P+a0GSZZ{TEIp876HnH=EUF4qx&T_N912jI1+#NU%^vRR03q7y+>ocKsU|xg3E1!xY>I!3_-SNTUS}sky274$p3yb{E$eAcG51od5CR -sX2|)?2WLjbp=P`xCEM+aYBwp=PbSPa(XMOGHdKhoma_w4byH_tP@>oW$jX-LwdPoZ472jFI>zL>5KT -;O>ZV{-R!$UMUDQchR1M|uEZLMrvd#oeH|%i8s@(8jsGp};M3XhG0H&M@!*Y3dcXJn$#r?&_bqy!(CNlzioo|0_S6c5zpCL!ZQ`4~_j^hDH -jQ)~;GM_LTc3lMV&|0R*jFIkETjwk5=eZ>S2%*bb|SRT-q324tYMb^-qJQmHKATpX-AoW61(W` -AG*YEM;b@Fn$Zg-aI<1-5hs1ziCd|Ol?rUi%K$Z${)UTsOmDoWUvOsB1&Jcdtg1Z1C11^5BUI3+clhU -PKJ$V0)K7D5sQ&Hu!$#Z3^93W2#LxDEW&HbUXXAWG0TP(>u<2A=ZALQvSLEXjf8V5&`~;$WZEH -LXa)Brl#Z~8K7m>mQB+6nEX=gD#mHb>HpdSJ|E!7;K^UOM!yy4g7eNN@$W4L0a^Zn!lz6I;jb&{ -Z7+~4r&Hx5lgS<~hLUmt3Hix| -6SHqHQ=8)VR9$T@2r`ivDh782R6D6kE(1bHwuTO?9?Xyf3ei&tO`-N2(8&~QDCG~IG?`zW^DA?V)OhY -~@P>S%P^%r_>ejB&E$MWFcw)A~&*l4-d4sK~-@H#S+q!Ab+QDQA3rZb|T?h5n-CFDc>@FNCu -uLSz;0-|78W38>&Z|hi2a>a)~477pG0jD^+s$%Ia`W>_{V-eoHFnWYO^4cL+o21d$h^J -(&B&UuQl?(hT`#d+UG*nLL8j~j(=MIkeWgd|H)IEV7VD-303L@yC>!16esOu%oh|9^_Gp}1G{fiXo8L -ZO_bgVmbDC*!_2sf>>F(OIISKNqTTWZ~N9S4_l-ZCO2`q?5N7L5Ba@ASG+$OJdU6?`x)>e?t1bQi_?; -x)c*@DR;)nTP2?3-0bPtp+ywjGJ;sX~c>zHKBBBL+0#kO~cXVs;Tm%O2^Ki|R?$dyat?;xt^n*YqKVT -9>p6ZF59qJ|`b$z9_QEoLhxw&}4R@I;h>`T}Zz{$V0Bj&Q&4;CW4v*kbtVOhNS1#_(9mW&DV;oxSSqWT6sbe}7wgIiGQj-3l`6+ktM716ngwMJVDy1)?GaHJycP4eP{La>E%i)=_2as -M*zV39Ylxlm5;s^$JwNmwuC$x`E)NtVmqQS4Av4q~80+KKgMUh^*gc|0IngOcmXcN}`S-Ioqp+2873f -_>WDnGh@6b}RFJ*t0+0<~tZ#a)m>Oek|y+)mJ&HdwQ1wkMuZ%Q0$IaOAyvx4J{i!;dJP)}|t?>jG5sM ->R;Q^2TZVHmQiCF9U9rO?)a65J@#!}f+;H7Y}~hpw-+=7EwOFD$rTj%w%@n_-+KWT;WisX#?k^>pY2< -jhGZavV~DM77~dNrl_@&C3a9CL>Yq;9g<4^;V9=3f&EiZxlH|?dRd4=c1CL@VZdXrpCmX9qJO=RQL&@I>P~> -bbyc5(FwqR(z2#WrTTM3zk7pOuNUuIUC!RM7@4`;y;OW`i*??^H&V|t+UB%s-+A!(>i_TdX&L6oQ?dg`u4B$E+8?| -T_p=DZ6{dFRHz9IiXdu`CMwrvSL7RSM3KZ7$iOo;5nz{DtgP^1jr5}So_NO{U*kQR=j=+eA#_s?w1s> -ciNE6ncQIo-|3ZAmjQ2B-2Rd$Y&apxD?$kvDu#bT+$O={NUk%GKqO!4&9RW|g*pxSH9nU?wFxrNZejf -q9ahvbsy|Hs*nPffnF31jUoSYy4um<)z_T4TPz8aNjFTbDj0ySEOxfzb{0r{|``00|XQR000O8`*~JV -T3!3Cp$Gr~OVaA|NaUukZ1WpZv|Y%gPPZEaz0WOFZMWny(_E^v9B8C`GOHu8Oc1*;;c1Z;% -lbhL -LbSm}daRZ6x*_NHe_bK(#F!kINIh=m%4r_8L3bQ8HLc{Mrdn07WnpL>57k -y42c~?s12t*rs!Icq}2-KeTT=8S5o)upi%9B5GrGb2acM>q|7Sx}vMhXFr(n>U(QVS}2ipoMZqpL(`x -B|ZOq8ZsK+EQ9^^clc>$KcvRwk_+*fOX+M&!J32147Hba%z~WxuN~R?9cL*Ys1W-;lgsV&&}P%#nsKt -Nfg;tq8^l$bz(2tJcZSA!%AjYKtQ&5_b8O?THcvcD1#A=&b`wGJkgZ^4!W+-qqIU7@jYmq(TYfqE3ys -VoCCzQ-AgI$SPKTtR;yLXE28)Ei9VtWr+Ba-+Nd?L8yob|jJ*9oKFf}8Rs?<=@?Op`ZyL*D3QO%T -%~&FDhRP~frQD85x&`aOC8p3lKINoK+3yH*5{?ifDD6N~XA%sx_Nnz{S%ClWb9co5(O{UAu8I5XS_6g -A%rZCUTc!05fV`hs5%?t%!eXTZW1Qb>-g{C`q(>BvObErr6{(m1%XHA(!1z+2*D8Av7;9XJ~$ls)g## -hn=jYc#9kIivyI^FmYybh<`#T;~z=$41LFQM{FpkRrMR0JDOVUhztNBT^APRIq|TN<~*R -Ly1%{ltF(wn9e+CIrNMpWVNEAQg~D2rwO~a!`F~serykMUw3{!2{eHX+Ah -@{7h~Y>onlE0AeIbTAO|oG*pnf1cN#L6gPu%8>!>SaRN6WD>FVU2N_<0>3u^)-APEc&OI>p+)Wf?HLR -^oqqU7I>oikU*m%a>jtwGGu#W6F8Ty{9f@ypIWPi(+mH-Eu(ctd6lP0(g%XBTWqGEqn-Kbv!r%qS(!N|d^kMQK_kaiMySkYB(Ab -t8AI0lTkVeReDU$eZoEEVdx%pBbalh}b&jB#Z3i7L{gVsA~0%{!1x9UK^!7IpaBV}-tlJADDYXqnOnp%3-j-f@rbmj9`DzzzvuU-RY80A%~{47TkqSk(YV6-TuX3j`X&MD%rYLb -zNQ-s-rbSzc+&wPv-Ay<|KZbryNxD+6I>NR(twYi`yh3+e3CicQkHj_trP!Fg?|8$V5wh9Wks6k|o^;uOw6m@_og-)Dr|kk@Ni6#Zy1DrC^QY_IFK#z8AqQxWw9!-D-lJcg -QVqD-^GlZGSOJ+iX6cM_77g8Y`$pTAZswSC1+3=+%{$3OY<{oZbFg5*M(h-Wq&7xx;#0v1=Lhwj>BwH -y_PsuZ%)~OR)~O#pHh$t;hz20E5v{-!`TpI???s~tR_3F5B@oR54HR$&VRNm$KMS4CrGRdQ9cK -X~d@CSuR*!%#}0>;bp%jnJxu!7sr1vv8Akv{>QsdRdMU}@P&Xrq25?`>`$>5vbT#U|GA$nAk!ZlTf+e -0FAaRqOlBi2JZ|Nkj9qR6arb!dE|XPpk%jh&kWNKW~~NIk9X4MCOI{0>^jGah-lo)t -^C-}Z6BOOP%gGm}HF^6ss1Y(Z9uVjk^M^&g4W=3?ZO2u1Rv2eAH)t{H8W?I%+z7szPu(`=Kmk6RU_Xd -9fp*k8Dr)g7KdcEwMLHVC_0l2ctSnOi4!wV#;0Q8rzZ~R*>6??}vy3W`cVq`kF$5`$PD(+{QP;@g6qJ -Zg8&DjMbWeNE?Vcvofz=w -wK0}dt%1ADyb-w{K3(~OZkqt&VLt1Q<2_G1cHYn2I8VVnvEQpmbT8RdP0q}y`;wT5x55}T~0x2tZRba -OT4_Xi^_N)VsL1useawrO(Pe75GN(U~tT;c@@}P)h>@6aWAK2mt$eR#UyLkZq9 -#001Zx001HY003}la4%nJZggdGZeeUMV{dJ3VQyq|FJy0bZftL1WG--drC3{U+cp$__pcy46q5m4+a8 -7iK@|*0H()?96ie5aAYf_f*kVJIDoHt2fBg>S7A@caW4Ed-kF8k86)Z@ICvp3v|nr(9all8L+&eBdc}6)NOPnlp^~#4*V1*b`98^cE~_I$JAwP -FBNbTB_ZS$!ZY(2R^r4!w#gq>nTg}%xz6e0Zg{dIaS6o+ZxnX!Dz_+zjt?k+~1!P!IS;zR1r)PmU>7$tg0J~}?%xLi(xA_5i!eGnZ)TpjBJa9)f04 -{ZO4-^WCM);?W7~?(HN -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 -&GIT1t4V=nJHUnspj1;M|W*a -2L|$c{U6hr;(&$@65TX02C$b#YNX8Jf3@KqJ~I?Tb^C86s`%~LgFvV6N#X~*HO -6I`oQ{AiZV-AgM8I*YLUYHh`gW4Y6uZKE!_m<6pcvtvKvzxN3d-q?QR|kY~Eww4y&F^tyutM -ouvqTK0}09h_3|%CVt9Z!`=zBySTkJ^g1^U`fmt+oQ#w8;Wrlc2bN@8SLhmN}|4>T<1QY-O00;p4c~( -;`3unU81pok=5&!@n0001RX>c!JX>N37a&BR4FJo_QZDDR?b1!LbWMz0RaCwzjZEM^(5dOZuLg!Mj!S -%IYmvf<{?GY$xNZP`&EJm@lySmttE6GWhkpJG%tF5&Yn*ESnNi(CFXI>TB^;{^_^8KE&{c0z8hj_1gE -F?kH9{#}XK%_lW#~$eX$p?1K(YYc5-?Su0U#L_$`r0eX8E$j*oxzWlW$bBRRCk0mTogU7Z?V3wxFbRW -jhFX@>~VekVK;N+eoQ4$2DgFmqHd|@s=J1pJLMCEg&;qxns*&$jb}P%4f*x2;6s0A51!(c3adc^dqbp -W{&aQq^(sf#-_OoIU0*M9TA{L#1dICM8G8W~RRcHyDM)TxtQCjFjxUKJziMJl^Aeo4pM>BdM;}3wpf` -gix!~$EA6S#4OWK?BC5cqq4eGTmsTCIidCG3j{N~eq)^nuV>1ANPw7Z|p<`Q!U;I$eHEnnN)3$hC)UH -k1z*d>Cnnk;(KNl{qriK_aeI*gI?&1_zpy(L~o)IArspn{wR93XXVcb1p{R=BUDvi9`%f+|w+LG^=Lt -#sA^3Z+k<0#x{7_R^|ixFc1StyZhL#ZuaUbKXm5(U+}P2tG-YnD28Yd%y(gxDcI*5rpqBZFQm{tEvSZ -8OIP=1b*{lkov3)0`=#0FZdq0l#}QmC*gg}*^nK=O;fp)O2KKn483`eE;u9WAZ$#!LQ_XhiFga3)EhE -e1?FhK7;j4_Qt6&?K`KbyfeIp>=rqe-Bn1NPr7VnBH@h)Prs0*a7oj|ekFe)UAq4 -%WCuuBsO(iP_H;>hKZW5F|*)?@04^4YuqFbQ{Q9v>VhQ^jS+VwAB;ldkTR)nBD;O*y&d!OYVGyJ`$)+ -9c2VzQ!>XO+abo4!5D!U{k?g+HG80gM|K@u?I0n#SHyrA#V!aK01()lQ2JVCNO1&s=%Q34`J5$3N%23 -Y5CTvwNt60}vn(dgqLmo -CV{roD9w{?LtJJSR$t90K{7GCOx4#OM2ua21AlBWEOTWOTBa3H%Knr$(4R5SyXi{nq6jq{uGg>(3;Wx6U8 -6xS^-hNn*vjwtYhLRRLf{JQ&?xu^Znuv8Wy~CwhL1bRNa=aR1;? -CuM)kB%YeWXqGCF-r^$bOKulLw2o^o$(cKXW3RE5FH(b|su^)81NhWy5Bkz2Oh|bdxv7Y_sK*VvWkm} -ZB8>%ZWF8YHAUD|xjh%9rNSVr7W6(|2WNvV1Na95S`v^2fQ@4-3CasK>(dD^F(X2J$G0+X@y;V~)fvuw#3)|5C| -3^B|@)x;`218cJ~kDf-(jZI;Yg-WY`lk%GwNVbh@s}2VRlIG&$B%i!l)8ye1r4Nr;Y(egXO&&Zk*N5= --98+Kd!#MZNb;%||pZoX6i6*PxP)h>@6aWAK2mt$eR#O~ch$~Z3ybsJFby+PX2nh5jbiU0amqSJCpLR -a0-EN4w1$!I^q<@&5e$?b*de`s>-N^C|m#R(4#-rtNN9RM)!U%NC|ulzhrw6{4N8H`}(TtE@bdjkm=% -NpEUHZ2P?_GVcVKw_j -}(znokHrWTBwpnw{+jP5Y*L5}4x8WbtyxwfGYNQbNPNs&u_D)N{dz6bXA&(w5j -V>y(rpsnwLDQy6yC6GB%-Tv$hj3=aL(6&)&X$^L7dw_WU{QSEwidlyi593aGQDshcT#S;4qvnPC%p6d -I(g^UN6o(Ia37Ho-mE@HSg!ZDzOT)lJdV6=EgW1y)sxWK}nttc5jJZBv)J|1b3PxqKMvElC~$OaQ3-L -%-B&RTg>s0ssP{n=*O}{b*|)+?A##`DcqO`q=8^nFyjyHR@-zZyLO$F!b<1=40>@5);)+!v={kk~ZVQl8w?eTb0MFC9Yqw -oH!jcjiSoIcX))2je-!EXdauC3~@ovBiZI_k66z)<$HdKI)w5yi9N&8k3s{VIc)#-IvFS2q3x?2>I>7 -pyjWlv3Vj-QJvYj)4dvcBcZ^M)@G30%%V9pqQBuDfz6he&}hEv`{QZE|!}(q^%Fzj1L&VmwXZ?a`92X -g4I=EokC2lQYLIub;YHK4G(`BaogFcsO)$SYVga0MpTciy9`P?)WjfhaiSU43rTLme9+(VNo(5eB~d6@~J7p`P{Jk&)Kx&A1M=wH>IN%ahh1!?J -2&NpM{yYrVUMh#cDYosF}%THWPSSOHaEy={hmgm4vz;cP|{P*V<$aQ}JFR-pD>2FOiJ@+shFkUUIV -~53=K@nMBOzxXIYc4K>$ND$75-)XBR=|toQZ#No^jEePS9L#33_h%VnzC$AXtDV%XPp#e0Uu$7r)*JCXDq -;@*x+Y@EcC$-Vjl^9s -Rqk(smHS=ir3br!q%65)B=e}9x35$m9f8OaySi3F+n9tC;s#_2j+cE<|!9R -iJLQ()({$y374#Z(Yqp5k`gZDF=b=&JETXJOc!C)4661dQ4Xv(vS-!f%;h{SC)=~2>ep0&5QbB_1nu! -Py+Lny}I=T7k9Ug0joixgj`=n#4>vK+^nM*<`?_(e_UH@It40h6FPK#|~L7*3<5r;+?S)SYYxLOz61* -o79gV8Y&k>j^Ta+_C>TA&;aynWC0>{OtVv#j|(M9{>Kknx+o!^l#F@OIUotxee8_&O4MBWC{{6h>7I= -#o62R=VupZp~0}#s>4-C*CLr5u+azaRaJkB?OK6)4DOff-+v9D=)?x0#1|f0`LoS!6#$~nP2;SFbHv6!+2+Kk3bYwPW-1T>Xl%9Y -|UgT5r^#jRM -24!+d-yiy#lzm9L%vQ2P0Sg5GbT@;J`R)fitv4T7JDlvANu*NFJaJyeak}Ivs5!i8!jOayKhhvx@U2P -?Os#U)M#>#r$&6j71zU+(cK0?b8bmti35JK;gQ+?Qsg|Avh7&(q}~zozM^a7(nl(0{oOcR^P5dcE+&+ -4;&I_|B!DKW|km;uCoRv0?!Nf`8Z$@thr=K*y_A%8c;K!z>EW-?F1%(v&9O>A0Z0I)`z*UnE^vlYb++ -=m3XL!CArJM_b5(?B*DC@$({-=GdknRV!AkB%T -n5S=sPxi9|SS>)8?*#nm?9`a7&KHrQPkxR^tNS04EKOBcAE(czA@9>?yP6pWEC>{ci&14s*OGRh&?JF87>VhBfWp_m>hF1qa{nzAPXaDco6WyLZqnBrl%J -UfQQnF)Q*wDQdK9Ic9FYPBx%paLC$1mjAxR$tApyVn6@0R)Pt!naG=B+s<@`>4D3l+?pIJ9U1Db4OFY -gk)$fL!ijQZ5IpTj*e3OFunuTWz04yCF>OKpnkw8k<9{>Ti*>lg`_5>Nls_!cZ&?t -FeS9FlTLw;CiCAiadCf57`5xx2vq`pWOS>p&V3dmAV%rx{JDB~=Dq}NT|ZCzS%1S^g}ZQOk%xF~7YjB -k^CkJ6bptvliOQwzGueY%CBQvYG+vyT)7VrGkCSW~8!bE@hJrVP`Ir&K|339iN!1%8XF?c=^+BiKsKL -_eg6di4uE2FPt6AP}wmJeyro8$B~6kJ7nx6l%>X&8Z1^#dVKo+ -6lMy*W>hxy4Cy@zu37eTml7DvYj0ld0UUF@V5vaMf&`8RT4&1T;z@Kkr0i&TYij=u7-pAOeMgxtOu%e -g)Pl#+drBic#WgRPgGqr%iQ7<`cYK -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 -nv#&Jq_@Cc)p*KsTL> -o#jDtTJz-A`1d|a)br2W*`ttR=Q>i$V&@8Ukt#mp_IO2IXAfiF#NkmStx)Rf*4XK~Hw!BB2-&JWR?T# -hvzv_-+I(TT6p{3;!GO9KQH000080Q-4XQ{z~G$36@I0IM$m03HAU0B~t=FJEbHbY*gGVQepBZ*6U1Z -e(*WY-w|JE^v9}8eMPOIP%@Ug3zLL-Rfo$3z+A9D%58f*(x$RVubYb9y%O!vUFeM^T-nopkb -qt>$Kw0nE-qfae;-3(cUz{JvA~A@OCeej!{EqCzHRH8Z9$$E?G10PFYMwno)#i2clfVEey3M|QHwF;B -kO5JdukDWrjq>~^eFgdKfff-}P{aD$R$K!a -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 -eHoG+o6OA>`McZaIq=kdN_uZry49aKRRQcoeZ -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 -@E+d&py>rsg>(xd3O -VaA|NaUukZ1WpZv|Y%gPPZEaz0WOFZfXk}$=E^v9RSZ#0HHW2>qU%|O3Dg(CCeHb5k9gvYcCBgbr=MB?U1cT6>g0g06NrY4l2ygsPyzAiDyWXEeyEZ0(%o;f&lHnY)*pdyB8j>rAv*0Qn`w1Ki*%x1HSQ>{Z(d1y8cg2m -;G;Lm(McmLnoJw1j{Rc+Z0y^bo7z3dbjm{b!RTQ#&GY7i9zozI*Ek7Y=A$1$rGO;0yqr8Lwm9Zs&-pf -oWU{IuRtVJw4Oqq&U}f`*e#%ec)Ux3iXO^GFqtx>I%$cA)kogiKC8poWdKWH1!{fi?uN;+-^SK2pm{U -y`jct-idtaBCPkv_>vV%9*?XalxqpZ7%#-2IfMj2Gg}hO0KVp*ZD90BwmhZe}yjg3$IA0!JqHG#^m=N -97rJW&0w5io)BLzkkj!vLD6JJ`jEhb)@;Y>j&jcxjcwiLK6Kjh7rh?|dD$wtA-ADW+>ti}=MmjP#nue -e70L7b1Y)HU5XJ%H@D$*71-Q?y(gX>AW9*Yo@$Un;tngeBV)N`N%$pFFR`x~D+#8~XHHK7O2L4O~UoX -h&iu@)8T+V_}LhO2uh14?Y{D8=M4rkxh<9vCvvT`0+VX_~Qut1t(?sIiaZ7?!-R2B0n;F@;a6jJ9^r) -=Fx$XnoTk?Q3%@ww{b0V4#1cJ*f9g9|;GA|A~ExvEW!wr=SS ->7{2)jnIC2b6;Ep9yUhV10Qe*edKQFVhmMfu)`)0dv1>q5bK{pP#Y!^T8GV!HW(fL7xkSni+b};auFX -Je-TwsyY|KHh3RHyxDcx?#=dm_n?vS-a1=fTZeE{UEWh=v1gZ(m5?jEw%B??&BWIsj_Q31zX_oP8GE#+GXDX{zUD4PJWhKs<)6@7x#s44v -THW0MgJ02$IT;l3kmcVwjCig5yao=rnVqkvgrpr=t-3Ytjv0F9+H!&VeO12|NCpOgX{%M0LXT21^o-a -~bA0Z0pnpzuB4X7VGJq>mIPEUiOL%Ywi}Rhd8=~d&7rx7Fd#VYuE7=UQauugcvCUX#!*e#5Ma46SfEq -V(EzywTwF)Y!)U5H)a-t2{Y>tV88iUCocI$9mfq}l;i_dB17NepvZJU%0?^)RSsj-6j#qSoaVxz!46G -MAMBd56SG|s+5=>_R1tXl#w@(Apgp67Ov6RB)I=kJQB%EH;x-k2>q2>yx -|F#$4}#WpMhypS={UK9~Xr+qiTcnjZ4y?q`XNJ*hgWqO3;zFcT? -qagiDu?fs3iwII>V`e5!4`;3C0#*!6AkOCK925%ztew2vSD5pc-%f>g5I`!^ha%i6GYS -y8i`GO9KQH000080Q-4XQ`2<~E;|7L09pe804D$d0B~t=FJEbHbY*gGVQepCX>)XPX<~JBX>V?GFJE7 -2ZfSI1UoLQYl~T=W!!QiK`zb_sSPNYqpohU=JM=W@20I2PQCd;QZe?eoZ$BkzHjfxe4LQj2_oGj#*)? -#YQ)XPX<~JBX>V -?GFJfVHWiD`eg_PZH+b|4;@BI`+Ze|0CA0R-7rTYPTv0@l@Z_pNqHPP1X+ppiBE5p@=fQ)xO3cRIqcv}FpQC*mE&}e9Vn7+2W@4swg-y?Wg$Q -*l|W1XC)+OPjU7mk2hl6~*^Zt!M;CG|A~Cv5+(>;rkp?yBg5Qmz`>LVgYG*DruAw4*kv(a{;4 -$e?vDXW+`z-lP2oP)h>@6aWAK2mt$eR#T7SymDOu00932001Ze003}la4%nJZggdGZeeUMWNCABa%p0 -9bZKvHb1!Lbb97;BY-MCFaCv=_O=`n15QX6n6U$E2N&qQyv{2k%0Hr>(h-!S2-YRA# -06+4;8u?Tvm#)GbRr1!GwQ&3ZF?7nk~?t{O(0a$EY}iQ=-lAPL`p2Hlu?R$I8u^}Yq_Q0L^Ayj9v*KX -6hry^Ls0ndqhz~o3>|wp^W4g08!`u~RgcLMSLMlq-HFJaVy{Wbb^=|9ygN&KsPmOckBo4-u&h_9Kxqu -N3~25O?k*4tT)OhVQfXGrEVGC5qz>1QX%FqFjgmS#GyjMKw9c8ib%vPNzJT-WwE6>3O9KQH000080Q- -4XQx<_rxOWBs0Nxb<03-ka0B~t=FJEbHbY*gGVQepCX>)XPX<~JBX>V?GFLPvRb963ndDU1^i`+I4e& -1gq91338xXnY-vT%jANz2jn8k&?ogy3lH*#_jx(9=vF7L?@dX -8ojis6iRnu*J~jgmF>6!xBG`3zTt&_9;{fh03Qoz&32EtQJPm3r-v`PPIgP%L{q}YtmVZuKR~{xubng -S%0B(sJY3pbgXSgI+Rrg4xnF}XK9 -`{yoNkquU4y~;%M--?7JeukuC*syhm9MlsHm+08;ivXxo+<_HWIw!AYTWlBTK_OfI8h` -U(>P%7p_GQ40zXcg0v?Z6m106NDtlp6-? -=y$r!Yg5qmUZqB|xf@;5JkH4RBWTQZzGX!Vt^&!U?nb>&BLNkmjmeYfkx7Q)!uCY(e`B?{l6LSVx -x?YdCZCD|K>>WwHMKZEsHc7W}dTyAlJE5e__$}K>S(&ZI?7afpHFm;qYuEGQD!th -Y@YH${?E<6gvmbcInUfz_`pm+m#UTk9wjR%WZdE?J!Fe;caEv&o*{_Kztz&H3@i(+Ch@C*|O?dmLAVL -ZrPB3Rsm!PGLoo>=_p2*ouX&$SI7MA5@wLMgPBY@uDHslKJ$Rfg4P&%VK8o0vUvBLlA7uo&I9Vt>d`1 -<-QAn?`D)ro^;jz`IJI@f{Q#31GKY4JL*$d5oQfh~cXn=*0d+NIk@%lXx#0#DXVJ%!&~Q5k-|S!noxa -3bf8|r0|rHwAk}QQYL1}feQ7vI%Vv+G>uR*j36*xDameSca3rCWKV>XTN=wvIc=jR8r%Ex>es8^|GLU -hz8;f&QF%2+^~pA=!T?6)D4jzB;2NkHsS65nhh-(Z)5I<{4B6|EFpAR1*F49lN?cO=;O2@qLZ9-2aw? -r>+GUXH8>^l+Q!N2X}C_^2t0`5SRR3_yUD|pfb)V;-czZ4vHZtq|?D+QA1#2+2v9A9F -3tW&ENWs*s%e7$-;iDGb%E2VIf%Km~}C8Bp`6UK69Lej-(NH`=O=GL2Xzfu~I93O7PFiX+4$DA}i1o0 -#xEnj%W(m>?=`Fd2CoMl67oAC_$X^ -7=*xRblBise2|^vvRmF!)yE#uC;4rN$-KHKW#e{jMy#(?mTYErTy)|$>(9et=RI>C$}8AxR4o*w(>zx -2jj5evZUpBk*Ddx`R3<q&kXQs@(XCnkeE@Lrc4kGoFt~lwZ>3I%v|HNFIqjQh}#rON4U&*G%O14Ai -2e!+W*$u841HE>vo>&R_P}~?>f$Z82x!pou|V=OwLr#|BxOZ`H-Q72L0JRb?BJ9!OPi4yk{;n13RXgo -;t*U0D=lz=p#E@*1Z}hI@6~WpvdB71*5p3dzQIYMp78qh)#931}}kb6BrOP8lo!UKiBDp(1i;AK*Moi -j0rYVpicz6KO;5yIo(@%f}-I(0(1LQ`}ws@iJ;>Q%YE!|Pyn&08xngt}E?;g7F?+LQb>E{JQQ|w -~$TGp^swlreg1);g0^!UV~vf6bw#eTnKcO|Hz6Xv(M`)p|{1p0B#sWoDYux9pKj5k1Zu{`Tcfm}=<#L -EMW7sRU_H)cY+Pwu>w+>?xprjT`u_dTq!Ap~^g#JBTVh+^q?Z@6aWAK2mt$eR#N~_vuN4@003JA001Na003}la4%nJZggdGZeeUMWNCABa%p09bZKvHb1!#j -Wo2wGaCvo8!H(20488X&tU0w($p?%C5(w=HAp}T!tlY+(iAa-@xVx0E$4R%{mYtc?)bV@qdw%JIA7C` -2YM#hxAJmB5FT5CcQ4<>*!3?7h3>`Y}Oo!I|rN-evMbU3i*Kv -!%I8A?EhM=ZTpJCMvaUspe#UspJa^vwRNsO7)x=EOvB@q$S%bTar2x$8+`y&MNxDXV} -#tiXDe?htInD3bUJ;fF;FDO5fCyp?5S0e8-HF+1nnt;^9+ti1G*UmI%2@o2pAW5OsjyFgeFkOc819%y -GX0`&V^G`pY^P!93+*IY+u2<4{zLACVyf2;!#K7E5T-MwmnrAF}>IbvH&j~gAcRJtsxLk!HMFHYkz~F -kztCW)^ME_2ePw74)WoF+J}uJ(C38x%2V0)zD7H>c&KGvtMUuy|)>fWAxQKdX3H~rWOAHP)h>@6aWAK2mt$eR#QViOU@tw0015U00 -18V003}la4%nJZggdGZeeUMX>Md?crRaHX>MtBUtcb8c~eqS^2|#~tx(9!D@iR%OfJdH&r?XwPf6ucQ -c~gq08mQ<1QY-O00;p4c~(>2N#Fa(6#xK!L;wIF0001RX>c!JX>N37a&BR4FKKRMWq2=RZ)|L3V{~tF -E^v9}TitWxwswE_UxA@#?@EnKC)>I0j9T4h>+EiJW|Qp1&dr1Ckzo>&7*m8okd}3~d;j)(J^&CPDSO* -I^r3EVR-%A|gL8i86JU}gCs!LKwz8b>5arz^6tF^+>;cD60DlU6v|qbe(4WX=! -v@XUa^b=}7_5h5yJS3tbuaXw_CgM3??S(Wp9lQ0n!%sMhYQ_CM6heQN8nEbh{stWEz}t$ -uHnY24GtZJDa3Rw{i{s;#P;EBJh}s`XarT|)$f9r{;U!c6|HIqZN1_sir&z~6-|OL|}bvjO>bCx_nSou4~85rrDN%hON$o --K!Z4by6^H&d&h*QNo`!GCT}PEK;Q63eY@KOop -Tiuqm45a$;1unt{6p5LL|*VN0_0E@qi4lXc?86*^{2yQAHBY9XDN#I&0PE2iNVKt;2k{H8T;+IaQoeIgVby(>u(6V90o^DmM(fPE8 -v8#^*jP2mpG5t7--iX20*%8kJr#w{FCB!Tt#rJPsf9EiODzf%GBjfqs>FesF*B4N7YLXAKpqNj#9WRW -Ife8H}jXNg;!KT#N3?$ZUW&#$d4>Pf<<=VQ_Fyl$#i`kV$gW~hbx1I;r0(bE#*)+`#;er2}pRR2*0E|J#36Z*X$dF-jpaoRq1-I4>Q?ch{a8r$X2~5h$Ekw@K>2@+1!u(`53e(bX$I6ziUr}Vl| -Yn>cW@*v9MUg7FCt|R9iK -vzXg@N|OD#yw5JsGK2?<%2GAk?Va`YMEmODn%)4*cE0BsiQ>D9YJn{yG&Myg*yeF9#VsCQK{Txb^zO= -aX@7g!4a=g@j7c-0Kh?jhX+M(tsvPASU!*i5u|{SU)j`2tiXH#1%~PUb1I(owp$4jB?I0ov9|13RpM$ -R+-r==m?OoOg`0{qt_fy1WHXeUVd4_Fz@rYz<8fjCwGij5D9zPfyPhQPRMVis2!k(7GM=!}>f;uNzTG -o}%la?Y`!7C4;h(_2Ym&JLT>2~;aw>iW?d$4gBlfZ)S9u4Cb`JVvyMRn5la%m4E-2B-_2!=&NEl0X4j -G}!WDDBH?P$o@)~bR62%!NrBIfw8_huuT9)V^LxD!zN0G`F4{)D+IxF_%zWeJuF|Dk#iOCHEM0fVdl{ -Ea%ie4t&x+R=2I$Uy{Z#NWU9S^&aZNN9Tm0N8t!Kne!m8gX|3fj5?0u5tgSYK>?kcEAF91f0ZZ^VJ~~ -*i397z&;>yfss3q$O@Lz{XMh>m)pw6Gr-2;YuW&)6n##m&xqcP>TM98W28sIG{_tzTDGi)?41){UjqV -4yIs)m9tjXnu`T`vk+GJmRgs~uqEL}ZjDvejZD}8b;EgmOv>u6UBday+9oPf~%=~h{wFaN;@qir|z^jKapUbXbzH1sH~1!j@tBVJFW4N2 -h=Omw){_o}FBvF3!+3)w)E>@=6OV>ob3RJ_eo&Y_hQ1r~H9^5yY!I4UoqD33QXj`ERg#NMCY8G0c)M8 -nZ2DT{Z~y;!Y~Q_c)Z^kxNSf5dyKB5(yA1Ov4gH{8E(|JNFoYOso276?k!^c(6o}wY?82_oynlv5IPn -Ehk9iw=os*Jlo)AiNFw;-EqqVt%T&Np+{`(9037qMUy_4P>>!t&f}9X!%$VGF^;kWwvKdS|Lw{GTZTS -SN`B{JDDH2M_VK{(z^xGC4Xp}FXXm)nASBsG6wu;K9w#}*Jl5hw9C8!6#bP;I7n}+@N!W?kG0B^*+=8 -VOFkhI`&`L~dw4mgluKnKB|Zp@q&$ei>gI?2DT;@yMl -?A{5SDSM^1uCWp0@quA}>&DoaZd~5nmgG8aZ(Z+^(Kngi-licFou02YUVuY&47w(?B@nJ3v4WTjA!d@ -K=jZriSgVrU7Bq(oVtkN9P^Tf*4aU8S0LmT%9{0?=gLeioqrwMEEXtNx7)VzdO)#|9kS(GUip4`YTwDF#0yqJ~g{Cwazfw#HGiKPWbGo^1v1+ARc^d9ADk -SOmNnBgW_!aFYp-rF6&Y$O<=zzXYLhcs8%DS}*EintH=4IM>$s=vgik#t{ -%|k_>P1C3f>P%?f!t==lb2f{mAf5e(UVSy6SvB=ixQ_07$99Mu_Ai$_$ZXgBp%jGAgH9`#j6!!0wMPF -B3>v^nqjLoPDhg@{Y?Yg(bOJBOWTxq!j=BuJra0FIw*tWr^VVk^ha3vs7Hx`e1>>@3IC@v2=w8Z$BYV -MEZXi6FuPJ_Aad%NB`;pNG6%C$e5TvRdcHH~AgS9DAgu3P8Iit^S^@M}?J@&TpIaY`zuOK1Znw=qT*^ -|o5!Gbtu647*aiH>&3T}AG0-~hvlUsZE|@8tTr6XVUyUqE4dyRlYyWH8=Ou ->)hvdTS6X<*G#dkx1R5 -7;aH+{BZ;usFk-8GN;?lL(qqB{Vk)ktXmfDWGf#HMEeDzZ|b$TuO&t9%yGA5$c}Q067EDf&~XVrO;%N -p7Af2%1_5WdvVq|PUY3rhQR5j~ADrG15sg;9j{evlCzjhITMPFRQ*&Gcs=pnFG@+f_R>-0m-(QTl7_A -@azLxAYIQV$zP^dDe=1v@94E9Zsmh}m}jKz0J7NXbO@FHN*vUoZd+SIZ#SQPjsBe~+#ed|VI1!Q&*l! -MS}76jbL*>X_w4hA2&0EEHniFVse`22>NVl04T4HX%7Q*5kxCOPoFnkdJoOfCQ|q97D^0VuA;aMb~XW%im28Th3E@A|7Jnt={uocCdFyL~W14q^<(>-_Ep2vOT -Za|zk5vie(Qj7vE;Mux`#4t`e#Nwarw{@$d`}D17Eanwf*PLgQi?~(`1l^covTNrVeE)6p+)^y;=w374rL=Gqp_t^Ix6 -q*wo-RdDgN1uf8jL|4`4eEx6Ncn;@XH84S!gB;CflBT0K@jqxXBc(R -@A`>x@i5rcV))2Mrrvd(o#!k-NOxn$Yrng?Vu*dVNc87_`Ufq -%Zvsp-YpA`W!cE{CMwcX<&c(YmTWsR-9o>VUQtQObC6Z;p&nuov25uQvv#cA(WNykY1y!5_;McH#mUG -aS?t|SPKJa~#~gD?%g7#l3}^K))Jav1_OIJs%Qd#3BcUhi-zQ`SwqDQ4VT{1eZ)Gd(?*dF -lk|6t!G-<*i-3ra4KY;V>Vcn)Lw<0WaGLJHV?s`t8FQe#PLE-Qy1HS2IObh3RBe53UebQgCN9v0epe; -JWSslT)%?doF}32kH~_08r056I^hQ=XYq|DR){-+bSgN^#M)klNoALfzEaz5}&ZdBnMzC-4 -3CVnBzFRhz*H?Kl?kKUHxa>3qYNMd4l_Pz#Jtdj4xh4&)*#xMr@XdjS4fic0OKe`=E$X*zKKLhW^g@^ -qZ^wQpL@DP@6nON(FUG=XX9=kdg@5}Bgx`sXcHM*=YE|c;1LIx5V&r3Lp!@!7S_m;{ZBI90g84&SJI} -|70ON0Ruk3}H0*Af -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 -s?@SS!gastA8XhkDQaKl)+Kil -UWB05N>-VdZe(bB6r2DR5_ftUrylyz$cPh4hwWq)@vlx)`FZ;Be7fso23ekHMtH$nHu^WxV_Q4?h3S| -5gj%U(?&G-bzNBb%I^B@damX7P98F&(>*Bnm}pMGg=|~~-fO|-#L@%<=X$`Ipdh;7^^bE0 -28B{uAn`~!!(!H(h3p$Y!~s%_2$8Vn;Qm!9jBqZBI2mEdV?K&WmhO3sN1R#kpyccD| -D>kOXPNMbY!HYd`^tzj%M!)lDS&S>^SC&XFZa2Zmpo8d -MXE4A(0gMI_}0WH%+g18j?%%?HtWZ?Dm_bLeC=QZ{`pc0liD^$|>SD^r0O2t)fz%wsQZ-8aPB%Rd2R-?PdqtpX{!d+e@8=-_4#lrfuT!+C -SBE%LErfz%Tr4W=!F{~b>IQj#9K$s0^-t -MT^WKt2=|B<&%@1@X>oxlPFJ<72!-wkx}d63Jqd{(4RVwt7f}6lG%JwbtXOBYegzE1U5YrB^*!vx6{W9 -{?rGOlN>#aDT*y#Au4^3O4E`r!jWzVM%^|y8DH0#zReZ4tMVz58iS-@NyY6WDI4Iivl6MW`=(qpui50 -*X|aTx0ZGtU&!wD6zRbbyxkkX(muuh8D>`9$1qfkd{5bsI)~n;@On<_7>V<1eBx(;-`CDG4amJ-hykK -xeb=~)eGW!FV-W`uAT;lMDX>UeI{gtB;h(egTo(4X*umq;iAC^N*f1$87Rje!$H0Nnvob$1AvT=5Rv+ -;KOHytPiL3ufv3a`SJF1(;lSL_p34Q(2v)ka2Vlf0Idxt6JDqMMUyw%G4NN6u#qOA>Kza_G&Jw)jQUZR_|tJ -rm{ZlNZE^XHf;9~Zoi{QOzL!>#kf2zz!WpH4)hJGsJsAWGLV#lcSrn0VQNwgs(Oug6ztLN9YBDhEIsR -sw@tW}#x`1P@dfN1GQ4eLj_cm?qSNqg=nck&eoI*m229SnKtB!Oj`$fk-%!y+l#N1CqYnp15EMwdlyn -6niqwSQC|bp?U-UxOD-7IlGDdgU(;a%g#WpiijT9;+Q^0!H#dCcL5{-aV`!`obD|H^m@TGGHMY;P?32 -<~LS7+j1mp3>qKou##M4|nUg$Mu?=4P`(y4;lYRcbcu946KC8u+Pe_5jO-u>#al7QU^}OVnos2agOPO -ia*Vps#4I>ls1^Ru`)lM$l0@j(P*DO#4Yt>~}WA(JtpOf)Y8c@u#m*3~CcSfx`GdK(F%*6=@inlwR+a -CDIrESxL}7DORVs372u)ABAC}WoiNs&zwJ#oc3Ey*8a!419g7-Pe&g`E2u27C=B`_+MpeUL -c9lnvq#p4HU+Zh_j -ttO9lcgYL3j^`mqGA=BKArbJ2AlrA`@zMwC{nvB`OHBz%xjLp!xM-~V)buM2>2@wO#w*IOsLUN4$GIdG@ie$Dd9e{Lt%Qq;Ibys3Z43RUx*mu|(Oxg4 -s) -6_nzVc6&n7e=u3;M}ERbV!cr+p&ZOTjFU6Xw2@yD|Ug;UiL5W^K5c%fTtd{1!OTvbcfbdY9G{d@quto -_ZbIKPyTau=-C3iV7b?-GKr$tDU-9lH#i_d*P*@02+#CEip?%1UxxhB?~|nM~df8lQY4AbF<%EHCJLp3Z|FwYr8=ryVtCV-{Jm -e?MP3e4?*3{1r1K9<^=LCelKQZgGX8fw}csLpzk}_OS5ojqLvCeYB!LEK7kb@nWnd2Zj&A -i5r-~m^pau|9)tcQQ_OcihQvc#Mm96;y9x2f*AFw9d_5KKMgj)O#3f(-^omvB;Z^Ycei --Jy_+0>jkG3hI2JeuW^kEn4p&@I=c!lb&8!~*}cuz|%H+A*PLFz2khQf*PJHap@XQ*JC`XPIKLj2 -%0M?$m>NFZDfzNK$aJUy&JS2t{(!Fg0+&^G(zsv%(wK`#;#luql1jP3exVLz{9DzC2J|MOQX;T$XztZ -m2^f<@SYN)JY=UHVJOK6o93(SiRdXKz_xoF2dyI5~!aCsjXk9aHZ?Sbb6ZS7$sT=7{U6j6V14t8Fgos -*NVYaN_s+EObw}#0N|j_GWyk#f3XceH*18`5jn1#*~z47l&y#`jh;m9Kdo>@C=^tBYsm)$hiaV(49a&go-7R9|l(e_Gks@ngTqGEd8K1u1s10yQPI!~5j{gnJBP7?|XYNj)U!tC -0fZ0%wCGPqFWM5bqfQz4{KUZv*OlUV=v5|KW8iq6oku=sqQ}B?fRL(dC5Xo@i2w=< -j|dNw?J&;h(c%qAr5JQWIsVG%&x|i;h2`%1-LGbZH(}VlJUxvsXtuL3PV49IAgx1sHx*GHN)&BK$LTlBk9LckCETNa -^vzEV8dYYsm=bQ6Zm~6N+4O0rW;IPRP)3VvPPA^S&w}xB4o1v(`3`NzI(mwNR{lsV`b$#C|611CNzCd -JR?psQhwU?SOsBu3jC;Gv;4k@o@l93P&mM7rGKXGuNUSuB=n`leeC_I$*5{5mKEenp6J#0h$}IgIx|i#gp#O7r=ATPAbm$X=Xf#qD*1O -wH(C7pBZ9h>8f5NIHFL3t8X2*-Xx*!Q7!lVe7qd! -NF2P|oE?`z-bpi^Fi;&>iPG=CYK>G)C6k|%3Nr{QXtHU$ln1u(_PnOo^=x}KxI>T<6Rh#vzPu(=X-HI -4Vi5+o0+goaDuRin2lSn(Eaqi98M2}9=JdJMnNwLXKserPA&V@VQ)rxG19(G-UdCv3PFEI_33ON#n{Y -;j`pDNNTp6&(NFIyw2F<|)r7Ms?o2B%$V%Yv3{5FwxNoeqXSA5j7?S{eL>(TJw0mDFC_ZkG(&9Bx&n)u{^hjpr -C%ck3brp{$3(qMI!CLlS?{FE0~pL+E1(PILAffywY1VmAU6cLm@?c|(GkW;u -bpxbJ3?N14diG6fN1Ntfn)H|fRL -M(Njzp_$-5@bIiq8L=Nb%$J+8s&hiV(&u7`E|VvZz-0aFD2CjakRPwXKw~<{5-lnl3iZyKgv^YPaw06 -AvL8Ie@~5;-K9&>?!aoKT2%mm7$(YO?VKFeh}W(8w%Fr%a~6i&y{m_$-c|y^eeajOLYKJWgQHZld1sd@L8VJcx_}U=?9D{-5(>5Bc(CbM -uhn-U0q>hYt&mxnPuS@94)fv5>F5cOzmHi0yKV+2$ho$@2 -$Hyco_Ro@(Mx6GACxls`#ElBOn19BKEqpZZo8Mv^)bm^nRuE{B{Lt{o{(T*Zzdw#5f73~e+q -}b_BN?I%Tc`FPU>4&~GYku%fK(JQnrpIm=;SYXf3BSD&-en5yl0E}#|Co-x9505 -a&50!_{uo6~Q1c<(^fNa-K~CD6?&udbn?vbPkdwP#sKhm_Nk)uj_6b@m)5I4{QJnW4_BL5sq}bAS)9B -`Z^z9D5f|iov2_ige+yS)mx@g~1^vxqce>yZYA?@mdn@`S-E-Ef&h*WxM>C=zF8^gv}H?arTiS2nGxR -0XCO3)}GH|R5!kW2F6fXrP-@CQE@Jn+q2T8~H3m0!Hqt4Gd&r8pR=5XtkdIk%CofqQ|Di9H4$__AX7w9$c&egr@!Tvf&#-j{*-i@*fQ+_kX?JP_aLF5i -CnjmGg?`!P2$F)b%p@kZ{Z!n-Hof2%h4!N(JNc^ommaAqo+IF(Y_x?_Is*owdQwrV6x`&y#kpKd1+G% -gQdY6rkvuCSN}Z%@P;Kehvy5D*M0mZF97T86O;Z$?WqW=S5P{>3aj{d-3|_rG$7(#GtJ9hhl!vz+;YZ)4$+jQa<5Z?KedVFG0uTC-dpT9d!OrZ!UjtEog{ -vSVsg&THCnRcGBh!CGi|WX$n(_)6(&CKe^5s`pSw%4;p3GD~Kx?z$3d_Cv@)4$%@n2Q@j5KDTJU=HR# -Nr5hTE7xU^BeF-6A)zmprojIxIN(*G`$$JCsgX2WL4f#Fk=X3fCQl2ImyT(u77^f)i$~~?!_Dwje*c(oNbGI%bpZWUbL1^gmBmagOa=+RJrfkMuzQWFeJ -yUi4aJwbX2M8SiTL`qv*sc!JX>N37a&BR4FKKRMWq2=hZ*_8GWpgfYdF@zjZ`(Ey{;pp^co-@dvbN9OT&(C8%z)PwS+*~M -AO?mnP2AA{TjTc`a{V7>goN!iR -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 -bI~)m+2#~Z7>q?b*F!;z4a|x?BC6Gq -c+Vuld^GRqIT5J-#YY{3m1pMAi0H7Qiyhqnj`+REq4^LKEn&(S&#Mq}>SMg>U?r5!!*u+dh7b}48dUc -yZ!|Bt>SdZ+NF#)hg#=3`oGm~iDprp)zkjq+RjiRpA45_s^Ci>(dK<_wtUn)%YW+FsnRd%TIcX+V-3R -=p!EM&7`<28+n-YRC20JWQ%M(u7w>EiNOOT=uo=`RGc#XV6T-5l*X2~X6~WxpX@Xmxg{YE#wfQRc$Th -WaKmY2YkbBf?0CPZfSzqz8Ue^*~*&eVqpj5pxo3+ak{q^PS}kQujs5DT&^;jzuYx -nE*efW?Ck1h!N9V{cLSYwdX%mwKV+lrzV1aIq9#b{A2fu&Jxu$e{$MJoF9_dzkM17)Fq~Wa44^(F{oS -6PvK+l1Ry6bJ*y?^7m>?z$ed>WcOD_AXCLH`xAU=6Lmkn7nc6VI4GK{fFXpnkhJfpiCt{)Ny_%>nJvS -P?Vl>qxq&cuhAu1GTtw7zpk1X;ldrJSnD&gBe~y<_t?<|H7-Ud -mO5aWRJ^V)$_KpQE;$5LSb}IjM>0iH?S_09vE -F)WIJm2r8Z{72g8*`U`a%ro~Nwj-KG(6PcUUpxGo&;VFjNoXsWr62$JQYJ6OW@y!(~&;JF^0h4g$H`v -b%Ovjz;1~6)K-O{2_mK^U#2gXCsT^P*#tB7y+=RHb)S77FlM|HdTcvfC!_xWP)h>@6aWAK2mt$eR#UU -|S?w1C001in0018V003}la4%nJZggdGZeeUMY;R*>bZKvHb1!0Hb7d}Yd3{vdjuSZ$eebU*^*qoGGtR -E|VWp8kj39^+f}qSGSRvGMx~mft$8Oo~OtOf7-{ZdIvSR3m#Fwg5r%qK_Z5rzW)vYOmwc01DKd^_DSA -h@fy+2u&*3@b_9)x|P%rZ%8XE)TSMij~fc1CK^2Bkd>cCu+I<@}>|Vyj$erDn4oU0iDnu#47I?26-kl -3dVD(`Z%7psbG>E}v>q6xELU7$pQWIX<`L=5?U(mcsw{i+1T@Ri -wg$>rN889nE3@W`f~BRl3#mI{t{Pdc ->8@-OjsN^r*F&p%Re7RU8*(jPy?kDsZ4EE^fJr$|T)u~5g_o+ja4Su~Dowfi4;RXWSV$}ZTl2VvKyXe5$^U-~%=rygGn+QhAQ29`GB{lYeG8IU9EKzL=9D7Z=J+3u4VP_g%PnOdwN}&&|u~4j-JuUgdQs~O4&Huu+w{R^j2> -NW*5|p?d^ucDhAhT%`}PHO@f^?U1Lrb%1nefZMacix$xb%9L0~VcBst-eS&%rKaO{t#c>p4vmv05!Qt -m2>}BkK&|%{-dLGv@MBxXZt<)_?b7fReQY#Ok$EAK^XgxI{=pi~yUA0&uzpw64h<4JsGq$B{H{-RjR)EJbJQ=3t;DPN@t^y*v -k!0PH}9vPXY+Syo+tkSP)h>@6aWAK2mt$eR#Oaf8puKd004Xj001HY003}la4%nJZggdGZeeUMZDn*} -WMOn+FJE72ZfSI1UoLQYt(3`*+b|4+@sVX^iAt3vyoX%|{99cMueRrSB0q<(Oy+Q6HHR)@n`cquS1TQ7`N3)js -39={shvkH2qMC$(;60s1Byq|bmZ9!u_n8LS{Tk6m)OvG%d-G@ehztTzdi6h=6(E0*crg<;<@&i(# -HOn42)G_j|NJh+~OY$S2FPTZpCR#k6+3c~%Xfn9kmsvYGMFHfzBMkCHSp&Oi!z-m5u!{=_#CQBg;sCG -ufMa6AOv0Xap4tV;5Jr~goTErH}ks;M@3NfUYv80h#3iI{~u4AZx>DwYu8!h=%rN)j5F3T=x({Ibo6} -(UPOf=fOa~Dm4jy%@r@h)OW{a%lFAu=MKeR+<8^@G%n;X7ALOTzPqZn7?VwAkI0|HIf0mHfxby)}P01 -;WByUSjcTglSb54AX@XViX<$U6a{l{{c`-0|XQR000O8`*~JVE`z@J2MhoJUn>9r9smFUaA|NaUukZ1 -WpZv|Y%gtPbYWy+bYU-IVRL0JaCy}lZFAeU`MZ7vPF=3lPNuW1+imV;n-?po*ZAceC%rZ2Q4k49s40R -2Ks##M{q{W%K#%}MI(FRjZo0%0N#J>Z2l=KHQWLhJJdZa_DY{}KZg2jeMB#qtVzuJM%3aE4(T}`b@|= -y9Qf!FcmavG_kD?KQ&+*yRd|&?Se3m@_X7>EsI&D$hf5GHAN<3$^KJu$u_Le$`O0f-n(n7Dp|^xoXKY6adAU)p1pPtr -lk4#n%xPj(mZd#oC>)CI6t!Nly2Bv+in0kFzGhFVk??%-`bl~QvdMz=LLukBouE%#&Q*Bz-OwHO6H-a -j*c&8&o7fV7q60&+2!HU;pJg+dU!Ikl07oJ-pIUj`4+jSsDqi5sGdn5YQt-sicLwi!>V+a^F>l#uM{C -)kjt~9vjcJ>ZWzhfk{29dk~Nbo9)`$Pt0d>ewS(!d6Tq`ha9g6b%3KWt0ZBA^fZvH0cEgA-L$f_FtoN -AuxfEKYA|HW#nO)^>k!M6KegIjGMn_<@k}oP`tkx4eH75J6UyWCz5qchzu&#E&c*XQ9b1zDg0&FEoz} -iP?PL(46po+~3Ew|?ac#Iqk6HC(HODRew_10X0kO}UO6w8+rviDa|h_nmwBPmx5%u*k3%G~$`%Z!F2K --Uiq9HIIJ!L6jI%1;633Bwn8R~(2VNhB*}k_@3?x{?K1jz8e@SkhY~@}G&lu3hnG64~oqq{b6|o{UEx -3IO?74C5goIg~AU7ZBe8o~stocs%|Nor&l*IG86E!WBTF8Ow}i$D?M;A(Dz#ZWN2_3ZY5CQp^*Q=UYM -v27mA{&A_$5XQo8)A`KC!24G4HvS2Efwy -6_P`wPLl2I@T!i&8P+X%V!*g6u14EdZm1CR&gZHZyr+r7u|yI)e)Y4+7D*j1>(yt_dwND_Ed^hrQTW>0|thWn5!^P -2dRzsN^BGbO}_|`x+Z0OCcMmp$T~6zpTY&`NAv$>LItrHp^`dGuC3069A{cKgzh=S}RRasjUFe90&yI -J_|q<^FnC~H3r&2OBklXej&OfWHjpJg}|J~z~ob*G<<TB=IPSl9R5)f}Ohpa{7f7+GD&JjucA;I3K`ZM6~dX40^!XPq -fy$R0sR}D+nFc0?V9zuw=Rq#l}8Ys$y#YcTa!+)gLB9DhsklH#eY_5BV@JmLh4q=su47%JD0#-^A~L$ -)8X~S9=Jp^-0fXU-J#mskC-KpF2wD^C?EXk0pr)Oea~i5&aV*`3p2v9o -Evh`jETqJS!7XTN9JricGa%_hlg9TyH2HY%gG@d#Z&}5r+s~g{8Zjhci5a#zkE=GVK3dp4<@KW -ww6U|tubTcR^sci7cCetd=6T&Npf*EEH`^|Zo2%h$wx;M*WbOrX?9I_@G=<)aP8(R@5CCC!CEAj0RQp -v7EkhVCAq;8|lFMD-gju55dmRmRiC%7bpR!Vu7hq%{?-!yftfWH(esCM4qF&SpGd9TpS&C``chE)w0B -EjK2oFaL|JDtv{~STZ|9pLRI=EuE(RC{(mNx5^uFCiom-PZ#SCU0TUW_nuGwp9g9hS4hQ91- -5EDD_Vd|4XBe2$O5z5r``v<72U7bNRxPaAlOXref#Q=BwR?B!nJ@5 -+102j$?;tAk1zAfiw?;5}TQZWaS$fUn*S2V^XJ(Xq1JN03cRNfD8)!V00KNh$0D5EypX`)K-6M;h`lX -Ij0CaqA6A^hLO(E(jr)vUOyWCX;)j(?-b&fhEthWFekg0P3c-jf?87o6-)Tb^fNrrig2k2wJPQ*Sw;kJnhxM8J{SzYuh8v!-E -V1(H$&EXqe`Zz^w-d)azlLPaEk3np@P8iB1ZOEXT#5o0SndJHA;hBgwu@RA`vZ2+wV{lV0xeaWRAQsB -fsUgGk+rQRThc1mQJO0w_i+dL_#wqe9|F@i;b1@^j;WaufL}DJ9nVvZlCK_(-yGdAo%qt|Ek7IN) -z072ITY{DZzTUd&jup{-NpD(t}SOw&Gdck(gA~XA+ZcW<5~H%r2oOI%-Z(az<|uxX2;Qb|>=n6x1#lOLUiEOWY2&wzyEm#1eJv*(AeXLrpFS!72k -rYnu4wb#7-`q?BF(FeHK$8z0A}+%MErv0%3FmKoxufR=;=nDtIjaRq$Wyo)7~uakpT{%(Wje0I?ym77 -bL1HgvpfP1DE*xgANsn*R_*rB_$?I$mJM>pmZ?}xEWHn_u?)QRs&G$@VW#<7W~7FFap?e2Q8U>9hgi_ -x3ULEo&xdkq=U0Zlfb`~Kb|Rmi7%en*L>WH9T;6L*8*(B_L1$L`g?Z<0(V0)+|-&b2<&4cE8fi2`x1- -adV01H1|ftHWj0kI9o)3(b5#GZM>r@so!~QtC9Lf{5>ym=J~MKlwLH4qLvKvH+V$}--WARAex3`)n{t{R3bP^Hm -{Ac!JX>N37a&BR4FKuOXVPs)+VJ~oNXJ2w< -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 -gM#}ubPm2RMa$=Lm7u2)tvXY+J==T&Nt;+cB& -GTsf9{!*-lPoV#7SfT&B=MXp!3CrWJuxR!_$MBd|%!xO42eQS(1gO9KQH000080Q-4XQ)1|iV`u{a0N -4ot044wc0B~t=FJEbHbY*gGVQepLZ)9a`b1!CZa&2LBUt@1>baHQOE^v9RRZDN&HW0q=ub8qIEuvMCs -{j`Tf*S3mIpolTVGy)5>t#)m3Q2k682;}aQZG{8I6x2`7R%wxH{U#Yw59|9)JuI2vg?iYaBa<3)Su9o -@Ui#OXrW4=<8F5h%DwC>{)oCYw(3RmAnwVw8oX3)MredKS~fz-ugJCNFg2Chqb1A=zcd)}7rPIp>x6F -qwAR?&Zr3Q`99$dQ3ID-pZ;h&VKp$i5FH}8RI2sFN5;=qeX*!6$&L`QNK)^^511GpcklJ~n6t)$C>>? -WHP8shwhby>Y+VDH6g?(ZrFr(Gee7qgCTRf75)Y5ZaL`nD@s$;pF7L0ny;0-14#(8^tOJ4pl8dvSa$# -?blaQHnQ>&09iSAQVg<~E~4P!4^uPi+(Lm#tHU**=TAO7WNfxk?U_oYJJ?XG&?zBp81}TZ6g#4;vqYz -B6<6m!Lfnj`h?H7SW;{y>T8}hx2_NibX$IO)>hvyIx_3)Qasc1)a?2hWO=bWU`7b0$vN{aK)LV)Mmv! -df~mDqI%fF)fLRLi7^R77e7#IZrtzFWzBqkv+c@&o3oBzwB8xPD7FL&9g)Q!f_qJ#H9T!+vk70Zy;`O$c$cAm)fO8SD8Y`vx(q! -3t5olvt-U((+USM07vp|HtVhHp&NTUVlOiwQfwZ&3wLH+A@}q;B!cq6qKc{cWcGEBs3dHG`*Rsl%dyG -s0PD58gm4T_bD@_=xt&pTd?E4k8vbhJ>`ceFjIxVs*fwh~tP}h&q-lw(KGfBM_SLqJEh6a7V*4W4{J` -BVpld=mC*>zB^{R>c-buJOS9Qy0eFdS##|#r^=;JkwR;nDV!XjjhT~^Y+`|1Rw`rjsVd(x5WVv&M&1iZDY-|=-g~K{Rd1`6<-$1(2No&}{UnM1|4JeQ7LX$1B$LLU>{qYp-YW*gDE -=8r5M#YW{JD@D%jDJZQX^Wqv5_h4@3P|#XOX6K>1i87tAks>@9)3QWrz$L_)^A0VbWG7cpTVY_nQVQs -OIEPi&l{9C|n%?r)glBt5}X{?$w%e>h9n$kwt?u7` -TxJp_xevY%#MtI=k?j$Q*QlqPIP@6aWAK2mt$eR#RFqbX4mM003Dg000~ -S003}la4%nJZggdGZeeUMZEs{{Y;!McX>MySaCyC2ZFAhV5&nL^0;Q)8*`63njyuV)tE>Os-33U3yca89qDf>D1QrkWeHY4Cl~jh!>M}J_7J8K1fkMvbc{y)hW&5F$I) -A9RSu_JmG*7iEiu^9FxYA-YQ*y=3rUD|&eZT(K#aVLt`t0{WGPm86(@OVgn}7yZSq3Lv%Cp -t6Zne+;GI;M~RH5CgHVNB)BXA7L4hOu8Q3MPzqDX)cYnL%y<+X*i2@kXrcWiHh<$=i%f4|%z -NEud&uqNoxh2^mTVpGkb9eAbu9guv|}nwOb)h*Mb<0+d9x?7IH>?FFBU(Fm1AC^l`hCY}rPmd3&)DM7 -SJ5{*WqEzX)+|#S$6MR}_uL7i&7F=skpK -=-Q;!b3b69mDfsFcv6G@5Zn%9*hOmf53ncEQsn9D}{VV}7BThVvDIy}x`5i$_EyNpvBNP7*+TLnA~xv -DA>a!Ppxfyr*&{G^v3H`S2VhCHF$-DxpEkT)0$C-v?te&mx)2G1r>%k2?(40^nTZ)-%}Y?dBy5*y -QOr6b0B*vxQQb&~Z_*my4H?}H&SOC%>EQMj7;r92(JdYv!=nIex2N}x_PNR1$DOb{@KP}GTGei1#E!n -Db(4-yAh+gu0xS8-o*I5JIb&ua0JfI3N(J~c)borP$-;#K{<#UbYc!&}EfU_)?TG?d{6Tim>&d9ZP5ILH+qk@$y8H}B|z=6&dqUkSrAZaACeqR7VJZ$z5Ia|W2%#n -}aW`PHkKT`zLYR(!+mgg4`E(<8+h5;|&N9a&h68-R6J8-WxN5Hi3X+3@+8aowYm37x|H4#2AyFSm|9& -tF$n&{1^2wDMWY%4@x(3(I$jVQ=W5`& -95B+k6(ul&f*aj*OpDo@+1^t!pxq-?%7!thUX_yfd4^{+h*vLUU1TUzq$gID*)+Pwu0)?zB#CcMKtmN -IJ07JC~tKkXGBkq -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 -ZX*j5);)(!=wt+S1%FS0y_}k%YNcji -*Z4>arv$UrtqBwyBud+WLFKsZm8+kY|df?zd+I1%W)kR5699M|@PxtWi?Jp3PaH^n-$e*aa3Pn?`kVR -O7`I*!H%En84ph6=hT1h$p!kU0Hqo9Qtd#Dwqz{MoTd0H8qZ{2~VwLn_uck4_as5}v -o4wQ>d-YqOo3a;B>>pNsMP^CbodR#DS|tE^(_spm`HklC#X?!N1b|-&qRpoiFsfQt$LE=Si%v`p?rFq -lBJ-_F*c!JX>N37a&BR4FKusRWo&aVcW7m0Y%Xwl#aC;K+eQ@q?q4w|g~+W -fC#8X6y1S%w1A)>m+mL?P-H5StZBHX>#LU>cPMZJTb7!oVt!&y-s9`~x` -M75KC2%kYz3^qjxGNNV1Hq6;2DcCb5%7>8%x&G_cZ3J{*;DLwJx*tmTfxhGePARWgdAXrwlkHcl8CEs -fp?x33@)htr=_<|g8`BO@0DO~lX2x=~Q9RF$MVDsyd;N@nlnid{OnvbFIWx$5ew5^hRaB=nF`<&-WP& -DZ5l7ApUpBYcGb8SiK`$_k0V{ONoo@B`kL9Ud6IKleu|AEk!G8G=bNy{YgXdpZ9MwBN-SS3j`x9hvj` -`8@gNVm#$HK25&97>^sHIQD|YRu%dAZGQ7^ddKn$e_HLmL2WG;s(q;n>HJ&c>NdGw7k}O+IVhZ}a;e8 -(0nwzzN6FjE>mPsp?J}H=LuffIMGEeeOVij=v7GqO8TedSXU1ZBE`HG^%^OclOZ_}IQdtzq5W{okt@K -q>yA%pJ394eLO9L8g -dFy+3$i|Ctv8@W_;^-s|=#gPV2p%!xZu-oHkn{~ZyuL(MxVkw)aJo>X3}Yuux;3zHF`QOP>c`lb_}Hs -V%(M7mBK8yUVeGr(0ljiU3k&v*zz57nA6i`wrg8kKZSxhm6sqVOM|s-d@LliQoTu*%dj780>(}JACbY -5V=cyGQzT~B0F`JNXv8oN0Wxtl>;_L~FxhRz^BWn-=^;gNA0`y5%>!JJcI -Uxk{C^I1q3;R!yqB=1FwYLVtBI+;zn96zq0H6_Q=O-mw^}KzrZ((=6GSkwq^*%x2$CM;rsCzM{DoK`5 -`W(x8Tp;tf#vrw3);EH7>$o3M1{*I{8ZKyKkM8)$AXrd15>Pib$KPe`}Re+3?lgbW_+nU+F@WvSbgw+@g155R(xoxl%*AUS#=~R>Bz}q?0`J|(>c_lx(tBK9TpV7>XZZ^ -y=2H;_M-{h(dCGC7O`L2I_j>rG0R2JgG>nus%I)X=%Z6plr@IRq_X}lO8`oqV(anG#E8fQI$vFBa#Vf -X!cSxhw_1cXPiL3ZaZo(K$=rL1=p!l=Va0G^?O$41%dBd|&@nyIcsQen~fN4A4Cr@MVWzvQSd~tFG%b -iwv+o2^ot)kKbHDW7C8f-$G!mAX8G%IRCKI+nc+bhX{hkg~vnx?}_;1uekL?fe4);#uFAaDwrm`5?h< -Nw}refxg+H+aAMyG__NY@D910^T5<4y^xkXxG3(>td+EKm!loP7=W@f5f9l;2;dR4sD@F{_{c^ppm~a -`^+TD;w%0SjadbYEXCaCuWwQgY7ED@n}ED^@5dElSO)RLDy$DbFv;)&+7BOHxx5N=q_xGD|X3i}kpal$5 -vtP)h>@6aWAK2mt$eR#U4zE>cPj005pZ0012T003}la4%nJZggdGZeeUMZe?_LZ*prdVRdw9E^v9RT5 -XTpHWL2sU%^vwsNGZ46u3{u?E$&8X@mCKUYnvHf`uYWw5?lNR7GkXqv(IX8NLoFd3T$hfoyDv91iF8n -IW?*`;M!#81LB2Nga8w+PYG$=-Jo28~7+!t5>2|RohF}_KJ`Ds^)C{@2OMm7vcoZ|5eGh#rIN)e%d*v~Jq==1Y#O_ -+-4#PMO1|2U;>6TtK%(~j%l?wHFWOq=?Aunt-|q+9P1WV>?KE_JWhM>nP?Yz)Z^T&mg(}#Drvu+%XFO -+LzkU0I-{S+bPq0zkkeUCZd7Q%+If&jdyEt=FZJ2oAsss^?b&2dycD49nCuV%niNk?x9OiusPxyF!#; -KLH8@^sK_*C*PcVA%iFsbl!08N4-8uMi2Cvnd;PbDr;f+EZtIdrWuFM#h()a3nUiJALa7WZITpcYj7C -^{~ -%`p2O=zS-8&e$_FEWTy!p%){62f~1FCs_5?k@!;ohptjd2G0x~!+CM8#vaHSG2EsVcgN$vcJkK_p@LxYk0RYPd0y))77fsdu>v7lGf82bhGu2fs(t;*(6arC!W06u;>2_Pn?2o&0f -JVf8=T!jSnl-1Jh0f{?00PXu;CaW0|%B%R+(fjI$4&*j1fZjMf#nztB$3fC7HCZ?|3|qgfEQzS0l9qG -vz!BCz)mZ$M^_i};0~V|Dvdsj=NniW3WQA9Rc`zLPwDZ4IE|yXmmbzT!%yF^@_^iwaeao8Cl(aEkE&! -YJx4C@Y=-($jOm!ccT5qL?)zKO9&%GP(KbJbtw>KFHgyy@$+&1ji1=Z@e{W62Yh{|tX18?d$yFS*Dk$n%;qpkln|jIke6Xwl6QNM5$J3{HE!8X2y~& -h;{>v6`-XqOFfU2Sz?`e&#;s-9#4QXWu{k2`!jp!<{(C$mtO8@gkA#j@BXEML-{b-xt6<`9zxxT#*e` -%`bihJ8Ky{g=L0_ONj#(=5Rs=P7Ublo!bzikzwd+6@XU@)T2SIDUhuJ_zR`(p&I2G*8J}g$%Xa+JTRM -ZhTxUUc@q2+Le(={X_JizaZVGZ1rz1$;@@QjeLU_ysq3*xxU4WX**V7xqvscZBvGVlF+WR(OM`TjlKf -aFCiYdW-4qQrs307z=~CJ;;vz1S;pZR|@YBhXl!Y0!p(wZw@=j4dD^h~}CU`}xOj*{M?X5jBMGtt>#H -@Tv$=>s3`B;UR>b<^ulTq9`s9Uwb}kyhofEeKHZsb*S@%EMQgv60xVnsdO$Hf@!4lJ*)y9s%+aNNcbZI*<6qXT -JTk-@$OMjebZc%V1gtt#BqKxUty}5aB~k%9Y -(%yKY-OPf!+1ADMql$c35YhWgGT42LGk02gVgrRil#U7NV>nxVfH)uA>_7E!nh*XFT;Xw!&{2vg8~u^$_8I#LtA|;h+Ga&Sf_GhK)IIL#^lx5@&%3{ecwxn|O!UOl8?V!on?B?8Elp7n~undn4#?2HhgM7h8i@9`W#lBeuQB%+-&^kfwoI-ix^k7(Vl6;%F61?^D_x)B(_1IT5 -LSOU@@9lfMW75%(gAVi>7XnS*5#G~)HGB6eu0$ -XqCfd`H`GP<<1q448RsbtkjAKKsO#nY;fZFxwBdVB>0|uV@T6Z;YH&%}oYg#N>Va9UVu?)#0^i@8bUE ->f&V#ix>Q4r>Vt9d^6dwUd61w@eo<*N(7*+&avnz6|f4W;TYV2ol+>Ol{0Tum3~eId*n&rWTb3QmE=07*PZ{w`ww{F<@`!V_P3XFX2ebsHmYp7Pq+6N6(z?CKdBjixZD~l2K${(_F3d -H1I|@gU>b;vAZ%WbY)`P(1QZcPIZ3JJ2U$>34(8w?$8Jj7~~G)1*WN?Yx$^X7`bgc&y6}y$a-W?9njvJRxKuk!$5>I_Nl2)xS<{5-H-Pmu(zj#zMpo3&9ia59x@GiM5}(H -g||y(V+Dog4QYIm@VLgV^TCPgoSjCnEobb`|Oo=E~MFsq&KDGk46H&(ZKzCHpdt5*vGEY5R*% -k8a&@tVS31`6Oo@U899m~62nWSz^fr@ABDKXodcY2Ak_c;#pgiN9G=J99r>Y^3TiZ*vCi -BefU{Z&W_}6tH#6gb>E;=Prm8k+9;J(?G>HGS=6NMAz_4#|FonnT!M@CSK?y^{kyHY1jiWEZ+J_Kru6 -u6;*OJiXkLcI?>}Lc3$m(xqq2ISGi<&69W4d@kM>>tU{i_>&A@+ny^ohj_)^9jTU;ltFN!uVi|NMb=f -B=LJtitmJeE(z-{zZc_~{*1MMHc-u}o672ma -EVPpn$5krtE}wmO3nIwbyX*pgvp&1sAS{7jAwmWZ@i9^kTYpt0XPiKoyP{6&*gqHoKZ+sjnCPgQ3VZw)fHmL{*`9{ZS4k!^&-w6Y5#fmRaTfH=CEE)2H<>Vk>o04)w=`4yXX2wdCZPp9F@ -br&cw&*1@N3qN!7^!F-H;=J__gKhCO%h)Ht|CuW&_oMp+^q9^hxAR;txrPGvx<7SU&q-@_$R#h-N9G8 -$}haLlPYbAOJKSrz_mkw!?k`Gw``RIl5jtPNhfZcC&vXmnRT3+ScPpG}c`>7mjvHukaA2^;l%m?DVl% -xo}SQkceUhEw)go(x5())5+d6?3F#U>bR@_e9Hj_^xSkxd@2yrh`Np<7Qt#GpDY!rYm1)LLE5S8qVdD -AIv19M5*J;^u&8O=ve!+FU`C3NuHrCL(Bw&hvoTS}5~A7jQLKemVM@^_VCxzCzGrfR;!eu_)HxROU_J -4%G!${V+OQ2aLdSI^*czK*w%#KbDhxl^&&zs|nHtpez1RtlKt@8#dI(;zu;D_dCU*voyfsCHYOY^+NR -jVja!M~*B!xg9be*abBQ{dg$I`}>ta%ntBsU1BL%!b3rWGIx2v?V)8U{054s%%SMhA#M6#Hb15XT|`Hw)SRp_^Hn83ZQ!E?*uMxo8lM2iFu|Xod&xnCHh$5L&9`|Fh&| -3jBokvPfvQdq56fTxI}3Xggdzzt`@ArPmRC&()cB*6jYp16VbflQ|r_x$Zb0mHZ4<6R;Co_y?H6ic!JX>N37a&BR4FK%UYcW-iQFJob2Xk{*NdA(IlZ`3dlz2{eq!hr-*-? -*$)#D}Qh0*LljRgsfS8n=#ZY-gAK_lzBHlD1Uh0=eYFGjHb2dv@|X|9o}z9a`QF2x2r6M^v7Bp%u&WJ -kK)vsI3R>vfklS~=V{Cy1M+hS}tWbgU;(cXS@dQxfm)cuOOz{b=g -2T!BR+>D%)tqj2phAC;Rd)}63fmUY;JaCJzmBb&meo_0%jC7iv5Mr*mtfG&%EvI=gIMDuouuOMGh@(M -1vq*N;L?vY%fq??SPM2FoRU!%3!x+0c3iFf@~E4-PDk}-tPVMe!Yl`HlA>S24%`KBRJ@Vcu~XrA_x3n<60V~_*~K^f)uQq6?ebF~utpe8rLwG*bcajc< -6@3tF;}^JP~klUF`=R5F0nccq8&&daD)I}!{iWfAE*YtfxhRKlmJo_TbkJsMt+QNN_%K9O-c|U&=w9> -_&{#u79WBnHF-w(x-J~bzFGly;Jl5x1UXiQL=5e=Ftr(KhA8qvWVmfBh?05=AuL~z$P8$Uk}VyS>k{? -CR*>@*aML*PPcAat52=5}M_gN{``Y;nxQOS<)14f5+l<8)!C2j^3L2}Y+ra~EEDd_H8?;&Ep2pko_Wr --qI&iA@_=I~L}}?zB+F!$DW@Dgu`4lxfImviG?0#OZjwZjK(^&2|I$f%=KKj};<>w -ey3Zz-P~c6>y2w22~Mo$Yr+4%dm<2hjAO0J)OP0D>h8r6DAexxtz!@k;EUo{!hrP=Wub7f}Vq{kzJWu -egOUC?4LuQ89~p&e#auRdjyw$8AZ=R#VS(V42k|yNSS|7{69>F#$j>P`@}B$4Nyx11QY-O00;p4c~(; -|#ZZvi1^@tn7XSbu0001RX>c!JX>N37a&BR4FK%UYcW-iQFJy0bZftL1WG--d-B@jJ+cpsXu3y2aC?I -XBkOKW=zy-E6U4j;A(6}40AP{JYwzVqW^t&q)1V=5;xgCO+XQu;vMhhx#y0J21rDTM#N*NU(dXXq}PSrmb0IcelLFi(A%ILgteFwP8xDd@U+gE2rZ4)A{V=d{!KvemVb8T%P@Ll9A6$wW3i2 -ExduV;DEfWdF~u5=2Uu3E&>j -a2u+rtDC|8^2MvbX7s`52grPBur)>IdY|IJWCMN3`Kjb2%YHWHws+L~*2ucHXz$0V6L9Og@=Kbp0qC> -pwEMUh5PRIvpq;8U^Ex-Gm}ct(Q1CLTH|dG*$V@(RqnoCAGiwN%iGhp#9fE#B -l$Kd!ymUo)HQ#EcoS#C|y0G>`SRV@-1QsRu!kmcUs6|PpQv>Q8=>&ivBweD>Ys#D6Kgc5H48_z+++kL -2q-w=Rcfo6c)Fd_crEzYdnDk$>5S>Y=EA5XM)Si -vh&PftTU?GzDi$YE``zrEM#aUoTIuek9u%j(hu_x;@VBEeU4 -K(kSa>syCu=G&hNy387$eP|y2S`h-F2`$%He3T7i;jE>tURQ?#J!lJHzf~$t!3EE@$Y3POf>Y4*a^hs -7O9FDzNX~3W3_i2;-WZmBe9g150rI&36-~0uktxnLpz+sz+C1fela&HsjIqk2>w+2Ef-J^-k-5I2NsbI`+}#ij(C=S)LKCrg-?l_j)&gmvuZCKihK&Nk4T#s;&_}-IUj(Ha^3pg6 -pD4`d-iKMHc+p4}kwTG9%-%i3(2PlY!E_aMvd9^Un8B9~MXOGYTv&q3Xe6V`wN>(4d+nIl@(B+de$xx0CsVn)KIyu&jPNprfH0gM@wy|9Pk`5k13*}1 -eo-R(er9ql`=qs=@4?@rCV*`YE|1HVvRuKs6SFtpetSxx%8I?}`S4nOqf`E8ahESKys$Mpg3P($)ikF -W;Z12jI1+#NUqNUeR^F3^WFLL&#pTlU8eF$ -2(i98S2n1T9Y&No_BdI3NV*mG>8B!7@%W2w&R+i@b<2OS#2!i+g`M|zhe`M7+q1d~-yN^K-%w|c#iy} -$bimd~DdCSYKxE23vgf^c`-PCoZjL3qB1)0<)$yK#a9(O_%Nu!G3em285Y$LTu4U>B$cfXQCqNVx3_aeI!_=Bs%TlnJW4z(<|E!9PNh}avMg5ql<6gQQM* -n6-%VmGysRmqEp-8FT~{VWGc{>`&5_D#g#fwWk{TGny$O_W)s;+m%qd<$CR#>hfP~9%3!D29j3(M_GTeZ(d>=MC0An#ha_!PZ3ibDj>-#m1x6Fqv4HnElG -Dx`JiXBSz2(d*$V|2UOlCP5;$MZ82qV$0T?rpvx&kCG5kvgzD5jwx+mw<(sO6d7QY`w&uW1FxD}>RCF -9Hw8>=>7vDj_(?F8FO$Sxz@?DOsWct-4ZJE>U%D#$yh -&5j|yK*!twx}ca4qTU63&bVgJxeY7+`H8&+qm|&nrqT8M{;{^lF -t;AIA&QGi->%LcK6_t!V9R+XvJs)##oo2`}KCgF1)W7$Mt>Kx?*vOo>VZCNSdslCXd>|4lU4zrkel7K -NJmZW$L5H=2-RDJ_w$jDE&IWX*=5>wtArEIxPcWd})d-(`usMhE6u8T&Q=!zM`x+vsBS1Eg5*CNA$;a -vv$@$lL*F8H$?6f%nWS=#`W#wP4>C*dndqJq*^E0k$&pIp-8(`8Chx@yGup(-sDnCl?syXBiTiY7%Vj -l}TrKKe8Zi3ac_NsuH-!Y(md6!~|61QLT_mS{-J5|azXbqwnykuc{&Rf@hLc2L^D;N-^m({>O(38hyK -P3L{Bj8HX1%KkGHf&dW4m$4;CQW!aGwPL@Xoedm|6WzLCf4bJi$KI#jItqmE?nqi8$}IrG3WGpa#mcbHwKwFt|BSK&0SQ_phOB()bFy$V1ocj?8Z6zmZWX^)k!3Z4?q{x!_?!? -fiVQ`V+Tv|}wXQ`;r$#qkyl8VfsRfX%_!w{$1chXl44hzx!D7hqfsDL-^&j4KAkhw2ULwz -mn&{iYUnrT|sc_a`qN`2nDI6SwH@0aKth!6|KoL?^X<rKqn4i=~~G7VLXXo -2n`Xs@%*5v;<`e0V{(!%^^*IjV%J%dDnS;r~zE1y_+KV=K`t=rvfZXQj{r -F9rPDT!U$F=QH}Nc%B#b~~Q)`3q%a%Z7vvO6kYi(y!i@`-dA6(ZE%3mFHR*s$tr~puXCIJ|lzRd -DyHw0$czp!QnhpX-Vt%uzXh$v#oq -VC{FNgs2z1<4s}aP}?Ai>0Q+n85Z&H;Lkrj0q0A0*(e1=wb!{I9#{3H*pf*;)2~Z*1%CEAEk~mp=(R( -~GTj!(6+vmR5olxY)dp36R~O?n-1f0fM}B+<&TLnCdi<53g|zm}-wt=g>=SlS-_cTOW8$WjJ=|HCDlM -RR`M;hl-sC8} -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 -Ubj3BZQ9Ry8dVHDL-eUtGyZ4g -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 -^V$lw;$U$ZudBnFY&oyscL -(OXrIE#oD(PZPY*^IxoImk~e~XW%YG0AjI_ive3HN&o1T~|eh8(5ZJrFj=|B?>A+#QLVKnOrQX(#^<+L&9d%DQDe3S -X`_+M!+0()NwggjFL>IM27l4#@>k_l$5GlGHr~|KjzfLb(&wikcO0km5j`i%uSU9vXSi+o~WaGO}fa? -Mnq_2H$fC!VE$tl7MGPAk7{K)`+6qx0Ncv**t6>t&(8qkV_&td1Rz!fEDQZ!dOF;fEY~pjyxja_f*2s -aFRXTr;Z!m0D)l-K?wY#cVNdzTB$B2k+@qgb$czY%!+7@X~bE*WGYU)Q-(B^`Mn{YP@f~qwnq~v&O>u -oKA=&t&Rbr4GD-+Xa!q%f=~$E((YWfQ+laTG8+wt_(d8=^w -Qxd>%^x;0>$eU{69oKW-)5;m*K3Vo^^(SzC@rU*J=7rO~7q5E@TC!^a-y;NUM6N+RuXbP=7iuY;e2#b -^F4KG8}<%QK)>p2W}EusaX!uB?j{@bpEV1z(K~%KX8JG(7rCSs#VDS(*pHDJ_Z^kxie5 -&!J?HAP{a6tjlpzen0fQXvLyr%q3BAHjPFzBAY@v&%Y<=n?J8_Plg%>< -pn(_n$2(NV^b}$OXH*+kDV{I>V+83rDO$|9<@rKhg(Y7AK?x@KCPV3M`oh{vIpO!onTPvf=n44vgt!S -Rcb`BYoJ8%`$&M7(_&9qB~RRI<6%K-O`q_cI^Z%=+62Q-R5MdkF^}D -#?vJkKGF54cjcz{mEajm&C|RAb=b_fh>=$Ug%$|&jG6+UZ}2qtd|xDVPay5nTjtAv)5#E@DOIP~^3=t -kv-pn8YpN0uM;!Lk6JB(ioGz{KDOg3$LwHtjJyQ%`6ZsD?bwaniRbex2TTX%bL(d~(n(r#X8W52`KYX0!oD#M*@c8WW>2V4H>(GR`Y^!_bd`DSM33 -YwC@`qBqlwG2eOBwyHJQ_=mlpyX*yp0qusa5fWh;%NX!A|8fxJelZmV8G_MKY3nbMx+@~zNZQ&xOx_A -N9cM=wmuqZ>(L^7UAtLUDK+*wXL^U5z%SkGJ@F@VIL0Eo=vhMw`lwHQ*=(NutuN7H9{LE}GNK2RwyMr -_X)?n@FlRP2Mv*!Bitn3NxhbHg!YGZPmoK|J!X -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 -!_7`&TMlC&&i_p -db_}udo-k}$2cALc1iAvpY`ZEfFyiv+aTzh5f;bq%3HysvG)-qoIL=n-fk4qL-&4jxHyIf!{x6oy;;+ -I3^K7LcyWH1XlyZ7grouvB0j$;0P-1n1B^Xa172 -{9sj$KaY;A%@vpXJvi3jrGXKVI|v7bE4k<4ZUghP3Pi}?`3o(qi)Tc{I`vBj|w?0r24O1Z;LsaJ!ze25Ey>C;{1Ppjmk`ncO -qE)WXCk=r3s#1)xV$ktL5eAILJS5d7^g@(TR(@@Fi3gcb(eF0e0q*;1z)go5CJUei+fgnVK08C&5#kO -O2w@LzuQ7qbRU%x)FR&kTzgb#G21$(t})jYP*zxU6-H{0@eK!NR)P40E_MFRzXbF{beY;|{WKTv?xJ` -ABFzGFQ!gxm8@k``pvU(IxhF0VG14*(MX0;lm$J66Ag~LBfi%i!Yk4ynz6^qWu-rf@2z`HWDNvbRhPi -aDZ{)xVWv2qO24;Il(A&i@Lp)J}kKS9Y2}djti0Io)0ss;d^U-(?B@7>Cao!^j2XCgW61djARDr!k;% -6e^@O_OVW1Nv(EajMyKbamK)ANhSMqc~SrIkgYZM=iodm<|VGA?XDGtcuTK3BCqfDWXcpw%??H*% -gP8=Wu$!#qR9y23aYQ+@#O_P4a2$@G)iXm&=HlX4u$186y_czUxbHwtzNYZ@qBE+gb8qw*<*I_z@G{RVo$ERZ5&Nd*Zu(832H -W_SZCB|Z(I+X9LVA}3M9&G`7h}t!_XKCYCv}G&0L+ud=S28>acDWdKk&R{7xq@MQA`gATt40MO{E7H= -75zO*Z18b7NJbx{IrM^KR1k9PDLK>+n7ITtxl^hYHw}780&AXucX$`u#ve6M}ad;a{zf4}4`L$h#+I9*4%;ABH~BJM>V+DJTii)IbZU>g#LWc1Z9zh2p94LM7|=+ -x3;XUJEXE{g1zU+1Qe1^$O=Z8&HGh=uQRFagQ9=>_QuJIsmm0a^kK`vVXjw8$=N&*b}E8CcylqJ`whQ -;=R)N^0xY<0J&s!-*&%klUhRPvO83!Tg -doUnh-aXpMt3ad9eZmbO1i*Ry5f=O$f%Da)K>8=i5o|ASfOYLMLdM&8I+2RW1f-C+@ps0d4FJ7pt?`f -=EqaF9jA|b3k=LYF~nb@{qDZV6j>bz`#lwrH0BA+!*XMDSOj%z*H(ZNua%MKSN~!HX7nF@-{ieYU_;A -)v93*RYRUWeTocEyC-VNpb;Vu9J(6{9tQA!3%f-%^jndSm7^C;D5kujS)HRR}dHlpRwoAgP&WeMUvOJ7 -q6ceN^5nwzVTGL+1CFe1UkbNM6R6MIB^(A~KCsKp`kt;J4ZPRzCDA2dbNE`c$Xqm9e_VI3sC{O~b|Fr -MR*!ri8#B`IgZ4*UYyq-`{r);QXBYBfx{B-Lu{p+BmQO_;V->%&bpd+By$du}OFBTd1=CA7R|k=f~_y -4gpNEdb)2D1D7~HTEH|o~?1y!4tcI&A9)lDGT`_v1DYy=%;tJstUfF12Ken*HVe#H{B*Cw_ZmY7-W|t7P|2eoGF)A?G3d;C -`Tt?dvbSo>!d5?=t)7S4#aqbg65Z+7xH3VRYQL!&zM$11!SJRb?&QgytWruwdeilYgX$Ld53L3v)qP} -+bYCnv<(2oItanb_lKpJhbiUGR9CiAiHD)c6)y#@Rkum+TmAjQW$T2>WBsSF2;FC!?tp(epO9})+6wO -K3Ix3g(l?D}eeok0G4iwHrFaVQZXpcs|hiz|zqtA)5C?zrMq|CI?#uIREfJn_Qs-*RJiV8Pgt2>S@`lbN4)MG!B%I5lhg7Y}FZGr4KtBHQnG$)4{jUh{fI|yYny`>L!@JCMH3{)A -FsZ!Oeara1Kjr>j>!Q6)`&vH;o+ffyQkVKsp+-OnE5fkC^r_|50p>WM?Wp4}276!P59V?)pjH*qci%> -O6fZ**Cbe<@{ -OKNMW7AlMO#dwoZ1G#se0jPJ5@vhDHL#9woQR!ZGca%QUUfpv9DXwoi4UC@MN25B%x$|++Y`iT>{ta7 -_Jt|pZYjQK#_p)x$|M2L@WN_`O!7e1*X!BgxD${`U?}Y$WI&=x`^2Qd(# -MwJdb_s`|@YWStbxx#v+(uY6)$4V;(dcNMW%=8;Hj -z{rn`7|Fs8mMRaDx#HX6`dd&u>@w*gDkDo*aD0Sj?(CEy!JvJ4hylcNt;rYS4TY&|IvNMv|;VqZYHO@ciS=dq&5EU(Lsz@F3wqmaUv3ThxZoP=Sg%{x+0;t|8qbj-BcK8VhLmD?f*mM`J5TPpTyJ -etItvv;os$ap5t_$>oP`|o9eqrQ`ycvUtOg#g1@0}Uw^3CVGUl*QQI+*bBZa_5NvzQd@pZ4!U{a -`Isd5pmdP3ojU`B$;X|eQI!sycgpEu(Ja0Y(##j*4OX5UbrozcbJg?^y&%da48{22f+j+mcI)O86C*T -i5v>azE5Uc#?oJs0-M@$lHnoa)%}&DzMmINK5{%v8D?e@T>%1nuZG*PbXt={@`w@Cm49$R2)#R9|pgg?(hb55gga)LBBhxfC3d~>0JIp2IKbNfyIlR88hr#9 -H51DM^Uay~W)1jS>j-ASmpP{wCsCe^z)4OkK*bHp?OCgEw5&pOcUssmH(CcyC!xH21vW|bgl;|WsDup -3m_%*YKAQDfOU6WmRgB)5y*jrxly`_W+nW#EFa(`*!Mxyr#ge-xG#dwDyI^#=ML6hzybAxu#w|E60dD -UqY-C8L*;a~aDrCGB -21Y2BKHAMnr3k|G+I#y?=4XqoIGPdsJq`_rujE=@$Qn@RP4jJDo@g^lrwUtyy|KBaDH2kh -?3y#{oduLxH#Kuf62gW=@n%##kiuG|p?CSUbao3;sF%0Zh!cL|UEN}n5AF&Wo;eT7QV~$_fq3@ZQIrc -j@xI(<&?4FKfhamg--Hh#wO9KQH000080Q-4XQvd(}00IC20000004e|g0B~t=FJEbHbY*gG -VQepNaAk5~bZKvHb1!0bX>4RKUtei%X>?y-E^v7R08mQ<1QY-O00;p4c~(c!JX>N37a&BR4FK=*Va$$67Z*FrhVs&Y3WG`)HbYWy+bYU)Vd4*F=kJ}&+z4I$Z?xD7nMe3od6e& -IIcGX^5Mb+LdY$ow+Fd)E5^Y1&@aXvQv;DQ*KH#6@&ilX?ANokc7Y|&y+iJS%Fw$#GL1&D44ErO<)0y -RUvjZzdvAq4f_g#<1Nu?emRPhQr0wAuyf(SQ8#Ngjgo9z%rF+w~a!=G^W{8H3?ElWf9Zm66RrM%QTfb -Jk-HJlj15XM42iPf6UFi82n253a>{t{4W(Q`HAbT^&7*ho0}%7XJ>Du=lm5FoV4C`3nVXlM6>7gvQOx -7BbqzUwQ6(dDrm*8rANYn&lk@6+Ca^fh??_*O0juBfw8 -gL$DE2hvydc1xn9T$hq9O&-ZoeeV%)DJ94=rt5-$uG_&+DKQ0!qHLl?)j&-jrGpbiFVZchN=U -C#H$BYSz=gaqme)J@;h2Whse9vOWejF&BPiH#EEAj^dzJ13dKE4T)qGt~g$^B}S+Kj|z~*-LeS%Kyxt -P1kZ`td_BadWy7b7)fC>^JkQb!Rw?uhd-W(54jzeJt^ChPI&|L|a0tj8uNflnfp{>UzVW$tBiy{U@oj -5%>NdFjgS^;Dvqw(7!RUit5m-MEtwbN2t(DX#@QX&Od%@s}Uvldq2K&9lW#u}b^~P)h>@6aWAK2mt$e -R#Uy^YJwR8005Z;001)p003}la4%nJZggdGZeeUMZ*XODVRUJ4ZgVeUb!lv5FKuOXVPs)+VP9orX>?& -?Y-KKRd3{vDj?*v@z2_@N;XopF4IEgcO635o-~y`9-XkZTHnZ#4mhH6k@0lcZnr>lpNo>!&dGp>ld7l -5oD3y#7_Gr+{NY;S1dum{3Jp|kP20>AXp6Y%$4I<0)Jj`*%XZ#&;K+&UfJRv_9J-GmK8d53&Y -=%*j@^#iKdgQJDz!$x%p?=h8>vOzlU5a)L(2LxY&@6)d22c}@n1>IOa~GA+Iibxm@E3;a97olVp|1A< -D%npxyS#*MC@Fp$S06TUVkHxO|<@$WtqZVQVT0o6lJZEVJ9Jr4EUM5Sl{qpK`onS7HWMfNC|hJfwf+b -AguHEa~(+V&<8SmUN)^X6uJNkL&N5v0y40D*uwq&dy$O*zcKdS1c)upjMW87rKUtGH@?$(6+1V`u(?* -CcqXGZd1n=ic9(+l73{UGxs6lRNy7rG&7<$J%&svfz!xQild+uw2dft9jeEoM27XzVA-3{%js~MN4%! -Bgu;aZ!;bDLm>CU5*{^C=`$JJEm*dpq8$;lN@Jsf%Ht$7=vl?SeB7eEc)0pi|ARh6Svq>fR -?FoK867S|M2Y*Gr%;Gtida@SZ)Fu{RL1<0|XQR000O8`*~JVr<)D?@&W(=nFjy> -F#rGnaA|NaUukZ1WpZv|Y%gzcWpZJ3X>V?GFJg6RY-BHOWprU=VRT_%Y-ML*V|gxcd4*L`Yuhjoe)q2 -s8U-5?vAqV|hp}!9)-g)oN>LT->}a(mBgyT;=zrfyv1O;p+PoN_@4Nf--FFJ5^cuX7!VA)X1}nTWnzW -`-6{(FSEi|Iq6K4in0g=jitF}W(ax9~iW|``GV|{=$N;lK1aamSd(~(~Fj4SQIYSFUopjyd6Kanx-a| -m4NCuNQ9K>Kr`s#VPON+Uft;Y<&jkHK>o_)|e2X-nzJxGS?ibsKlI+1*6~PqD$$8Y; -ERvYGzhH?7q)S4lpD6aH5ItTr9vWn4*wtOq3gL+b^_kw`xj-Q^2YA7EQk)4l#}|b0skY$a?Z8rfb$~D -G6^-|wbC*%&gE2OLvW-8Szm_dCL{R6v3|v5A~azLSO)+>wh2R&CJ*WDfdwA~V69pRT^%yItD}GFkZT- -k(K(2i`xZoD1_LeKv+}!~rdG7L&tf@D(8kYI5A4Fv3gzH*qIpe!ng!>XaBT)W5K{S@VlT8vZLmR}+7# -rHe0slEN{EtO8wkAylTwe3?_ -U;e!LqE2C@Gl+(nIju%#T;jDy>@#Wo|(_a>PVT0yrWcj3iK?@=r8FM#BEA{G*Jl?lSkJU-EdY0wv+%w --zhXPA3!Bseez2D*gU2?;u2t5UkB+K!6oG{ArVKi&>>X}P`EqD9Xjl3L2k^KWuO9KQH000080Q-4XQ{ -p@lM(64RKcW7m0Y%XwleNs(}+%OEi` -&WoSA%R4Y}$J#2SN3#B{kwG`u-C>ifMwreY$4yFIS{+MJl1nP?|SxRh9I#Sk1&B~`!j*DMtTI;z(_wMa`e)7^e^kG8mviq~OFCV4W=$A-k2z|`PM^ZZQFz%hV2Mja -VkH(-ECq+jQB9CMY@n81HSR#xbZYSovNBWw5wGc)QC`7{;Yq5juHzDL)OZH}p`>Mk(>YY@JeKr#6f{v2bv0S&^VQuDFfgw?#I5a?lF)uGBphgf!>5_Q^aZ5j4X4Fqx;W*>p41|t|*uEDgAcV!=E(2zfik#ROmTY -wgV&i}Y#VsmDE@4fsY@dZ1se7bP)h>@6aWAK2mt$eR#S(lq{Tr3004aj001 -xm003}la4%nJZggdGZeeUMZ*XODVRUJ4ZgVeUb!lv5FL!8VWo%z%WNCC^Vr*qDaCv=GO>fjN5WV|Xj3 -QM^q{V^*MOvvGC`BLvt$IZ$a*|2n*0GK4bXg((JN`)0-6p~pXJ$NaX5PH;K`Y}xsIKF(BRrYPlGdmJ+ -);ZkOx|1VxROke6x3YP@(E?54ArRLzIjIvhG~t)&KaMTdi=(%^&mUAi*JJKnhAmC6oyNO#zWK5abc+) -D{WMbc0n?ulxWHWcYSGnQ^6Q~oOMV@=pGGR&129G-Ph>L%Tlrd%d#MZG}uBla?zBB9#NFoaK|cvIRSs -dyFFNEcG0X@{pe@gui8%G0Uc|YV`ak))=rt@Nv^{|H_g}UltlOoD;`Nkq9vHyUX{GuMMt5C;R^f*tGdtR00o#!N@y)}xsNx3~i{<$d?n>D -*7enYHNoUe5>If>bW1q$&N(@gtnH0@`3Tc6s&@t>cn~q2Yv}3!Ci{IR#lV(`Nnh{lALv&LxSn9z92%p{cB|PnlVS5ofD7dp7AmaSXfet(MYC;$i5X}VCKdJUDW!Ro-uIY -oQxS;Wj%>J#l_$ykS+Wx;H}Ue~T4RKcW7m0Y+ -r0;XJKP`E^v9ZR!wi)I1s(-S8%mhKtdg1do`dQx=D%x#s+B7y`+I4OCy;rMG7Pp*9iRIJN(cGt+Xi?s -1KIK84hROyf-xMdLcDoqHQT}BkEp-%KB0tqDG32=u?j-;!QiM^oHE^nhIXB$hq{i&kD*lNL!f)+fQT)i>Bw$>$%<69ejbS9L$4rj2v_k3 -R)XLkC=A(of5X=g&Nj>=xP(F7#y$)-*+Ym6IvrSmeKggUb{c#&9qjaBrFhDor0@2amv#l)Ra0yS{n*> -D@2Hz8@5jtE)$ldx5{SAnG8F594tP<`~bWp8>Dck?KPdW1Z>jz3!Gh-+*zMRCAftXZ{p!JwGj&dG>Hh -WWMA!if0ug0stwO~Z+xMfJVCY6S+FKQ}1okqvS>;L5BI1jAz#Lf>s?*kB%nZexrwz(o#?Tbl -C$Woq&Q#@veofh&mk@ipW(dgFjxds$>M>$npHkXOyROkV9VqeoG1sx7kBRbWhcM|U`cK%4w_>{~MPk+ -cWsU(yb4G7kpF1$=V|PxE6bITAX-h_lVN-Uwro76a}pMH#$a6&DcmS1 -av=#9kaqPVgjKbhae04RwpF4)CNp2%*fa&`_y#ii6<1bPurFkVRX5BV6|9%*U^ -HMubPRVnkus2W9pRB%?>tc`@6aWAK2mt$eR#V~K6nixQ001)p001li003}l -a4%nJZggdGZeeUMZ*XODVRUJ4ZgVebZgX^DY-}%IUukY>bYEXCaCuWwQgX{LQpn9uDa}bORwzo%Ni0c -CQ7Fk*$jmD)NzBQ~%u81&NKDR7OiwM=<5E&m;sO9rO9KQH000080Q-4XQ{h^v-UR{x01^cN05bpp0B~ -t=FJEbHbY*gGVQepNaAk5~bZKvHb1!Lbb97;BY%gVGX>?&?Y-L|;WoKbyc`k5yeN)knn=lZ3_gActPD -&(>4?v<1mHJRA*GlbURrwrPh;`ug*(TTgeQkpSBzK3DfM;jdFf-1w?0}u9FMy47;@BfdWu54I;Up>0h -HA7n2U%bN&lcUN3w?nG&)AcGE?AajOb(iigPyuhs*bgW25=YcpJ6T?q;)y`)M2RTbNG#~BdQwNMAlLl -Tq=jgSBv8)`-Y=Br|z!-_@>U%kQz|A_nJm0dt1z}kQ$|GJe_;=V4J+)$($Vnez~k*t$lPSLt}S}qa4BFe -vzif`{ZCDRZF|<*Qiv8-0j(bmJB@DERXtlM?+@_k9YpFY)u~D37KEvt-aNDxzKJ>Qr)ZI$nXiML-|4>T -<1QY-O00;p4c~(=*3H8@=1poj(5C8xw0001RX>c!JX>N37a&BR4FK=*Va$$67Z*FrhX>N0LVQg$KY-M -L*V|gxcd97FLirh97{_m#{4y87k)dLK;kkSnlLIX*lKhof6>|>9zw&Y0i&JH2(-g6}1Jy}xd!0wE7E; -^U*TpY*o8>mHbWl~uunnpOO73+*Hz}f?o960-I!Sx;QCZ^9kisLwnWZP-uSflG&s(O6XHmZznSt+gSo -opaYWe)0bl>VI#?$x6- -TD73uEg^U+z^k{T)SnC2?~T#smRPGxKv!&Wh89N2_x|Y?dvJV+%*ZZPiALpk`eTt++G6AfWqSj8DQ{X -7_ilU+sXh*>3kfYZ^HW-rReoSI|j2M18-*4x -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 -ZjQwjRMzR4O*=7X;E -L1!y^wp3+Z>58m_kFadt|YrbffLWj#e?o7YsbKL17F4XS6x|Liq?y&!J94Wx?7F#}Sp*RC(5h0@U_?BU3m*!Bzd%qMEF2ShM?k&K%fpXDhSJ+DO3Q=j<|rExSb -Dj|2yCSLIrP6><$YUR$RTp~D(f;38z#Xj5?z2HQ>0CQ2&UV$yI#mZ1#z_|9YtBshTVF2Q^^Az%w5eIB -9v_j9{Ofb_oHpf=t#zBw?$yoWTSwFywXDO2b7?&`I(xNZ4gS^fQf~#IOqy63qj87do&!8h|+*`rT=hz31-%WGclNIwJ#}8p8Gm-ewT7{u}@~zx$>i20U~9V -R#J!gxOn&UJv}>!ij!?jb=F5;@}=^2Vbaw6gnDGg9h;4gEs3uJE-R)X}c$&b#+_z~sqR -mp}kl=|{V9?`J%BcsxLAfF5|gE#VfgH%O=+YVa7=YnFoB2`UiRDWC{yj`p+?BnKF~Ej#c=^cvdqPH+H -?ny!N94eA2GUUs)RIC5GY92DgNJ~?2*9hiVp#2{uy^igUo;f4Y;qNq0yL!p3%w?T?P)PgT79}Nl&Wmm -T&G+BI)QgG3bN!A#!V=_}jtN!k3rMS)V6gMd7(33p_u_kNj_u!vX@rXj^lhPltpE~`Fg*`D4!V>^u=J -OY4#|woLDuEyt01R>`Ah2X1guot}CQ1oS^ylAyo;xBzkXV>E$(E!U95wy6f~5rF4#oz^y#n&*nd}o$; -0WqYPL1%l95JR9?k&tgSgL9RmPbbJkgFv{n+87>vhsVW6Z0`f?M>m-G=|NqE^itpll)lN0QLDGw(@4a -M>ZsBOFt0ec>`duJvU=G4m<+KZ8R}HFPbgUo{55QNN4K==n5^#&H%>1Cft$|!G+k?k$Qzt@Ff5)L~Y? -x-XvE{0osAltY&i)mM+?7WD=kVwqP$cGU~AzEJM5X!9k02QsM-nM-J1%cnfLCRS6=~RArG;;382RLW3 -Excka$gfjOwv%j1{o!5<$|Z~Xq{{XdFz2;!DEvq<4iM+hTr2n@!^ctW(?*Ihxj=A2p~G8hm?frEcBii -nXka8&sh+Sq7cfqG(&GH|`A+O518h9Hl_T4941xdTO0)qcO+1yIeayzeU*fttRml+#4YLjb(x_8#&{>?Etm*skpf8!sZefkCu3($EtAz1nr}r4607yWfi$~p+p1X{9$t -VR>y{Dl;IP_O$zhdMhgs8fx;gyAR}a5(HHVl#R!@aaq*$Wx92caO!GX=mCMc_@?OCgc4i^<99ojNpH` -Fck;?aHVmh=sb=EXmpwgw=80btGxH=tW<40v8+{C?p(z!+Kv5vNi5K^JDLaXK2q#gA(5s~3g(AuCdtA -()>fHMxW}04l(ACV_1b9QmG0otMn%G+TqQp+(9VTwY#at_$NKSG~~=ef^!x7~-^FrZtShaX{E^P2|UG -e{?j}#hKn@&)G11Phn{T2zpyWdo-TyIjyBBg;1@d13NqHsE2;g855O*0KddGZQu%H&nQf^{aXuDcCS3 -O@5P|ER{a}E^_U*wLb)k|{<}1`c@FfkMsF{4focTb+fn#Uo-M`m0?~8!%i&(S@D%X^Ox$GSV)zJlw2C -u5Nxiy6L6QxI;~1rB%*u@4?YX=fL{AK8mqJ6NeAkM0#=@uU&=rD*Gxd#n;EM{Ucp@=ofUAr-Su3C)cx -P^b!thTGJEhqx7V1A6;5!2CqX;m(v#>D)GnN$S;dxMnz8Q3~^N*^s3I{;v!%_Sw_y8DgPa+-Oto@u1j -48RL33WFBIlImGvxy#@kuzyOMky -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 -a7Sp!*R;|1i4qZWpDR$K -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 -OO0VdpZU4u?X{eQrsjRu|zAiKjS&9I2X@V -*L`+};_y3Sr=@dqL;0PrK^iCEY`vH6|oy-SZhgdXESGbYG%h*m*`~1C|+I4F8wP@4#6U8;3+};MiOA( -1qOdWW)YA$VS7dFSDfsJ|@Wrh)g5w!iR@&7)kC?j|b_1qpf2Z^_gt=f6AnLj%@?DvzZzCPk8NQV=lNe -{7KB$-0rKWlV!+N3XVGk2LBSk|L~4MYF{9RL6TaA| -NaUukZ1WpZv|Y%g+UaW8UZabIR>Y-KKRdEGo~ciXs?-}NhS{C+vnP`{t#^~y8@sdZIx -a&?ki{`YvH)dACzJoa_W^(a2|3Pm_RF#li^TK3UqHs=@#vlW=cZ6{Evr_GWl>7eOumVdm+%QC+zCBMBa<}Lp^Zl&tJ(_33BetTVL)9a*Zi@M57emmbZIAAj3tu#fGW<>=PPWahv$@NGaHc@NxYLP<@K{ptjhQ>v|zeoobn@`6|=%x^cKWqi<*UDK@f_?Om -;IV5ruxx*c^|G-v$fzCoL|C>; -`Tn+bpwRDD$1hWio2p+iMm`!1+vi2SK?MGO{A@t;&j`tYFduQN7&bHaMX1OVK1tM{0O2kzsauwx%jFa -jri*;`ZyY88L&{6i9>NQCh5tP$3x@G5y~&J((9s<8oF5txFEK^fHkK=x9SplVi)2#1^ui9U0h)6l%)W -FPmN&Wh%>bSd-C}a6RKTFn92?lRl -Wvt1F=BdktjeiVuel9NHxzv|CF)?B-#<6pyk;Fi)~U#DlOYIZ4I&ssN=}mt}pYsnhAn+ -40{1p?S5))j}*JP>=cV_~__JoV5$_*?bs--)9|ff^tk9liYtcOS#czn}gI_W#5CZ{B~x --w47F7ti@PS1}|-k!}!r(HNwrfE8LYBpspcX^4tL!8(tFy=s -%wrRgber#2q?QXAXZX7fq%3&xloF26KJUd(w5Y*{xuK@)kD~sx8I0klcRaCj!4qA}byV@Ehms0I&HJi -*#KZsGa|(7p5$a9b^8dF3W^uYXMD%m%cHY%+z|9l>SU7Q$O$Rs8bu~L2-X?ROTddDUA6FRCR+fII -gtSq7HyWXr+|-^<#OuYJZ#Qzr(H{-Gs|!@aaGr4*8~D7b%y%7H>P9l{t$zrnR6*F&=+Af3eF0V<858Z -ZUpeN%yhj`bLovj9UOF>i2WbvbrwMAOA=`j_ZD{ZpoDJTTiC6G7jj&uqq!UbI)S;7`@<4%A7W}6xG*%SXQ#iFhrL5u-r9Nnb#rfduFSj4^r^A -7)&gy1iM5N`P5@udfkmJb1dF*O=TEH-~x$lAGF2gvb=h*bXHP8)b=Q}`hFxa7(5D4L)HG_U9AF<+iDZmksQyVw~wmkVITmhO_65Y(airFY -1V2rx)FDc2!y6YGmd0!F+aY8*^r)YSo5`(*`Kjp*(C$-|?b9qv@nP2NdRm!PvQaf~rcMB));b6z4}K`+1m -LWJm=L#Fwzxm!`k!mS6N01pU2!~`3&x?5s!@+&crEmx%nNcPz77~+zz3ins||Q1y;u)F;#$&zl4{&6j -YuXK_vNZRd@||H10xZFjQe%pBmIi&3Id@FTmx2b_jZ;a8^8v<-DWl#8;&rz>OLE~81C_K;c*iFnt3k5 -S1UszZ^WOpDF7vb=WiwF3xFiBIm{to@&Z!;t4<;VmudXzvw;5+knm$NPGN#|-gf*y-k*<=W3h*C)zRE -QqZ)^9i}NRXpV@gZt_bfMBL^^q)ErtVZnum24zHVZPrL$1oy`s6GjZ29wh%b)$6QSp%>b*_;%A73EO` --!EPDRjsi*s>+khgW;_!wZ5-i?_Y0%RH@~{te*t4VGVUM2uewJ2;J=#rsIw|xV*efEw?D;S+3r)zrTS -t4#r#tM6>nro3<)6Iz*#>}W{Faa`hvLK{byXEO3=2Bw5oc9G~RZ$;iT< -#rHGGp#m<#<-8+A>XEE5Tu^zK`S0-L1tx^>?Mm~u@$b>z=-5%#NpAw) -RMyt$Ie`u>DD_~6qko*3Ri?bFZ~{cV=_@}L8eHS(l#`*!vLHtL2faJ^Q!vlS4`w4i16XY~Oqf)XGs~N -1feH$#m~SBD=9*h2`VkgB=3B~RJf=&0{_}EGUTN~suSzC6AUW(O^&Kc~{=@EP-_^2nfuQ%6_FqQ>0Rj -JhHrNnN(qetBzN0D|^a7&DTx0YUousQC?3^y>l{|n}sBf`;;8d<(RG{M2Ma1!mdN0;-iTEsi0_TfG<} -Z{`3Byc#$bWja{WlE9I-EV!#(#y;?t1LbYW;;k0)2$}nlcuvXoB*4t^m&S3flChL@IG+O@YmWu?Li9z -@$$YLt$PEWr#}7Mt@?W$ksXKS -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 -dA+C-T|hsV -3jLstNE<%DN`^F?I23?Bpg)MBlvFC!U~EGrhzR^S+>Gha6kWD!EY~+J1i1L8I&Yrt3hVTIVh@^=aSYz -aX?DfofDp51T7NSxB<;*TaPnUI(H|MdpNL1F$-bqQ9l;2g1fpAgrsHL<0Kty%Q>J{Di}-kO2f%OJsFT -(XK5zZ>$AF9wdtH^q!GCUwag!%qq{h@W3v_?&BUp~{M}dYsTrsX!gN;-u-V7x=ol?fcvz^;alL22#|~CK@-$#>NepNW -1cYMw1j%U^aoS2YVV#5G>l1S6B%9YgcOglo#!OOhB~u0x5kZ#^r`6s~1~wRf4r;nFdMOz=qilEzn&vm -e3Khi+YKB*49yYL?Ri=y0Y7wJE12KN9Dw$I6fEpG_ -8lp6PZ?#vF^gOu$0}+UXEJst0vebf%3hsNT^>f?U>&hmVRgbN%uqMd2g6#^cGWHx+t%nxZu$mf~QjOT -=9h^Z9BhLPP2z0$|FUn7BgW5u*1zNU@W79Tl48#H@KrZ9dJYXaCB>*;7Gr@w2v$=z~z8Ll$?9XYGbjT -e?qY;zVIK*m`n8J|;>^hhg(To0J -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 -Ga1k4?OB4RMPUhcPLPnMX5WTnCCu2mZAp7 -Z}LpR5XN>a=JLhp)xb8R-ZQ1}3jrnW^nC2W7Wnw{BdS_lQJZJrG-vklUE4Ed1niO>wfs;Tu+(pwDseQ -wOB0JT{TM(jc?g<=TcqJ%@+W#*v;XZ+@Y&pB8NB>EG(uTV#-dB^f5fqdNirx>dupeLg@&U%jeM(i2qk -Gu>k=HOXU~x@y9)hcxTl{t;vmvgG9xB<2k9P&V~=;K?7^)s%cD^A8)XsJLfNJi@qk~IRHx!HWYI5xQM7}!|9AS1pn=~UunRY5lN9Mbz~PGB$`oAS^?lz+ImQG^SdmB1UYARjRfIP{6Kcrj2a!ft -yD7Albl{$&%MK+8Oo8hCI$T29p>(j7BZ{1E|@X2Y`kLi)ceWq<~w58dJPc|H~gfpbpH4kXrSyz+qUEmmb29O{>vKHs-5Odqjc&08WL-nQ^O)j7zKQAXRa#d9k>FP%)KB4Es&SAG -t-*#|@OVC1~=bY{lw2EI8Ajkz{Mztk;E-WlaL>1-k6i8WBpnZ=w>?) -_M}(?#QiE5fZqTxKsTO#_@=4B<>1`t -80gvG@%lC!=LRZ3W>N3`-Z4(g5TNB%n1&MA1_{Q%Y{H+ze8x)hq5!i78?^iQZ$>>AQ6@?dO+?*7mhuk -duAmLs$)aES`L;22!4@3R?Zo`{g?Zm`Gc`DL} -xj)qFX#NT5M(fSMDtZFja6S7M9mr@LR&Wk-HmIPTtpQVjc@$IVfAU9^KI3k;99@2zyMnRZVSSf;ZM%W -hn$mMr%@@|vwaP7}XjNrQR`ns|o}7ru3Y-noEHU`=;zRdai~#|CB&{~KfV!v}lDA3orYMPI80{FUGgZ -}NIm{-XvfVYd4beIO`zK761MaW7W&M}T&wp^TgTV9?Io0iA=C_#tEIAhyl5HRy21%w-Khc1a>L(mhp?T<9i_?;j#oAUJT3eNCE09cu;i2b -q$bM4AX;aenpvmPDnV6kludh~6pe}!>20=nFavMGM8I|ivU$|?N?2z#QfMJ(7PgHiV*--R4%0t6`e(% -fb&I#>Q7S0!zZ?jHDEe@os^lt2WRJ?j$84gbYk4;&yeITHW8!$ZMbZ4TW&OwQj=RfYAT9QBzsHZVqip -GHZIUi4up#1wdOVyJorcwO9k9iXz0Zw^4obqeUgI3d2oF~R?41Y~G3HAq5;T@*La^$*eP=JJY8uo47z -e)0|G29E7b9pr(Ep(Ji=oy$WJ=*nz!cJ%`))2i-=Px6SWVs-j|TGjOOpN>>Y&Z$2YfUH@6u -qh)(D+Ntu(j_B;R=BL?d%fZeMbT=dQ~*zo+=g%~;z>^N%gbp{a)mflqa5{vFop-NdYtLJObePCX*>v` -RYSco8VSz6=S`fL2}0{<)1`7yPZ?$}flgmtOY(61V-Y#3gZmvvfzt| -NeaIrLbbi_lVGYMuckSU$z#?TUAd=^`?wN(>`iWD4^#Z>Bf`HZqZ*96#w#@&e+0hjJFSdepr^W0;)zJ -?iTTL@I|P=l51+0qPb85qDv+j9?K}GNAWycJ|QSXQ)-U^mCkxAd3cNuiK*vAm5*r{de6x#gL;b2kp^2 -B}o0w0}bcd%i{5qr_Xx}Uh>fk!#JCb2s%+qZ~buU+^?&<2 -F1+f>tuBfI|uN)4OfHav;F+G2gB*(iId%!EUe(>AMyq*o``s`%| -FsJGj>~(zMM1sIZ{ke~h~M}LPkEQwhI+P --tIDy!PpLdPFuhPhu#Z8==x#N_A)871WUQ2gpcCZc5+;-~R{PGDst9RbTrDO>D$f?w+pu#k;@Z}G8>F -drr4JUGmruIa*xRF}KYjge+7UK&J!6+cP030B9?ud!71I6`o{Vl8%6E%74VJr@;7BV+qqod~Fc&-w<% -$?`I&2~FY-n@AH0&iP7$Nj -DjX%o$y1@ix-=?!YnL6tc_BM77ozc7Gu##l*p7XV-uQ9TI(O)sb~3{TU)C%yVnVkLC0Wu$OPnb6maIY -dvhP>G`;N?;aMTpoDUZRv!M>m+khh4!nfnx-k({sMJf2eI6cF#RhT=UpOF8xjFBr^kDX?C@a8FsBf1+ -&8?-JD$IuCx1i0L~7{3l8AdvS+57}9Si>5nN6dpCD(D(@ppM`jIia`$ViJ>U)~%5#cW5H7Eq~ -6kh>Zf3)6}!8tUHI!r3eA{4A6=qVd`kX$Nj1Z;HV~S=_`9XU4b9>@(+4=z7Bia@(;zCtd~fVs#w}|bH0HI=+{Xv!8^<=l3n~;4){Q`c}fmQMRHj%13Bw)fFwT9ykdudL!f{t>TSpM8&1}50ni*2 -c)7wG03%muNAa`mTwzvxRjh;{-f(u7b82X8l*0sP5^Qw{FxqNn>+Z0{^1MDc_HWn{L+gK&d`_`acGk| -x2V`&NMSCe;YYpMkk_GOyV3o{5zJ5aU0PbZ%QD++HkIUBGf&t@Fxw5oPYnlFqCjEFGSwgtIjc@kJ(Rj -{Nh;K(wNr1|UMY!^K6O20?9bn^cd7x$gg!}<{Lle6!1zK?flHfdAITtxj2ngNx72c0lNpd>?A+$Yfk&N>k=S&1$|*CGf6!1%v0O+UBeQV6kn8RQnV=@GC1>f_0`OLp -LEdvUrxF1gg?;Q=_^`v=7s`EP_$#o-=1x$5Zwz?`!5J$e`U{=>KpUotDov@%bxLte)%q0dN1HwxTny{ -NyyIHTbA}kSr%i_hQ?a?I|)r1_&wZ2%IuQ-H$`sc6bd4*>oQUq13C;iR$8yGt0aO9Xc-`UQ389!Ha4% -9W5L@Nkv{b(7pnf@imsHLHng3hjIbgJV^%r0B+ahF-o4L^u*IVd4fIV5_|4IK#nyRa%R79geOB&v>ac -{rqy>3o-VR?)$F7uwGI3J8kXiBqxI_Npsjpg*S>)mCdU0VW=4(c#T=_Gn##{>gF -l^sIaoL_gWm&2CysdvP(zL1g%0WqLjiR$(+kadjT`q+as0xt!!+#NOC!*5Hso?pZi{l&Nim}a~oU$(1So -tf?FLS&;Xi8(W%Bp_sqKo=(H!y;q{hbh6$IAVT(e}8jGBQj)tO*ASfg_24F`H>r!vJP1v)eo9^uE6tz|opI+ybUCNet+iGRoqSg35zwJn8M!eSbF{Lmcr#B>Xl -I9!Ol76+^(+CL1x6E9NOdnENNI|ieJo}s#CMUuHWzF}SLeZXKGP?8ewj*sPK&{K8wp%(kqr%lMG~ -C}S6-W(6Wx}r-e!Kjej;V&Mu~61zat%#;Wi7&4KDB_r)+NO75Y9|9NB|kHiVXLbeJ0`c4<6d4+Ux2(!hQT2 -JH;7KCoOHnS-g-ry7yQZwO1!G2JB;1z&NRLJi%RcWZ#6!xia8GoPS(TBqA3tTNy#QY6-U`89Unj3SIQ8A1ZyI>L@-rlB+sp0d8tuGlA4$%c -B;y1e`^}HB4vSXIsaB>)`Ukz%>kE-8?je#A-BjB)vP?wGqBG%vYOK#`Z-SaoyOLiODJhSq^1NrSl3=M -WqUTyzOlXs&gj)B(prOpWeRyaX$;PLT*!G;ilL`X?v@B(rOFIF%SRA(F_egiMv!jx4LgrIYc!*8SN|%^loP|FWl_aqi6raK+j83GN7|UD!Pa=q$RMpzArO9NjcTk<<_HT$0p8Z -dB|xYTD;8d1ixZ1-e9brN-RWhfC=R+Qr8h5}sa#vm4_MP9`S;i`90!60g%6`u*NI{lQL`b{cg&+04^L -$8Zp)H2sqok{clk^ -%?B?0Q-5v*bO*hwT=G1mVL;f4p{aBL`Kue<&L8%fbB|uMvNIjiOjYH+QGud>md%x;rS$J$pw+zo^WCe)V?I71C0j# -SG;$iT$V$9hhFZos7*;-Yx8M%8Z%19#Vh5;aE4Xni=gJubAn%*5dCt?xu(2of<{DOE@#$0zT9KIQb-l -et*YpEuN$<^A)ZupGoHY+sL)c(d{zCS8vDJpO{1@>GfYtvd#YhP)h>@6aWAK2mt$eR#O+blrnz>000# -b001BW003}la4%nJZggdGZeeUMa%FKZa%FK}baG*1Yh`jSaCx0q-*4MC5PtVxL8vGu7pfkLJq!jitZU -ODXwh|P_GAPCEzvd`nN&$Cv0Lb+H%^}KoCp3JKlYF_uX0UDs3FgwQ^$bO1YZJ@Mg>HcJNs0%| -@zC`>5?~ZS;=0LnSP0Ufvj!wcpl>MF_}aEaD3~MER -pdLNqFp}#Xc96V{gUb(im-iz|2I@y(5DwNVN4nAUtyOAVpb(XeElM92`Ai*TN!gf@vc+blPSGAoMv&J -B1#vR-p%N7{e)Y^pWcmH9HmMSc^VruW3|0vZWQLD%@Iu=8)ERn#x5yzB>uU3k)cz-Jv?1m^cD?tpEKM -f^KH|hRz&V2+_skj)?|6e-k94rh|;u}4a3INu*hWI{iC3fq5f|(=dvbr*ncT(~6b_PceFB1n?f=14jcm{nZUb!S#@^;JbHtszw)Q? -z<%AAGoh8H{oid{L#pVy@zjvMKUDQj3)e|o7791(aS%%6_=i9@$&X|tbn#1$d5&jOs|H9hs#4FcrJ~3 -DY&YucnBxu?~qYvG{rz`!on{I-VCauw?fMez`JO-oYZdlixBb%-aO -b7OM%01Q!yn=3{sLLMH(FW6bO;2B@0%%UYwVYnUZ4K@l#dGV?6qae7IPSl*2m1@ki!^<#1A=yop^R+g -Wn}kIVU_IZV(7uRjw|@RxG?_Bw$QO7FIv}71b8qBaN)1Zqy_aMDMJIwlp4y|v!@~U3L>{g)ypM?Vz4) -2tzxJAE`rmia&oRJjQ3~gO2C%%DVxA1B`TR?5&`p(AKFuvaodJ+ut;l42zW2|e+<@^I&>?CM^>K{f2f -G~<-H087N-HUqr!P$xk9<2?s{PsjaQApFo2fnEk8{qYBt8UO&qTmS$f0001RX>c!JX>N37a&BR4FLGsZFLGsZUv+M2ZgX^DY-}!Yd9^%ibKAyt --}Ngt@MJ_fB$UU@omL&S$;8fFPxH8AJ9RU%1_p^Gg$M)?02Iyo>c96q_KgK)Hz__yAh3J(?Ai0)vnY3 -M(+y(X-=CFyyV=+4p=qjKKWNNde{^rxO}#0%XPd6s34U6Yt8_na56Z!3AmaNDv?vwK+=O{amkci -XI!Ja|89m3psrpJy;mSFZLrjtjZ00TmcImG9S*urOVIt;@O}a1QwRdDk^vtYg)zbFbf44%w=#v$m6)^ -8Ld6-uE&J0EyeD>t@&HgNE_u#m~=vdGl(zx!i8Es;uvH2mLz(Ah*0a8nJ00RkPhXiw21P2u~y|OIa7P -9stC4y7#W^*ShQMOu&`*cMG~HfZCawa$gS8ecBUZ6+iNS)lIj{tMY#&&4QnGvd(w1S3h<`Cjr`g8mmF ->+N>xIvwTnZ;WW -4u@M3|}+wRLtu>#SXi{-x)yI;ern@gHck|f`EG9RQEZY2n5RUO42dmzE4l!ZV*qS^4fvI1saQ{zIcj@ -11FNNeAKK*1mn3cp@#KvJ;FwgiC?@7^J*@7|@<(GBL9^f75HFu;yfBb?@acvs6 -Ov2UZ2^Rf|o{;2HAVCmt>jwyT=~+Y(r>fgUW)i(-aN=cCuhQdR|lIdh52Qv;fQs|b3gChXV~QgJao;? -LdF74-i%8WX^f5c@!RS&0S+Sgi6Mv;{nW`TYA|UOrnM%Hg)z4@+Q&>veN;<^bfLLt`Mb#wm5*RQv!?B -^ZAJy;*6PmkR+jB4F4P-mgVo7kJZPBYfB+Ll#=&raZJUvHyV4VM&S9b1~$1sO<92sJ|=&nB;h5LLsj} -yx1rlK-g$4H_g5-7NX0`9)LuQIGYkAg=m|;FIO~^4cZ=}A-gj$=!>6Uz5edUAD_RZ*{bnwhoS8+FD}} -AeV3#41ayF?b=q{>i@ud>(A~-*xPZwvT>)*D3JF;(xu5JWyhRjn^s(U~qV}vr}>)0`O{T4<|zRzoKo3ljjBAQ_7>F@yN8yxL2miK8zR*=E1IV -&)L9hNfllBcpCR1^T0qEmG{m!`0P?2agbG+cm!fpOX?^kR+z}XhECWpAHiJi!0?kxmO~7Yxb~e+gp%f -DNxIzXyWn#AkCq`@b_hYDUxyjbD)E!j94xKxwTw&(mtu1B}||fA0Ez~Rg|rd;3?yykWa+l!2`P#uR&l -GZ;M0Tn`J>~1xB%hnDw&2d3hqDI)g#xJl!c4CLpNry5@kort__R=upsNXwP_oovy#Uv6vis*UoOA78CmlYx&W8Dpq~LMF!7;9(EUhA%`_x-1t=a_^A -x~7>)4G95}7Epz`iHTJw(pqgIPwN1s8S5l?~XHbqNl>sQ0@S3NKTZ-OgG5wH=c@ --OlWe13suhPvwqLj!lDu7~%43RsX;1IXC!3sK1&{KcjQUmKoL@BS7-1;B{Ng*us^l7yQTm91@?DY9rH -cy0)CBar~z3Itp*6&K=89mtB~uWJZeZS{!RcX#9z#1zo`yOS4KKF1hJoV -k^*zjJKEFY=spqCIoPp^|)3B2^4Lwg6)v_}eUpk!LWZ|t?D2K@fZ`lum_&x(vGL8X1ffaR_lM@#zZo) -`+AT33n&MidSwFLbH{02If$0(GQe~ro&`|k21%zs_&3+ug<;8z@OC9@R#1TO%Xx|iVS)obaqwdz|435 -r{}P2gjI8oEn84`2mD^Mm;FuLL-Z!p2;+>J!d74usOsfR{9Uq4mel$~0XFbj53Mwmj_SDU!E?ovh -(HTL*E^}9Y4s!xUmcmRW(B)5f^^LD8Zo!=86?$Vw;1pQ*Tad8|dByF9TvpcT_8Up8s*Jqo2|c0P^!pV -IcB&OS^d0*Xs`5Xpf(hRVGj%F3F> -b@$T&#$CrMx9eDTV(HR9Rh-Ql)`SkZ))pz+!&C+kj$P^D&(1;8A+db)Qt^Z`u@>gPgM -67*L}0S*n2Yk!k-j~xdlKt)h@xY)R!;L9&7P`#BS&MKtT&MHZ_IDdN1T^ZOwO~B=Y#RiP~a$A?1Bcp` -8TMp+v;VWyHEso9XXYB_%8_=NeXQr6g&g>R_5mG8>Hd|R}S0!IzT84$cf-tbR=0Ypt%I3b1If!a=`%? -K7j&6P;o&gOzaKogAN|Yk3+_8LjhH##8a2z~5eLVJ=*P+vFH0E@-ECZ$xnjnK2 -%LE6en#ElwM-T!%?fG%dAJDhy>x=3Pj<^Ayxy#(n0?gklL>>SAW4#|Hbau9xos?0b_ab&A)z05Hf9li -p-@Q3U6qkloAJ_g>i>tD=!7GTGPq1U^%oquAUfN$yA~8=bSF4$3ebOZ7QRH?2B@N@e>5QLzio)3IZ`B -hnicd-D%xy`Hb1zG5Wt1pYFEUd8Ba6hH=HUtfTuM-uwdpku+JPIW)gxRq;-=ACgrAKAL52Co=9Gi -c$8kAc8cNwC+!+BMcgG7wYvht}d!2O*S!D!gVzN)f8%b)1v<7>&C99WId{FOi>s{Nst7nc|> -#zFX}(7TT1lfm0bu7DB_&*;F&K6=E4pQ_Gw+^z?rXxyuX!2m=H9k<~!F7P|mt&T9u|D3)3SA?NQvqKh -CUyhK4p^0**GVr9y$7jrAz|t&|M}M%9K);LjlZb^)GNP`4_yys1QQC4LDru?WP-lgUNy*QW1{e@ -W_tEFXDC}4r$hddC1r~#68q&+EwyEqqF$o!5LuIs6c)J;mv{W=4_uiF-$33%3*m@mfbnG=PP1Tg1-;$q4xzArl)n(2H -G2(?WPsnqWRtqNwhh$h9wx@k+Y!b*J5Spi#*+lq*e(XB&piFchGDr}qT%<}x#vubJAH- -c*7gvd4Z6~i2u+a55ti;hN+E8um%(T;~UZk8E@|Kn~#{XqiooqT@FVMke2|{0toFOp5_azvj1({z|7s -!%&`*Y;2TMwd+Et=vX9ct6^`zbP?B&O9Z#C(84Rb+VQwD%l}1?dS(cDqva+`J&AL@>o?R|cxT%p;BpA -ckv-5N<{tRzRpb@2O&~|CGlFZ!tl7-}*c{cYF+U$A2f%#Ncz51#cOY7=hJCh%L*3QrfuYqcNwb{AZ!WUq^vBD$$83Ea#E+)Z6qkW2CbnF7c7~{75E*=@ZHt=+c8c@5q=T*& -P$Qz%+|Wcm};}>IAq5vjS=(lp=F&9>JqjdN}A!b`#eeWDayTN6zm#yAbnixEJ`ZzwZIO*6D_x;8g*!f -KK35GNi;$!omU2=!;&k~P -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 -z+ZpZe_mOf=c4LZ}}Z6X>Mz3?? -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 -ww+l`DL388XDfnK7!eTPQ@ideC&zq -QOla5wA9`$IG@fOu?0SlHvOybQ6!Rrjg8=iT3iHS@s(q3E)*`f8xCL~%Iqo0mR$%>;*-}tCqarfJ4Od -esb(bLx)Bc-aF5Oo5Og!7>rd@ymiWrD)%pre!`wFTN5?Y_ZgRT6ciiwuxQU7$6JSl%}-hIs;g!%)+=v -sgjmK0D)FEp48kv6l^&$;L#EY?Qr}Lk^Zy4&oS|j1G<&m+66VOLRUSF35bDz~Mv$1mucq&!}gamJ%Rn -zPl`A?!qu#s0FcM!0i}>$sIgi7+3QkTNmd|A|1`EF|nEQ$AvOu+5&9954ff4yV@~%;Jiyg#G*xJjmxN -xc$P4$u&YhJjHDhB)*i){j=_i#DRQu?@siK -#Mi8QJz|$p2SNWxxeU5;JFQc!L4gv=`&!Q>N0Ao6QkSj&|ynbPFw&>aU$UdjPDw(TlN~D!Ol?n8&8i6 -eH0PgoqeMXAl@DH;4on#SFjrr;(WYNAj*{$>=gy?vg13Bp{rwiwP-2B -~G=%oaOvKG1y)rCO|I%DiL%)vNxO;M=_A}A-Gg|BLjT1Hh6i$G39&_omyAAdG%!rJrj`|b^RBQiMpq^l3pRuV3 -$+ZS^EQBR3Dh9L@iy#z2Pdj{g>_z)2lo*&-oX-}CN4$4_|KhtYV^VCP!jnY-Aw6OT5?442M-(vx4+^b -^%#!|u2$q`YJJs4!uNpb3~hcO0C3H%|bQ<8}8RwVvpZht38Oeh*m;hWGWF;5%4qsxEWve{*sIQldi)H -0!pS#$3$|tqf;VoaUOq));Nr=MmL6CfRcO8&+ce4=LZPS`D3vHbGRm7HO>6?1VOx#&t@yGP5Y~qoHKXiYXESIe{-zy@{A99$$MA`IuQ -EtF*OuIy1)3=uT0&b?^|y?XG&HVG@CKpx*39U@054^e}(3A1^0tlR=I}4hYc!HFSn6W2Yh5qn)UF$ml -9!S{|dvrJjS8xe8pmV!J_B*jY(!8!H_^xC>je9gwdM5@D+DdDUX3`zXn;l;RVpL5DpTIx!d!K-Zkv9? -KkI#qT5)NF^YqGWL4tFrztvNn3^3V)LkJ&(RnHiv{T8N!H*zrE3B}<)v7t94HP#JlUy%1IfbFe&tE1t -5NwVBFX!%anw4Iw37IM?u3C+4d)uI2Z?R0Z695f-bWQDKO5mVtV*Zn&j$%&`2yxR2;sK;%gcd8}CH~%Y@*p8HASUU;vdquQh7kM8Z}qed3X;Plw%@siIMdTC6bQ{LSW6FU?=0Yy})LE;1ASsF~9W6Y$@L$cyFu?{il-$AVr>NFVj@Cj)5GvpFjK*sADrViP5%?{-Ef?dq4s06PC7AG;{vtX^(C1*q^mf8NL;W?mRcu!t(794ceNM7n`ljYrhpJ9LFFWjHKL$d8H_FHF=IIAC -uc1{1^vHJu{O*{LBNN5SORe0wc+_l@*d9lhkUjn}mt>W->6z@6j4~ZqL7F3@;)4Qm21uw~4OQgNa|cw -eVV~uHDrJj?{F%IZqiB+K|)f$}skmQ1z|^KSp(k1Tv(X5DGWbB20K^^+7<2LU$5pQx4ZX8q_>$avvYF -jkmVhk@;I#hq`n%%N1HFEFmDA1%>`t$A=mAMXLJXn+be_3%mqOVO~=z#)ZpB#e>T9cpqpK#vimHMbg) -OyuVuR9zzimZh2_Fr#^8P=CEV~l<$+m(Cb=x>X~;nb-NWd)u!)(xz@0+-)z8z;kylSW3ZEwufE~opY} -$lcd~aboS*LS+TLgj^W%S)=9!6K?Uith5~M}AXrYE3g~Yx>Gw}(eOVRGsdAk|k)uRi^L{Y)x=2u==@Q -q~ReFX<>(y(^*l?4x^dt&gyJ^(K3xzC(l&k&E}1y>m?kJG!7;Bn`YWxAA0vSGwjm|}8lo}k -m+j>V$nGg3*&6q*++rE42jmB^Pkb|N3e6HLD;}zJQDLrXBRE*71Hm+}I`z8Dif$2aZ#@uMOipvB6iCI -pibz^$34g>Fa-HU5C3c6@z+pn -<2iEyPet<0Nekk#RRRB_}rWK~?ZNz=|An6W3xdXMoH%u*jl*}tfvDMcyB$~GgQ@AZHM?~73BlU$kLT_ -v@@=EYG8>LhX0HaxOm^gcmN -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 -eZd~B(G*F_x*O~{%Czd{WOG!;_g -tp#x!V5kl3KAZPFyf;(6`4I9sMN5xW1lLbHmQ_(yH}v#A-GWXNm4`8y1|3?;Hf8?vhBN8&)#W7`vbU+ -H@*&CwAKtjHYqTO^nHfTFi}Rk;p73P<_9a?7fE+Wd2kD7{!c+{r}RoQ`_r^D@bTxnvl%**xu*%TL%F!QqIy@ -vEA71D@+wT@UL-$_1!zed1}uo!!`^L9aQp7;_T9N#vXiueD2RWma*0b6TT{8jCqilDYJQ9V1sokILFc -#f|NDd1w2avXzfY~{h_)ZdO)I$v`CA}tsaqOC(>H^#wR-7ZrDYv5_k^#Jl{-P=9q4**&vP_fwZF~_hn -e)xbI&NAR@=kl7PR3Vk6LJ%9@QyYIn!odH7(CVUw1n;F_>W(n01#FgBWoq?J0H9bp7mQj}Oggf!<@Yd -(B>KS;}&d3{X9hMw8LI(w2gI~!K6Nepxi0QX40s)A!ba^N -?PgL+tPY)dv%F7*u_51Dx|4_1sJS8B;tX0~uzf>nusH@_ev?<}N`}cFf1Hm2$+D|$=jYuu>K71e!#~w -lvAf%5ihD<(^4ip?P7l>B^iRL#FTd}6( -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 -V@Taz)Zp0wv;(SwvOkF1uH -0=R8X^eG-&l_xASUQ@f4zlq2vE0ovX)?s`uSzImy_qOGtRM`&PSLZ-ktk6#mPX}bO4EX%F^XbwqtBf=1uyX`F1B8W -7PQ~jj`MZW5&yHek)YcWsXX)BDPy%b;%Zu6o7=zQK}wg-H-TdjZxdLX2D8RBa*aYck$2={Tm|;++cq>CE16j|Nw;<{e57n>{ZCBk -^5Mq)^#9$A_m~8Z@G@2rZTMXICKpM4sd9n_VPbO)Na5R$ei&*BDHrm=?v(&?p_~CeiZmyD&W^@7&+4M -MB;&?OhjxqGE-OBtTRb|N#yG}~#5~X4C?w^rA91|bk+UOGpsv#$7LYWP9RvlY`E17)X7pd##%k9xzlpym`y6@Irp2MwHm>h-{YOtZJRZ8J9V6=w-psS1?R$dla -PzT!WDrrV{+=n`Kz?16p_-IQI{D5-=aS?Y?bhj6~!&O0$V~Q!f-=$-5owxc7ZDa_6)5N>lq$gsPP`j~h`X;@RvE{_lIR)*jRtMc{omT{_$N=6^J_q@13ihyCZd{(;cYjr^s%r7p-2) -zEuq=ESY+=fCxVD&4&0RscNIJe#22bGwxNl&gm<>d0(qV5_qe5_s&paPG%J^VSttScNNb<0zMvCiOzZEZ23Nc#kPtAZkv7B6)V9S>QR(ik>z(gK)=TKa%{mK&hw5 -^Fe95NniX`JXI8B;bYl{LlRiR?;adBNt9Se)b -Dk*B|h!~CM)3uRe}M@4#cw0!x<)FGKBgo(3OpOF?)v@aMf!u6B?p8|Drz!lZxdIRdhbI`%bX-^en+rr -t~}VNW(rB)1(R_eA(2In0h01*h_noe4CyVgxZPRt2$ruIRU7J;x9LfK6yFVAL`jU|It`jX>0Gs7oaOj -1&KJEOb41?bGnxV3-VEyI^6`e>!CatJ!>i>rTEj2-+QeQx|NcSQr6g%7{Fsi>k*{txof^&URLF -{&FK^1_9qjD#PUO6KR6*O8hZ|*u0Cm_U)vxI_lWfWX2uxH!)FuM888zgW0kG6*b#x&bJg^FD4^^?1DC -)$Gy$hNo*;Zv4`#L3uV@E6VW8CUkjvLdo26kCP6>D0Jy;RZrqH9|(!U{7Sf(D$3yPZxRJs8POBA6kdB -F4{AUR_Vc;}iZ9^tw!KyNhi0Tg-sS+~F9gawFF)8ItfZUxQLp9ZTe-l}qw=$&=*4Q^Id(VcWLY;B)NA -c%WDrZW=(klBbv|I)s+%my!TS3|^wlsCHtcR>-@CR4kjh6p~daq!F^{<cw^JxRYr-~!{>QQ9)%*S#(;a?%siE%O_E$En$k%oMUoK)F>< -BOZ)%%D&ZV`Y+HZAK4Nfng*aLs`l>v)eQ6+qQ^O~2P(HSytlT5FOdBbVMQwlWDi!PbWj++yy)j6!_6n -=CNFENI-#GFD5L!Pm0G;2@|YB^h-hhRrUj<&jpI?+l-2 -yBiBds)ZRWRClQpBbVYJHMQC-92YNIK4uJFv_Lz_BJ$|80~L&!@(Z>zECH>qW~Uu)sa7WZqmm<-!ovN -Z!_?{OrJ()8-dwgc8q<2iRVKoc`YQ=KVp -}!fEmlia48XM0YA-QLK}4}>+h)ugrG#Kpp3S_`47%`>U_ty*pHm#Gnol~7G -$y&Y8P6ooy>Iz&iyepqHgqnA76Na6x;t%te69~qrzQ^febY&uoyQ%M7}L@33l{#xo3cdsEf|zdcc?j#g7I;}fB -)%Rpfi-T0V+s9^D-!pIZUhXxCri4F{bpIUL}8SWQufIlVhE(+FCqVfhFN1;_vV_hA8S0l;u^fiN%6tV -aepv|{1ugX6pu`I&-dS1EU1mmu2GUeH4i;d?@%rI2IGW{d{ -B6J+XBdC{iX!{nqfDy_&uPft`YeM()kZ<$Ft^ujs64{&jj|SRa9a$C))ks5p(PlTKR;KHnhg|E%eF*M -F+aHM|#Xf97(LRpRiccH~}-=Zf>6qw^oCytkjs0iNm~g)*rBmbl7lq1!#|iV^=;b^k)$z1s>sG@@AXqB0i -Ft|p#{`CNP*bQ9T=J9F~x)X1x^u6eNJDkziBU?$xmEgl_A^$9MD)&8k_awo -@1=E9_y>4W~@(xYDBlL;UnEoK1kDCgLeT5_u#2idpW;_Om-gRzIO_LU>#W`1{E!h0ea^;xiHrM7<55xM#(-jp@=;^(N2@oOAW-i8#a2KJ;D``j@;@;#^y*j7hwa!ee#3|Mx(6Ai3yFgM`RBc%moHC -`MoO3-P7@G!Xt?Hp3NmCf?!|Jj4q7URZQzS8 -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 -W?D#Px&BPLKq(mAx}e(MGbbd?#6*Uta -WCLM@3D$GTbpbX>HYqk*m@~WP1#zgb?aOV^cQwyKoo$<1ISGysPHwmcvCW2kFwHWuwn+h4%d4x{9?wA -r1GVd>OdF~)bG2Whd6(n~GjQvMou}dI8wO6V$JWx%?8C@}3g06&US+Uw-}V*nKlmPlrXpREOuHmnQpL -Rcc0(>{TFPj7d#t}K`^N`8>Vb8ymuosiGLTE-br6tgv8`$j^(&;4EDI$m!#Abw7 -5!-&(&ckc!JX>N37a& -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 -pF()zX}OdFYC$TO9V)#TG=IvCp^x -L#fkz}v;@E7ij&(FH5H3-_LNQHz=*F|{??@bM~qlRc9zH? -EOXT3_B4)yJ6@SzTiCs^`v}9-wS7jk#Ys0BaSNBfVIN{MtzUbD*TFcNO!f)sO1M2 -WEat5njFtX4vV-;le*bBc$2ai5jFM}%@7YvyjdHl4b-{IvHp1OD`Dl@xNT?;d*l2C+e@=nj_C&6dHBD -}HHxtvZPHVU-q1EvW6`_+&+AW3_gcbnyXz$X&9R7+7_DswH(P1e2lUvMK#1b~?5@k0PF<~;H%+m25z&E46((jyd?S49qtv^ -JlS9{L?vb+xMB6SZbHU!O3^Nho=W~5WZ;CLQ;+xD4XT|ngKjF?}K*k6R-EVoO9KQH00 -0080Q-4XQ=5KpUlRiW0Nx1z051Rl0B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3Wb8l>RWo&6;FLGsYZ -*p{HaxQRr}0a*033dzi&Bu>Fzkb*9cbEdwr4QRa40r}AGACQGCDGQ~NeY=A4U|A7eHjCSen^`3cca_0`Z9ND@PJr0BZYP -%tW(?RMHwj#j$>9_(p3m#kqHpCff8EXjB;E9ghC%YGCco_wLRVmSaJq)Gp`W<|SpNDI0DY*;+GwST2g -ggJA)1&sNR?58hBl;AF%NNDa}Dgx_zb1W^wPV!{`@t96YB*XSjfj$2wIMtYdv-1PsZ#eZmRmucxm2xo -_7x24d4o#uO5vSxX_&tC#tR+RWo&6;FLGsZb!l>CZDnqBb1rasUuq-2Vc?Yt~1Fkl7@%Ovl)s2Va>Fp!F{vaX~iANMOCLC=cQl8t8E34N$!Njdup`3G7;Ofo7e!7{s)3S63b -P)ul$g7QSdt@+}D(vKNhd3+V1Mc@6*X>++s|YJaLT`qx@kHA -ua2SHOkOkhRh2uQ5!g{zgCG4iNx3(Uq5Dm=trO*U%e6@CR3zI`HSfz{FR7%EX~*0}oGbPj83O++{BQq -#$J_A4!!V)Pb;;2fKhWS+&T^#Am9XQfXV)+u -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 -bMA)o)Ns0|XQR000O8`*~JV4;ZV{dJ6VRSBVd -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 -zqOL_K(0I%TP!{gl?JUYt05x&|GCencx -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 -+pC-FC~}fzt>vnlx}Xr!xtvIVKI4 -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 -NX_seo8uv0Xg}BtT@lLQLKr@L73)KJWqlPJ! -m<7tI!&3_RqFypk$0A#I7)eA~6Ba}$M$#)hVnZpe=1Imo&ZR9X|pG%%+UEUM;YMfG`LiEHUudx;F8wq -W9bM-aMYH39(pEQ1Yo)H(QwypxuSzH}mzHLVqn*3}|)h6-`gp<-tR-0l(Zug?gGK|&kBthQxDrc_I0xjR14`0=yE$v4wpQFpQhp6&(YBf~3joxMo`# -ujNaSi+B9<$?^8&IJ-IiaeDk=dVK9o3^<8_Tvo@iZ`l+yUx)VFi=?^(2ge*}*ODxgM*!*Daw;7fj**t -_Y<%%~*V`+FfhN|<)F@k7+5unauaLl3bT6cpIYu1x?J}BJOw%%f@Z6{KTu}-*7^cO-8f?xAFh__PO`f -6YLf%}n2bx)Y_}Q^~gEX)T)R^)r`wB=h$fH4qP?AxH2oO`}dkfl>pw}R>>X32|trUY+V`yU9htPPc+L -sz5Yb~^FiZMMTIlZTO!KJ55$;0Zf;1|j)RZ>W>Vp_{D7NhI8#K-WK@tfZ1PYXHF -f1U5m6}TOy;c6F^Tp--^8K_UeeF5jy_UX6k}Xh0{&0;GbkQA=n+;VQ8@7@h1I?anAZH|+m57i}lw`d4 -swTSx0w%-g*29!@{u!A3Oo?YmcMGj4iQwZs&CQ`CW{btuVptCLB(Jr4^G4GpJY#ENI -6~5F8!y(0?pOBb-RCTdC -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 -$#H%&0-{RNpVkdY!buq<gTn-eQN -)Z&S&6w%7t&G+0Fb0^^-01sLgDDUxEVXwww&WI7};023~2prhglJC6)C@4(HdHyIjOB_X ->FyJEgdz~-jH&mtKS`9b8cJ|hB#6B&rF#j{B-IqqBW#Z>su#L;NWKs*)xNyxG;^L_YVK8Alfd~!8;cI -~VNG`GHIX$4qf!#*PiPXd^ZmNI}<%^Yj0@_;?v4ruco^Y!{l -NJ;2K9VIBNh4(6~ltFy9e#QEEIm*@X{fB7@|e`Vc7;v>k!lP{m2ec4Yua8yR* -ABsOs*-?y`NRYR5!gIRt?h%>1lk=`coA)4&$Z~rIT7pA4+7^4WneTtovVSFM5<)Ua>hc1t`yE|dbdZB>A@r -!<190F!pz|dph}ITXb-oad1b;n+*q|0&h1%emdei{)rFPwjzYVu$L6~g?FgxjT(1G#}8~}jJ6(9u-Wu -1XlO>$v;vx8UU`uVjasOe?hN%jM$Lub%`7O+1!?VUpZBzh49G{x+%_#mpZ06+^bL8r@@1#Kc`;J(B3Q -d}}v9ULq~J%&N!sdE>iQpT$(H{i#hhSFTtdQFbJ@)w}3CdQY)0`n1~!w`Wu4~R7IVcm?Y9tZ+s#1$iI!aM~d0fG+;6yrJ8M{p -Uh%T$X0q#!T)7+?JK;o|aqOi|f7Dr*lxYklgnTl9ZhGPP>q%R2~N8UxGA4aIZ4uDY(-!%=y2VzHws$|UAx2liB>g?RMbUw-R -RaOs>m(_PL@eQLA$V+HG$^*k$SwyOZ^@~cZ&J-h2-uqpX5Y*`wzIS^x?^AhrN84o;^+c{244Xq>$X9u -LTFI+&<_au7Y11)D@PGhPhR8!2;N+w{kB>LVsdS3O3EuRqxz7?QZx|d&=nz_B{$v<<6dV=u+Rd99=V5 -R1s$UV_*lz11nOI44SCG`+qlLI-$YkIa>W$27KB92 -1<78r1gM+JI7r@d8jF+w94onHhtmj=ib!yqE$kX3aF@mm8iv*H|Qi$FQ+^aTYgF8fHhSo}f7!3ZE+&1 -&#RVY+(v8z?hj-MQO}aS%rNW_)DZP;f~h!}MwaI~Hr%)G;)}QMf4H=r3^uVQy7ZlaExfn7mks)Aci`K%=raF@ -jSftF8~jy#>STi`CgNU=<)QaZf__&27c&Xt`lh)d@m7rmB`lERw-SvNu3=03wUU5|Xez)CvA<#si}Pv -EfIahI>p?~V68w1xeHmd6tMY>gC#Lot_s@W%QQ9X{RShF_XM;HLbJ^8B2LJv~^+Q0DgUD3OhkP(|WjH -Vq-Y&ZP2NNM{Kv)JClXL8@WHxYG_IG=oPL0f3%zMOqILWwbm0;UbY-Ib<`BJ?P-q;GIX*Ok@{x&iaq2 -d1zw5SYqq`rq#LBDN@9;Oc#u_p)mGvxFsfQC}beNav|oMyc4Q2tB20I4_)Fe0ZMGX$K0im=f`X1m*vu -XngT4B3;IAPYP?wJ)+dMH1Abs{|3_?puCy1SIRuo4 -q`r!6yTXB?IJq%6fWWG{r4(zwkYEwv3ksnqaN1&WPk1eTz##CuEb#NxQFc5fr+ol!&=7Eev9hHSU-+E2b(?&ij}1GVj)fz-esMs(TFrFYl5ybTygqqn9sV_ -GDgN9WKgpmejCl&VT_x#?{GP3PHe3%Eh(H9QYcF*nx1k)SmtM*^95v{_^6*SBKc_^duC|RRg_%h{&n9 -qMVw)PAZ?QE<6Tr6_p%Z;R--F_Faj1Y_d-7r~x>+A&yKnfGccZ?OORyYYHhLoXknQr{G92GD=n;GEbW -nMM9MS+BGc^5^|S85Y@_RjuWPYfKaM7YR-4}0|v1ODp)h67Y%#zsCg_u8l%i4!%YrRZ6U(^6`n_Jxz1 -6Sf{W8tQMLUW&)~&k2*X|~hSh+UFuy;4FJ7GfS!l36Eh#oqmldxS~R&hZt~4`T!02|{_){ -z;BeD)J006D$Rw5}l3#Pl&ReoGLjgDxAm~Jy*NhmG^;FqrL&=EPQ8!fdfa+?c57?F`@zd9SAj#9!2F4 -t1>vi8t@Fd;->Kt`J~^IC8?dDbw_WqjJ`jA{ml<=F5_>1c>VhPL;UgLzt2ft^)~Dk>9$@Q{hY}>6;&p -2zZCRMnzRXTdnA<1Im)veZ~&SI}k@kK-L?}>$%_Ah;M;aZ66=#gVTvu-f&^PAEmPj -M8osAJUJAf&mrp#|;+-%plHQDFbkhC3ljJ|s(h}JH+I$2AJ;0hbV1-iIE}j)zS0GHAnmAEk4(;Cr8g6 -Wp9WvYm0Vr{W4FLEERIICuY`z*KGnGI44ybMn&6N^BTfMk;>Z)mX%p7Ps9Zp?X~fLoOw&v;t^cND*7bir6~@=CRc@1*St^QG5R-!kSM+L-Q<#*ga8=`|iQHFk; -eXy34Rf0VMci}HIL#>tU2G-@?}v?)TLh#fTGD|M1@*{`L|S2eZsuC6hq-VauXuHWRF -WZRgI_Ri)&$Ln-}Jup-KLU#iW6fs%=tjGBync$2bD8@dh5GWd>c7(`BCesuU?To~!H2QxW4r`)q11o2 -dQQb?-^%rrT&HUgG7^5>1GfwO%wdnF|`H3oRdfj3wS#0`y>-cvXTLMIV>L3iDB1S}D$_8*z_d^{KY@J -y_mC(qf29LL4^Y9efJQX&5jpbvjQb?7d#g0Rru3xMDG6~vamE30Fje|=0Ja}cjiWF#n!S-$OE;C-qbd3Yz^&m%|BlTh=l?%KHw4Jz -`?5R=?+A0uV@&%M<`&@dWXT1PJC!fD^*lb|Dj-6CG7Tkb7XmBo$dIuf0w+YgR*_3Wm{-8oW%wD>`_F} -sDQXY>rNeNB8;g(47E8{B8DC$MGEp(=4Ewc#92APx)5=Lo3kz|-pK_LX<|T}QYrdw&}rB(n>GpWgWeP -nvNIx*?&b1`DiJZX0AyydNX1xX3v^$-p#-ylR;+Hbd6y@(O7CkIT$dLQREsHmoIY0bND~yPNYRxE-D* -c32I@$|IbQn!#$lw^ShT4d!^)yuakHFI)h?)tdfCwRz=!3U!idVr09-*Lws{VI2UZ%(Wc^c}R24Yjy6 -#x{2e=0uHv}W>??ou3 -8^a(6cElxT~Z<^0H;=_pkApq6M_toLv-~(-e`}6YR>4L!n11Is+k-gFF|Iy8R9x-dvuKT=EoU0Y_d6i -etPzT`2ey2Luj{#3Azivk@+H+AWp2vo~Y7};UEyo9mz8KbV8l!=z+w4 -FhEjDPR1c`uDKj%DZ#{;utiVrM*GiFKU4TTro75TB-N-6W=E|!D(=OCxKD7vZ%8N2F_3nCkL!45ux_^ -_+j+?GRb$%R!-p^X9bkeHnJ5@~xy_N1#R@_8tSR?W#{ -tJ3o1VMS7pURb5VQbhBs8&HJrV2{*6#?{uG6jY;X)3%K3}_vA92h~4^j&Db=8`^F7##&8<@Mz4wF@Fm% ->8vH3pmuu`bD?g7(G|npe26Rf&c0dDja|(m||gOA98DHq;az+WFFjBBqEHt#aSxVWY=I%2zQ2SseBi=PLGXuYqO`l=aWQ0}8GQnh?!!QdM??vusT5IA%#VgYmB@3i# -1B1trs&qDt-SElXaT&meR`f$DxZx@+K6Pa)Nr3x0(G7>D?ZBWA*3stG#yy?nEDR*aRC>!a!5DOW!R~1 -G5A2k)PmZOMR2<9sm!db4q*%SD@aIv290Y*(6h`V1%xI2;hty4QMqa767&&=r5RH0|Xr1>qhGY&QL(` -Vv{_Vjr-rUro4j84i2Qd0kC1>V}CUu|bXIL|>rm$-Wt|Wsw*iW~Hhd16HQ5y=xP@0B#CVdyCz6GHl?l -siID`Vhx&aoln)aBGvUuc~&wFYhKPFwIH8^nf&%GcIQc^dsoEsy5ZfvS;VD@vX$9%H*c7}{3L()5<~Q -sY%thDBM4EQv4Bpd@ri1D8|urR-cGPUXW{G@|ojmR{02GG=o560nPzLUFc_v6W(o8eV9Uh#$nQ8SWhwJg2r&_8q=IhzrqqGddqbbbP2Hv9@D1lMV%~hDpdSFPJdUJ5vWcb#|T -@6>ZqF~aGpzwk$FrA|5nFH_wcyNQHUSc;bv7_JE;A!RzVo-dUR?q`mC|97wX8Se>f}xxE}NKXq{pGNe -MOt&w#el=v}svD4qzT_9@xY4xK%m6;(=(55 -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 -u`K+$1UecOy>Xmc3xZg%}H -C-QGl|vJ6kNmU=aj?fSgfw(kJ&)Du5J8w4VKXCoPXDMNLUGQN6w`^oAFyu6x5SyVkft1d;zqEAU9A)5 -?NLGp%xob+JJ+2>6L3Qyx*NO_k?hK%}~IW#z`81&hD0o3Ylu^l^qh-k-ts*~P%I@C3sMsy!%Yn?9s&r -B*FLc(^aar+!U>THUhbhVRE=U-vp{)J=`jq%S=t=LaFApCQc_YqkbbW1=o|c5Nz5A2vDP6ku5i#@1R0 -$2_hwjr%=K6NGlkxWJb7Q>FzmiyUR`xTqC%{ILRQPX>%rabRw4sN1NVDo5Vk)qB_>r^da;R`OdE4dW` -j3r0&>h?P2;0!gmTp1b)VAB)b`i0BhT!~IrjMD&zuhKmr#M-mcy9-J$fl -Boco89Rz)DqDKq{nZ)KB%x8T)i~eX(R02h3{4i{cou2bc*Q-I!%eJ+_)=@wz=U@`589JIrHL9~J~Q(5 -RAD7id)B!SG{c3~D5G(ysFee_MHln_5D^A_r88BMA$ppPLAn*$S8GSCb|89P~GNz8r7 -sLC<&Fe_w;_DkrHFJ|kQ#-bX<=NDO?{S}ad>>W7$t5?ocxx&kKHyazU5w$;fl+iFi}AaKZ$b1rgot{5 -P!WnO>BWifah4+MB;Ily^#{^FP)h> -@6aWAK2mt$eR#VBC0E@{C002rS001EX003}la4%nJZggdGZeeUMb#!TLb1!psVsLVAV`X!5E^v9B8f$ -OcIP$xH1y98y_MIc#1BX2X=L5P;wj11T6QpSuhiu@|5*>3RlUh=;8wCCDH!~z9QdW}g8fa~a!!`Ykvp1*rLr&q7lg -ZuG@R~27X!abcg!t_r+O53 -gjw@K%$cr*T6(ZCEaGz^h-Qc5T-gR0$CvLr37i?DM-jA*tZeQD8gP6(yZY|p{n2kUoSUz}c?uq(0EJHb8;@qS -|ajZmyKn)Csz*Tlhh2x69M>kLRanR{ -)$Hf8{5Gt)*DAEx#Ab!V#SQnFGlaHcD*HBDQX4?D#+N3n$Vn%beL7!5hJN48ip3S5Y*10_X`Z9o_WYH -@8kuAUs$`t?-VJ)5e>PDDDZM0vQ2>Z3HiwUMHBWRE0?1NE;#1s(N7eO7B5&twD8wR>X0ExZx@9KtbEm -A`K4srohXb$j*9>h9?5oXatBAe$P4}QCX30dBrZ8jWBy@!Q1Glz;t66oU0dq{xlWv{*DSk8PT*xB|u) -=Age8Jmhs9o);ZZ4(7qkfHoQ)?{5>HHWRUZAD^z1iBc-%;#UE02zrsDyq53`Zuz`Un-~c5r8gNDAm#r -RH0pWx^sxi}ijj@E#j1}YxU1|kWX4{P0ShJpb)Ra*)RNR?K{+!j)@V#5ud(d3LxuK^Qo^H|9CfQeEIi%bh;T -O%AjrF6qxw1p#q@QnelBJZdwID!>6pT$;?u`K^pOEv8bh%!|ETdNx}MRB3w%*M>S7uJl-HrbgN^}tE# -{mk2(5j(-FKW1~?T55S$?r5rIVd18qSOw(Xl`J0Zbbo)Uv+#u;ozjVaTtwKm2R85ZI%M -<#!0@|hA{W+v0d6O_T@>J$sH8E2PY7Kq2Ehy#b?&vOb(C`?Pas+{N5ckccJH`2D9D?cz*xizqV(bJ75 -iCG1gGPmt&6}F+wrOuNRjMgK+VHI*7PnyTso`X+B;b2o#qw$fELg>HVBt;1q7O%mLdVP5D(m+L<(#m`pcNmxzoc-TAwRVLDwAx^vOPBnzvZ0*%gJt!&~^N(4@8aOOM -M?9vJuJ9zuZ8Kx=o39dJ(pGj!8#BsGT%gMS#8qaz!h&PXqL=vZ5K;)^UV{(k;H@iGYW#Q3?;?a=GTR6 -6KQg)onvXAn3Je18P#y5-ZOUIIpqs5dJzUffIFOJ%R)%dY`}h{vw2MjETKL9bst+25B%0^c4XiL280; -EmR~?L5lcJ&%XQc7G9r$EfH4PPw}!P3|gPT2|$717OYU}&K50Vn=Fh0q;0lHM!66@-`ecKp;vLVASPT@Bg^Grz>%I25FZCIC8FlQN -lm8?4-wZcxb46hV}8*MyKKl>u2|B%OA>> -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 -!iqYo^SCvW1uK6uW+kSm0iW9N}M#lPubhe=CZ -*!$a6&S&sAAd5^aq1PUTx0W$_Da0TADY(d%953L>OlcDke$#fupS>r4Y>j-Ka^x;k{;BTV>6-vKD1>p -4d1r-_gV>t@gVnKP88>rV)B!HnIcxqKjq>(t)@hm9U!=R)?G@ZPcrb25yO0!~`Uk0H#5J0ewQv`@me^{6Aen-G`5ryPW7q -F4Xd>&>IQxYNIfT8Gw{wZ#~@bOQ6P)h>@6aWAK2mt$eR#RGmvL_-1004sx001EX003}la4%nJZggdGZeeUMb#!T -Lb1!yja&&cJY-MhCE^v8`S8Z?GMhyP0U%{y#A`iCO4I3I1bB7>vi(*+DG%2>BE7Y?@*|*Af!rjTPvF* -Q)yt7{HC}{%(v1A^PR4}OknmLmReWO{55eLU#>C#WI>jkb-EN5v)W)OKZ8((e|LD?2$!$Z$e+7iOABi=R -qnf3j{F;b_y?;IH27c%}!cOPe2;ykv3n0K#LVYo7H&%A_gH%Le;h!G+{ -~iY?DLL`{3?UMtjEPpxgCi_dC -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 -;pLWWN1Tg2_jf^-QmRm`NUg*dz# -QVMkdp2`x98PO6rvs*M}2YI(;Uh-0ewpskzMdz0jsGIuU?uDuX7muFW*@={4iKFA<@e7$v^c~N!Tx4M -q`@rD3c^pQDEEk96Z_b!Exk~nWOa{E^x=nfVS -toBlnc(HLmi7;&I%sfe%6Z5Js#-5a6ort8dtTy7%Ojd*z7N@Sb6Z4 -@9~Y%=|Am=53c^uGwk-6i?OF0a=KB-f!v1o}47N-;?}2u~3XG02hHgSwwIP+pEdc=ps9n!W{+F;<`jv--svTTTcG_ayN_@!Wx*gkWZ%%NMvkv66WwQK+4yzVVBi45q>fjC6=pWJMpI8{Crv7xRXC?~L*k$)3G&%6%auQg&O8j|8azz(dRkG7RWW)B2*U==-J6G1T`-HNQsF3~ -frK&I39s#Qil0|p%!)s|Jj4@MVhYNraAFS=j<4MpUL?;!;Gh1Ns52_ROdrrx$e)Gyl1t0&b6zcW=Nw! -6iA;e@PcUh9@+x!nuO9KQH000080Q-4XQ|r5509_RT0E|Td02lxO0B~t=FJ -EbHbY*gGVQepUV{=}ddCgpVbK5wQ|KFbi%S_5tB2jT>@Ag(#-bu!B9(A)x%J$A&mF+MP2}+nyq -=q1+sHWz-Uv~o_!G~mfDqD4`nu$aL4WQAFUv~qRt94$KEX@}SnJtc_{+X*_&C8|ziQiK>w?7MEzg0yF -&!KIhy3Y#nTP0L^loa`jmAf@iXQstZQk7%&VO`2R6UA`4nmogqJUR{_!Qx>|Lfxo~9fdoEJr243VfxnW|tVRb)xLm2oVxEAbU35lgjB`7Xp2j@eSiB82(xr=zO*y2vA;>It -p*7ZFB!N<;lNkqS1VP%y_R&&ncCZBPG*!RKXFF5hKIKBiw6VwIQTeO|uKt1PBPbzc0;Q`vs8(VSHJ_s -@E)Gd-4mmdGroC5B}l!u)>~Q5hC_UWPc~un_BYLSK-g+7nkooybmwV-<?CS^f6q<=de}d!MfPBzZY`{cQ60+u# -5C?2qp}Xbpp(ePsTnh$^5G`*?Y2L8?V>5I`1rtzgHHmz36YL29jH4pvkPt)1$)6vW{zgi-Pu**b&0+O|{lgHtvM -q+8k0sCjAN(PD#8ngw{z->c^r)yYuwGxGlShVB?k3a{R${ANI%S%?~Y>xcOl1lM;DolU&3#doFRWz_N -#8UZ1u;a7Y)UfNA`X`T -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 -H%$|&ZAp&vnK1?Qt+5bX&HRW^PCpR8? -lyClG8f|e5jEzBHLJoTP7qZ0=TUN&$@EH0bVYO71#WIWI3mqq41Q&KpAu(F3Zkh~#`k`bU%^C*qCB{Bj6dPMg5lSG_*L-g8GiCWqDh=lxTVYb)+pTXYAIM -KHK7K8ng@q?#wac*Ej5URQcb!q=QhnDCLvQz66cJj3D|m&QaDR85u}XQpSi+&#%fTdulC-E9%2aTWqL7T>$ryu -c8)-6@5C~sLY0@Vhq8V+O@9-0J7BL`^G8wH9>@Fq1X -rH{l%ZZf44ARKz1mXD86}61(UvL;27G}GPpS49T|<#7%{5oV6s&*1d~3qI7-gXTSFb33TDnAcI#sx%_ -#YP-~}73)U@o%r^I{ap>&dNnKEm^VJA#=m7W=unQdTW?Y9uTSX6SCRvXR#Z!(&SloT`hN^v969zj8*5 -)AHB-lZi;s}2dr4ZtOR`?b||(2o<~-OaMc0v=wD&cqQopi0kvLrX;tJ*(oz)Wl!)MXgeiL?Mi8HLpKf -@S#Fji$&De<#IggTy5Y+!7GGF6NWK{@1#->vP3W_h+G*4sW`c1pg}N)1izr_wkgHM4Yk>o_)cYF)a=&WWGk$L82ZNlhKe7P`u>ij&}o?c -3>~}9Yvo%Gthe3vS$t%%+!rCF19DM-151ECyA2aMkw{*Pm|-pgf21biv$*17KgIO9Q5lB=ZGJ~;2q0XRU#gIiW9#12ZfG)of#)z2?LP9?0?&$&p~QM2=W2)uS1fM*)w42rRb@DXUKdX%VI)XrOAVQ@ -%Gwx8JBJ?Gs+#sJ8l*dc06P%I-LMuN^jeNE0b8B?LpJ;d*vpcg`QFhU9yiJ^g#KkQ*mVU-yU*6N}yM6 -3fF{J}i_^WG{*ggR9>)TOBqX>;}Di6lPatVCgDZ=4`N06=5SEV -e|+}8{VgmerW6ny1KXb1t+$F?-9owuQD`bS;Cr8nPxtfqQ{e -?c*BcU+Sz6cgCHzOdfQ=DAnKqgno*s@QZ)ex@E}jRoRJjn$%1^G(mVx$uOaYc{40~{kNkCqcGld3m;S -!(#&~q+oOnI-qxzDq{J^=N_XobDbQ79P|@y16YqvfC4H#8mXu0PYPX3FaQmG>=8vOUoNvx3NlBp2jw_ -~&=x@4ezut4I+K*Mirje-t#DWhIxXii3dH9@JdkY}ES8QGZUWD^&3t99IiB)Sc{wpi1n=GRnXSM%G|! -0!)EIQ7AEGIPd_tPH71)o*R)80IE -SiQu+aHxx|QJAixOXnG524{@P_zcm07rxQzZo{TZpa8aAu3G!E7>hxtH6}Xn=zZn`K1=)-60!*d^H34 -`5J?#~Xy0S5?G6HyStAP5iHG}9k6cCKrq#)3Y?dFc>;A;d;3ZgnFC6ws@2#5ttn{_&x#qGhK|4l4+!s>v`UrY)AJJ@2t*szBI8! -8lP0x&dg=WVhAW$1l0@`h}uJ~yX6?s8o= -O97_Ow4w)U(;iI8^?-}aAhG(U=p|G9_UcE|(SwwVO>Np}Fm#wE&ez9J -^9!4n%JsUmBG*v=-Vf7vzmrg=$Db62e@7^spZ(Epbc5?Kky$RW~<&0 -9TiGH|Rq!0U-8n?zd68YTIF57dng~6p;%8_`~!X7UW{pY&H?Hk#f92cR=3V(;Vfp{@Jw@pfzo{JPMEI -;dA1Pqtlb)v*XjCvy@=7R=sp3w9GxtV=ZoiOkoCy@>5J&a$&8k8PMwK4(GGWEWNt~6ylo?&-D -0Xr|-?_DD(5h$;smD7tZN<8VfG@s+xh&3E#;V;c-LE9B$ssObe{#$?Gq~?i+fXoBFZSW2QYQ>V`Rdkg -q${7FvsSYX??$IScyPMg?u(>4mlonorjV=zCPiVreRnz{RF^6_ze7fe?L6g)!fodAPw=FQJ332PjrMO -w{tD{8v>(DrUHcImM63<1NJhti)!hdtMvB5EK8?@ymHi{EY$ZVl_r(umcKPNY@!w#Cr%gtRR3pu@lo` -I;wM3VzSFF%`)4P|NjY?_@dEIbuJq1zi<2m$Jt_%T3}HcvRSMQCDs88gE}_PsKKgPwPVES5BF`W%_pnja-Q$w&ojW+O*JySt$<}*D8}O~?*ve -DCc`2}kwhpA!m}CE{d{ssoSGqT?Dlf;RuQm*HXDMR+hWRUv@%~i7pwJt~mp8mOYfw-?o6xM$UTZ^mDA -E#DgwIQ! -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 -(?rJlxQUA!Ni& -TQWNnE0ifjhi#`YMp)NN_H1-)g{HxTLhFbTBok&xFhtnUSX-sM^Q7eATt|j#L=AEluY1|s7XIualZtFrK46 -{$ho9}K^@!ImP%P8-dYgB4bKN4h8c+)5)b6IA=|9U!zK(WuX=#9J>_ib+8qD}1d31+>vGZZ4b>3xZbD{8k_j(6*7Lnn4$I5wFxTk9OYKQ>f4HP%B??(GpD`>5{2Utt -L-%j%eLHl2v#4UIld?D?1X#H>Kn{`D|-xh6x%(21tkySAj -kNi=xHDfgy~|01)v&^g?#A0%u0ULszTV<`~F$k%gXGzQGKMCF%;d#~RUiIuA8YtrcaFn`|Q5h=Vpj7q -`r-`$Un^Plo%WyIQ{28!Gx9E)4Tq#)D}YWTvT3N -gla0$(JRuzlYj>w`u?Ve+!CWHoaTc1-{+j@l{&G=+Ua}iPZ~OJIsZp8WPQsH`lxTUSc4%>mp6(l_iUmPvqrhI@3XF$tMNVLkkAYY^szs6>A7 -kKnC)?64Ru7u$I5>`!m~3T#y|p!PUfMBH>s~teSyUeW{A~Kc>^j`D54J)WLa -iq#X!7QveEbJ^+%uZmrJRG#yo?74H|M#s8bnM7Cv5SM&L=e1s;y)66Tiy9UkvHNz5qNmz`C(4laZA}! -)AWrJ&iZi=BApiD)j&I{IA-sA-tJdy)8^N@NXFYwYb#P+VMz^`@Z?D{Y&hse6FSJ`F#U|7fUfMBWU!L -Avc*w9{;B`N%aunwDITe9@$dlu_6eGjobTlp>4nd2)ANSmlsdnHkaiF@-b_4wTJci$L -id+)i8ZKa_c)3mP^kPp -Zb=W4a46gwd{YXw99BjfoQ@TkRSw<@cUoV7iSb;~M7xMXw3gVk1qh&AgFjR+4lDGFQHtMcYwLP -@ag@W`Q`+9y3lFdRqfL}ZRJ#+mIx+!IQKQQ0aA2p4Evj?cmlv&`*O<+=1}JKzP!sB|%L$WL7U*F0&ph -F=tq*fPqBq7sXN!=$VcjW}#i+YOJ1B*t8HYj~v1Go6v-F*Go-3D`#2TKs*)ui^$ChQl#Y1baMg>ucUo -?ckgP6_ny|I1tNeyxgaXT(=g#vB0-a{PxzL&J+20oYe7eGHz33Kf>4`E88q9ZQg=4=lxsZci&bWU0g^ -nKTgW|5p6OlE8JXp(AhmY$mM0WQD%oIvbc17Y2r)Y)a2yw2qS2kK6Apbo_~P?RQK7xWao^-^p$9D@6T -9}YMv{Wj*u(LV8lz+?A<=YSW-b9&0B@g|=?W(ZoZPFZXB5Fv=~qe;Co<5_q?t0VKV;!eOqiXm3 -r5nzZ}X70BY%)#3cfA47q@Df5p1VHEg@z@vkeS1m++`(rwRh)awpVv_NChv4eU8|2?lj;QJpD$_~9Oq -3aa-{+N9Kpv?gUe3ceAVoPY3pGhr8dG^x8f|&Zud?`ti)*Rz&h74wJLL5%#K<@%_leOx((>A#RcMV|q -DtbW(8=DNEVf$uY+gJjco`n2x?Cpp?VB6!Z~V2jmc+-S*P;0T#=Y<*5yWBQ -X1WBtZ%of)`18*~pdX2+GY8dzoJ|vo_^<<}^WKbFwkJuj5E;ambPE(VQgmGjcTbvFteepPYplTrgQ1X -$2>rrvvr}uyUp=>?dc)Q`0w*Q+(jedoEM73!-@Og=4`%@x$gu(<-oK8e5G5{$1K(5CMtLbLje(qH97m -{$kohPL6MpF_E;EiegoV9^JUMYS*rINQdLZ1v@^CHKv&KO2eD#x8XCdHCWp^sOe2Y3F+AkjRKRjqX;5wmm-va-;qmu%D -W@E~mdujK=KT2tNEjv^yZ -1X-^fBh^5#|Up)0H6Z+aVED#xwjIG&X14ACnZ!%yc!3Fq#^DI3~p$shQVIyWdWNN9={fWld-Dg*{VcX -aXV(iz1z+mx^;bZXpvL91f;Y(`8;O|3=jt^$%vPV}!0IRGuTM3n*)J&ts<@9vTgtR~+<4KNMHEgZrp{ -g90>k5F_>N3v7tqHnIRhRS37G`+mS5<-U6--;D-}9Cj!2>$N;R*+2w0A(cCO0Qj9bcmF`az -U`5#i0Y8mq`La`q)B8(okcZ@Q}tvT=y+6f4|5*sHyx7lo-Y1E>Nec$FRoHj_~ -`23Ds^ -*%C;`L37OY5ZlcX&+Gg*(L|!C^O29Mnp(Emd8y|Ae)I3(;TE1bpo -SvLKk&j-kP20Q(M1Fpe_CI=kMd2$Ze!2R(kFTBdKoIQvA*CK>>u09UR%Fu4S-j;O(iHgKtjW-Pi{fim -i_Sj-8X+t4ioVQJj1Azt$mmw;q&wJmv4cDt~HpMTJUnzoU{lhi!KW=fj~XlDjU94KxbTRF|dh;2joY< -q%ASJ_TwaLy%Y_AdSP=oJZ1Cbpt?Gjr~w2zL6v7T0r=ilQwzIQV{PSi{NIV~zZ9#|TyEZQV*ImfC|B~U)8Y?P9~d3;ct2fz;^Hv1+|g*@T)34J%HI{`OU?_J -{iYyZqgiA{FYZskDBI_7 -M0yRq3SWu%WK&c`ZZV9qxW`2l&!hTYk$M{HPyoHAZnv@%bMH5(vs^cZ|n9lwS$lf7d!6&rtm)Iw&PfN -FVs$RXZX$ye!y{5+lr;Apj+un9^jGGv+}p3;$5C%b*ZN{b3{jLAS^DmMX-UBgD+6xims;xJs67F&<~k -DhsazUXuhg3{;-{Y$nF0DP)h>@6aWAK2mt$eR#RJ|zX2x(003kX000*N003}la4%nWWo~3|axY(BX>M -tBUtcb8d97DXkJ~m7z3W#H%At1ZXpjwGneM3sN9D9RkQ6 -F|Ka%MPu^X845rfJftF3WGJEsYh*i3PyEfhInDgk5Rjp!`F#*3DyWV5)OL~CJM;Y>rm< -}JGOWLv4TBzdDWqNvuXl7?XWlU;3kU5Yh{!UO|LrBF@Nd%4ymB*G3Rrqw&xC4E;)4=OV%m4iOQNr -^qupTWRoQ+t{0Z_yy|^#DbEqJGu8{ac1HJ}^7x!2!}>;>_4wVmtdqVTAKlI+$s=)TwrxB>AV-C-YTbK -do!Hi@s6{Pr3AnBMw$w<^^!6CV;TlpqgJ?JaK7h)JTd8~E>i`6Ad&VJx!E^!o -+tt;({POJ|KyhG926plP#rO>DTG`M7JOoa8zFa1ON>nM_M+(OzG#RU$W27zN8s&Zlnh;yp!iEG4V};ue -ZAxgg^Pvu6aH=WEGkg{9P=Wvc>of5~W`~T?S{mE2#70ZScOb*4RVPN8&wH2Q2y2J9Ie;)Ott^Dq^-H7 -c2A*^dq3&N#{^2iw1pLf$Wn&8vq^Z0$e+zppeRoh9soP*V2pY>mlAm`p`b0ASCSidJ>5bLs%*oMD&?k7eO|fVq1-iG((dV2)DTQY0in!6po6K~qqoC@f>7DdOrDmc3&^BHYXlDKzAq+;C^dY_~ -ylgEkwSVj%!u70y$e?N_+QkvNdW165eJhU`8Zk@_Q8J>5L8^W)xmEgiU8lPqC; -5H;sFqz!BXg9GG0f_|Bcr+maxyUtR7D12FasvD3Gw@G#i&P&WE*I)0W(6H%)Vjr@Py&n!;Be3{799wT -3Z>43Px=SYKH$jKiIg6oM2&P)|F+PR~%rR1k+&`=NK#Es8NL}$H^!`(ZpNfqT-!q)&*O;wDyBF3t7EwO8)6%mvxh#RE?&oAA!XxqPz{7DsjxRKaQGi?_?d5bfGMHf=*3=B -*J%o$hIUu?RBpC_EPaj?~!&v7s{<9w}pCB@e{jn>NKIRkC2(_f5XeI=20Q?@<+}<1Rar?nPS2q9% -amt-%3bZxWQJQm5-iW9T;WWP_^%&YWGh=483{jedU4o?mdztf4EQ3-|A2z18=GJ2C)(*3pSsCyN`2_n -FQ8JK6A3`V9-$Q323kcWOl4^Jgzkmv1&XrVwgYoVne|TD)zX*|cT@*VB!0oZq{V#o@^>*!SDSTb)Vj# -9~19=kK@7a?nUq@trB`-9XkhdOgUgGiQkYO-I(7`5qZwH=2hXmUTObd+XdB-#O*nJAtOUoadmaGtSR& -na`oag0bNzV_vG_AxCECbmV*7oX#8m5ti4uD-YlQCrXy?Ujo7>UX)~^{OOInY4qvfKZy -K)P)h>@6aWAK2mt$eR#T-cbkP4R0017n000#L003}la4%nWWo~3|axY|Qb98cVE^vA6ef@XaHnQmN{w -r{mcTXf&W}0;S@w9cjZtS)`P3@f6Y45%yE7Kw*Gp0x_N%>=Q`@g^W06+i)B{^}^-aS|6v@uCwFc=I5g -Tc(;fp}UhZp(CbQHv*^KK^_N|N8>}_oX;WOZa#p^Q{M455%){BJ)ZnVwoqh6!nD^dy9Ai|EMoR@rx|0 -w8+Ji=u^?h0zLJqH~1?+xGk1q9^Z<*sKv6903xl#G|i-tHxs$2MVgCAF<)e9oKK{5s=RPsOi$FJ9~&ogN;Xh~pRH>G9G2;pyS=5j=e__KyB8emXqb9|{R%f@b8+qD1@w$ -rMQ_6QtLPlnw&Zf)T0~a*|Hd3G^$UE#sLKv*JpYIdn%XWI0bO9LNgLO8`oi&eJ-s=}oIs(biV4*V{UU -H)m(myW;Fj0KqR2Y~OlU)c~#{9G<25@&420hK?PATJr29Yrlhd?#J;Yx9<)g1KYu*Ols21^OdIG51h1pZ!R@kGK?P53 -{*tuKoFA(A_trFoofjTX0~=`x>;&Y)zC5Tgt+9@`k72`qwt4F$Zx4(MKsId))P02k>pahBG%k=O$&WH -k&9pm-%|bO{kA(7Bw%b18B;fzgl4TY7=Bi&5PAEc#p|5_n@0Bg^reGf2RIsDk3N=qu>v9M~X1yr9WqC -XuxwaadPd03o3^p!d4I2y_~|dnT@NBr+uYt=)Q+!o=QE4L*s!p!Q9pxrIt3PUOlkOU9_P1W -62si3$#;=XF1F$cqJ;DD_{8H4i;1AiIZRua5;P&sR5QTjODP_I_TSt7AOkz2Y -^;B7g%QqBapN7zQrQZ>EE9pjGz8^@D!xc4?vizhCwGfkCY5XvL?mEA?s%#htEr3*GCG>MLo|Rgg|cT9 -*l%|0J9~Y&EpX)Arq8WTLOO5vY0DugrZKLKHzUJza5%~r*W2{AZXs~r<1yQI)u)<-;WnO5t}zBus%rO -;^xh%yr~b5t>@*!261}3@SYO4)t5O=MS2`;(LZV(#bPj|rb^SRo>ihZ6z_WT_-#@4Mj&F#q3F%i+

Wx=2OD|;*#H2`M4ua4B_?s -c**7)Vo;eWxZ`ThRk3+P=@MKZriOW;6|YyF<}6-d~7BGAD@h096 -USNJ2@D`YJPCE_wxbH$)bWaD3c(%aAJ66b5=W}bQ1S}jL|aG4FMi&8cKH7e_K}!}`#?VCw_cIKSoexDo@5f -}4#MD={$|Bs>$kg5)+iqE(WP=J>t&gf`iJnabXJ{@H7!shfHmubsckjW-9~`qOl+e2??LisC*L_V2Q5 -REi!!Zb^H*GTjKhH`qI9*tbOWhmc}3Ewb3n!CTpQGm3+R`rd0mv373)4&y}XbaSAOy1!87s2P<$zVZH -k0pG_8lKpU~HEP28RUo8%yULe*irMBk6#`<*z_Xr?d^%M`UCUezvm`Uw6NwTex~p -+?oj@01I_2Y>D#o}9ioc36~0C^Q(+4UA^Ql0iI&DGn+$(P>iTIHVoK6#Y`-0wog==&C_2)M33iJvcgr -=U)z9oB%@4_fAg_UK{~BrHqg{006I^Wv~D3=+!?4Z?<0#`qB2_b${@!*oJo}r_aRT+y1N9N%YC9ulKQ~zFu?h5`g|=Rc!sr5~udhEDyxJMR=_@e*tybv4|LGA5-#N{azI_ -f_@C?`(#_9S(<^lvmiQBCi;kgM`hd;o`Hpra-R!q8HBG1z}*Ltl|J$fJ`%`&Q96>{#q3N68^Flf(pt9 -%c1wd&G;i3WgqoMzU0)F@z$6zxr2f4a;v=gk-}!i -{wCX9Kq3;)nFUZdwWQLpqd#io&uD+>enQfonSBNm(|97;EN7V;uz@m4H14AP6W`7LG|=$7DSs#;J@|Qr^Uqn&t*u$%q@YK_w-tWyKZ%gh1e`Vp&d5Bwob1(;Lsq0^e(D2 -c`opf=N1^O5#k$hEYZcEmKBC$icEz!E3A$Ps*Y~lTlWui%j)LW0KYc$rzUUjR_@%g%`~*Xlk;Xs6gCA -14W=cXbdfiCCu9*SK^#;CGcz=gAuN|Q$c-$AhFJ2lpR@lx(*_M3Q%EL?L^CJM`{4?^w9zbRRsVF{-fO2pS&{b=Qb;?D3W;q7vUvgm8sZ8tI`vNM1AtilYX`1 -pH|TYN6j;#BR#n#PIYfCX3{YR23i>b-GArgY-{l&r-`Fq7D%1DkU9%}B<;)7Gi%o-1Sy)f-MKmU2fk81B5po)zwEbjq)Z+X7V>t#=K;!Aq8RY3D;pB|OQ -k9xi^zZWw35$NsjKDq1!My04s>8J2`e8QimGJ(CP`Sd+KDoqh|LaT+7mnR2mQ);zhZxf3Jumnt7RS6% -l5`T>6@xO|uD|_^4BZ-Q$)!ZIDI(^8BWkS=B3O&U~#zuEV-hjM5FEoq;ezalCkPfDR_(A5Ug;O9spN| -)~dAq!tL$Sl}eiqO1@uy#Y<^u2@$`}anp2RU0{FgW()w)u8IzpWTzj9$l^=EkI&vW4Cw4wXY^r%1O3G -74l$Dh@sjVCVWStc_zKF{@|`kFdp`aEsMw2Z5T#ZoWSqZ&UpLg;#q9iop@y2HdVQ2a7UfxdX2Mj%eiq -m4OierS#F4Dcr)epr_3e9jG~ecFlH;MT -`u?+e)ZgEKX0ug!N~gUS@s&gwtj`!Tv!b|MF2qGDOPt -@gyfUB}#B5Tk(e}1ls?xHmc|8K$p!0mb?HFW?%vX9|vmEhRu1plQ5GM*J$z4gt*-PJcTwfZ(trJQI>s12Pvg(nBP3CA2_sSyDLkt!9a-nbu_6MfBcU -!xIY5m%M;*H~}Rm?_WYlnw%E{dao;FIe$4MCDT57Iali*~bNI0#wjk>qlvBZ4_C3(zXZ1^(~!W>=D1v>^B=1oVjgKTDRlv76_hH(eWG@4NHV -nNOxouI{+(c|dx=bwHi`cGk^4Rlj&VQ&QcsnxCblIcpG8({Av(?b*=X;b1(KxU)SbYr0(2Js7N>R}5rw9i#_w_thyRM*|p}{l&>t&q2%3{=(_oHoGyxiY5Am>OV3=WjEkm2Y-OpKpBl?1TA -fn%BusN--)z2Xlche#3OH{?u$_%zU2INDZH3<0Jkb|)n%)3CVS84TXn!Ng*%)Es3{O)##^h~e!0pD+Ode9&LMivryVgh8`ubpngM6f -E)($*Ue7cC5G~Sp(%V@jn3po3r2@Zd3$= -NRE0<2)lCNijh)QdHu%-qTg}@4&)F^L;)cRG}+7tvGa_)wq*5eyIJ61Y(S>pxGTk_aduD^Pz+w5TZ&Z -}PFq?FtZhl2bJ(UZAxFx=#%lxnX%TclFs!09Bly1qJ=3RSnUT?@r2?3^nk4L51}`3nIMU%B6hPh9y7L -;2;b=MReqGkk_tr*Snx=n$7D|1*ZDH4u7;>^c5{BSfIABgc<9Ku+6gd_dJH6B?Q -=dvX604o_NUMiv+_zli@UX$#a3~v{K8=}VMJ2OaT*$S3b=CTqqRDXKoDv;>(y#HPIiJ~V1XuG8a)~`-)X(mQ_ -dp86o~}QJFA*u5AaCuaDV*l@Fz$0vrkSe;GkCl!PMFK*H*+H9$f+G%eq)lz=#g9d&FLR$s?%wr~yRrB -j4ch%L*Q2R#JSWcfogdQZU=+W(&uCSu@|69hy1cY`2W+s7KiBS=ITOZW29-x}^ZaJ1T0~nB0wW>u&nZ -Zn%`N*SfWuh;Wn%kmAgtIn6ewYQ3AUz8HV;mtMq7Wlr5JwDSbLF_x1Ho=-Yx3nRG5v>9gt+Nas^c3Cx -!ShT39uGMtjSFy<`zg^Gb+wS0z2Y9EpeMQ}WOaF?y($F#P;w^l8UeR?4A_3zmwIR!G&o%KQ}-68!3ermyyTh=m?HMSk;;LCJ=t* -r&xuRWkvWAk$t0>^I9N($E@b;Bo@P%Et?r5(EVukg&|4IZD!qS3L@w2L9xTOavDgbF6L-Ez}6onPqm1 -LuflNvO2?OX9P|Q+N-$4!!iq@upjQ)T^+jB-ixEG!vr`*K5%Q>hWBB6-4?vnn_tBi>$kuN8+mRmW>n} -tJd3*(Qf(7QR`s>hMo-ylcg#4MZ=jx9V$-$H6qOe4V=5EWCQxV^lT6SWV~aX67{_e!23FtUrC|}nZrD -tP)(qo`qHSH?qGnI8HEN1B`D2*02So)yD-Ey_<5d8imBeh(Oso3MO8I9ZX22Bqo&*g+0wOM!^=7nX;k -uT-Z8|Cf^Y=d^jYB>T??>YRqR}T*xb=z29@on*A14`pA;o -8|8oq|2TZmEmL_+*K -gC?I4LEDB&7m62zH3D}bHqAV7&tZ%ne#y2YwksfQ6bLeZ#0>#$DEUqdaK)*PDD<}0})UXGKqnn-s{(J -Ue+23?kHdHXcsxiJrLL8(2F(zCZqV;hSXIW)AEC}0RVT_(AM=M!dfY7{gHXfrgHy)p%zxi~@#uS1rn; -`1zG|PzWrQE^RloJZ08QDE)%vU&9#-Mwp0K8wxY&sNEHBa=<%om;S=+&#e{PxWdlOesK33OEC(y4-gq -VbgMrn>_Eq)@)`I8+D=`1s6ZgAR-g{O>xKWfxwUl!>wpy)qcVA5K|x#K!~0^S9b!jW`cQkLdL7HZ~y`uV>X}jw~-#2S610K@J&4;XSJ&041j-t&zT#^AJ`6uGyLexo##3Rw;h@ucZd` -x?2qG5@XTOA$35tVl`J#4Gs}weI5XWh&iITSTE4f`;KXQNL3T+SC;hmbQM@AgSH-n<7*-Z*s(FUd#zm -GUbO6SjYh`|`U~xdsKT{G{3C&PB=Hu^fp$W3|ZH7{~pO!$XSX!@-^Y5n-%xz2TBcTY8rn1T*I4~zD7| -@X24prO+GGCKk4;}hcRFDb?u<)zgf#9zHSX^U55%TS>(t4>9DPB`VhjtD$De45~HRI!63`Tb>qXbT$R -MLRIudoz0|&3ZO37Fch7EMycpqYWQAh06ngKp3>O<; -P|=0MWPRDfKJ2St1aT|oV)Sy)=S75>P;-!KSjSadu!5>I&|(Y!+PIm8lc_CItqPEb`)zlElSk~ysn0n --Po{waq#{fFr+Cr#uu{}wRPL!U0jz!PePREBe<%SA3>y!qj%pU8XInjOevlc#Ml&?^Jy`f2hVbdhGyJ -DTpZcK}KI0prbiG_3a)N_Anu(hc^jwbqP5XngGO8Qm5>_% -j6MBfhZlR+o<-!Qa7<+I+(uP^AiwZ7y?6o@!L;cswWs8~PbI=&k1%jL8o6M!m{zOcE9Ua=FH`==o`qL+_lOwz$u -DWndTpBrZK5UtjR4nFhqv@Z&x$xPK2+;w`YNncS!a>g7_Pu;sUo;s%-qS?!tq6n$t)eF$Mjk9W->ZEJ -SMBk*@(lp4j&z>R0X^fXvH}TleMoyCPu+#nk3Z`S{r2YoS$^#ryY9CMVwgeR0VmaA$t8n(F9QB*muh8 -byuxw5JvZ9o}y=g#%|9MiWf1ax0g4tIH3rdT)@ChPyrRnC60fj{$djBrl`yTD*5#3Lbl#dh6OD(G54b -S(=8zDiTYkt5q2SZTcmkk7aocpWhlm(W?;z-NNVRfc0n$ya-5~-3T)r%gXnC;WHat)$mBjY&;|{q)2Y -8nb1guGl~;l~?7T5mBn2wl02GnNAsAMFY=RDhIU!vOk0#U_uq~^qPvvl1PPM_7^Y}m<9iJZXJV_H7E7 -cC93$m69Q`_E#DYL!J?w~0uXtf7~e`pv$D`JW{I&q2LdwwX+InPYeCs -Y}-*VdQn+jL-q>is04o?lU`_3=AzUQo4DrK(n?u7-4X4z4W2!>fNz|67b5(Bl)nvyS21L{dDlh#5CGd -?^v);rstRlideHu -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 -CnDEU>F(Db2bZ3jr^Oh~9^=;_^x#DF -jo*!ZS3niuoNJ-zv<_gb*906tW?|q2C&0ekq0kDFZJp6}a3Wb!W5-z%bxQ96-v$N-XMDk?X!iiocbLF -dzrZyzNv`Gv3B$mm_${$02U6HEUib95A9*7-e*oKXUY46{v_rCp7AhyOd(R6g<&ac2WQr(2n21WDeS+ -1G#pFPG!OH~*0W6l?!m%(Gyd8EWyEWKa&x?87<>|f7VO6RoS!NF63@MD-0p13^6OFLgWDjK=-^OV^!! -45_lF$!QHY!>9a6-Zk5n~_3-#$nT{VKnS>EJy{BRWiQi@+QB?0!_1^Ei0&^)4d;9z*A1nA|MR=Vd@5l -02m0emYT`y4dqkw1%-9kUB+esDKM`06?K2p(3XRir89a`3A8VuzZ0Z#Ff#*;rt3aX-M-!ffj^-)5AydF$i-|Mo$W&^wiW0u~Y}-K<6 -VAad4{3`OuJUB!<6ONW4}!9W(&-Ii@q7bB}xR6CT~G8VH%N;`yM9z$m -4N3yF8j%vwfyir|Mdy>aW^N7yj}qXRTIkHHK?dXn`_k-w!UbDeV# -P2bk|I;4_p3JGj2HX|F`JM2WKdb(FYzy`8t5Z=I8mVJe^Mo>bE$3_{HeTF*I|Ss -js9M;~%1HRzvTAFU4coklyF;*GW#l`D=BMQ4yt#;UDSZG#l~v8DFo^_rh%knaxW;(jf`SP!NmJQ^A1_ -Ghm$zi67Sss)eZ@y@{$QHA>do6v_@&cAx<#0USIRFAB^#ozDa(Hw~9HvX2C(M=7$kFVm7SmEp -@nVpl$n}LYw;2&y2&gL^35Pm<$m8ZVE64wqc7Jopst5Bkk`E6Od1Il5tl=C&Ufc;j}NmdsTTZfj;0F-MG -^7u*~|q$9;WuZSWUs(GdN~ln-xsm6g_}-cRHbO`CT>wVkBhKz++y6LHj!X$Y`;^Usu!A0R17ljokjFV -s)o_`K9La!yj4hz@0`nF=7I6O5MQUe>Z=^^20TDP`%1i^#s=JZ33qcToAQ~{$>_J6&qk7jW~6R#arKk4z>0X9-^Mh(CI6nKaB47V#A-pj#|v3 -5Uc$s4iwP*NQg;EDMU-9fOT8W~h^4(WNp(hiL1Sp1C8K;CYpC;?O+&boAIK{j2Y!+P-k?t}(|mM8I4z -Q^1i0mS{ILS|&JA2icAmgV=p*plNHz}DB4YxvDZ0LQnxW;Ym^N+aZ=)Gcgso1spNX*3a`Im+MsTWHDfx|;9i@Jx&-0>K(26~4z#va-v^ -9HFI{i(5UuJCB4?7C~{c$`%LQH+r`=$hKouA -G%M{{+>~3DCL -qk8>fq2aDsg{RE14(Y5q%iLc{;&Mq(~zcDanC}qa@>$VG&0+WV{i7Oh_Asc8PJ%*HH;i_f{x1>r>;7;SkkK -jACfp1rB6r%^epcc5oF}ViqLwLl{r{<21$V$ReHyf^2szp!z+)Cdn%-+JAWRH1wV -<=T+%C{uI~3md;PvJRe{AlVYN>dw16ogPZBIiaU||H@gq`4A|0V(0eBIzqE@e*rAZYUbYG+ITGx8f4C -Kgh2s4i+4WZGqF88R@aS2dutRCV+WloZj2J$M-SXMb-%#in?!afWaG*&WxB3>A7chIPXm?2DJKQ2cRl -*21jF%Veohx9P`LpONd4PJDE7Fw{7fdXs2#a06|hV^=M70DAGP0@Ok%<5nfA+5%o6@&>iU9EebY;nOmSj0TCR#yiyTT`g%ohK#G7tau9RCj+0i4 -@#%&yUTt>$Fcd=m+69rGxym~5ZsnD3b%!8Xap3#Oaps3VX~V^nb&$ZWBJz*aI|>o0RsOv!m -t|P7p8UvO5mXE5~u-0KBur?QpR6sQrSFNqmOuzn$U2BjBFO$@~p;9Myk*hrej2=fea0WShDCzCJSv&j -H{0p6z;_J^+Ia~Z-S$Y4Zb#m=r&KrCEjvieL1rL`YNNlOuE-flTlT|#*m|p@_eYwm^|GTYXQa-&f*>; -1-=ko;4h$t{#tnL7}9-TY^#d8BHB?c+?2qXd7sxdg~4#amq1Zr&;4e=x(oPX%GdZ1x$ZD7q^Y4^ifG? -q!x-hM17JjI%zJ>K7CoIXZmE`la0Gb53LY?9Ji3-wa_4uUTyTK*FOyj& -Zo>|=8eSAlXDGjn5Gqzai#OhPIZ5!w7+wP8b*Z;(5^*hgV9)V;Bb;%Ac2voUiGkgMnyoFB$S2rzGLt3 -rJY4v0{}_`GNbpX)we$Ug0<&OHl2tXFbP0kS_TG2q{2I5WH3b{A{d`n>$S41zFXBg)|OF`QvSoSVJR$ -LP6~!rx9S_`Eg-_=J+Vc2z6gz?o7eMDc@3tJ5*&3azK>lO?*xXGwaes!XHe<|v!ZD@8zEdKMRagxPPs -`0RPhW*diS2Rss{-+GzRA{0hYxCQ{I5S`fa8o`$4z-i^8M$0ht*5Jh5Z8?ia=)=U)ssA93E%$}?Mqzi2XW46*&kS`1%aLnwuQzy&y5TUcsobqwfJmZ3?5!W -T&ArA&X{7^4sDGaN2ERjo#TXhLc%U$$WOPSSmk>6!(SHLGz^A43=WY}(*TKa|$tZQvSpS1UU%BJ(s-A -15>%(nirr~1`1#jXk3?WBi -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 -TNCaS0MFgfb`6>KycKvDkz@d< -N6McoAq(Dj+`7(wxpKi_Td;VrY|kFpI;udUGXmu(_tlmW6f=$!rNY|^jg>yY%3ah=1C2tLR%MYOLxJC -=p%=$!5u(^9ONM4qrRAwBn#**2^C3t}{FlDBTyv`!)n(OnKEJvaA|wH06||0`yGcIXsDh_EDNH**s^; -jljl!hzv^8hgBgK*3wQ%>5j&?#Qd(O^-I;%m?N&tf=0U7|MQz*q3eo{L_eNPEOnR1Lo#+Y=BI&DXb(k -Sc;Yiyxu2AMtD~(+*3bqum=5OHluavpS8Blt3WFv{ --0Hewn-cM1?e|e%)8tt#IJS9IJ|KDzm!)Pwh2+dk*b(gHkh8zdYQW^tbGt7tB9k@#guPHh --MzNx6Xo|L0<(#!JqAE7saSW&rr5(&nj2$1-bzJF8tKk@~q?0|$F(F)T{@2;*39Pdsf#c}EQ~-$;(A6 -}^W_}i=yce`~w^4bH1PnhAq4<*)0RG--{pww=HFG(7h_=rY6-CoFnev$S*NO`(E8~=iUR+1;(8)t0y5 -MHlSh4S;m@)j&lZG@6qPR}R;iq2zV4T_)qT9C}ed+*+_i#Y4l7Yyp0_+VQnH2Pnb#M_FwakV{FkXZ4M -s#z4rOCL*p9@~PSvKt>GQPZYoM<|KVho*{pP650>{e1oB+Jv#DO5lB)J{Y$T;arHczZ29rAOu8-yRD= -c$|-_=*sZX9OLYin30QP`tuD=R_(W1-GIXd!9M2t!kV}YUt?H3O6JsAE*nBOdI4qv!)T~N0vWSPj7^7 -7sz&UKz>dVQ{B~R3o@8hn()uyePikVm9G$6zXx!Z4!}W(sQ3>No4uvSZR$@0wd6n8m<&L| -x6%zI074xs2mZO#1dKHW-w-3|w4@Qgwe%i}D*8n}dmFhgI%L>*)OOt$`?Uc)Q3_xvF5ldfw{FD)8$?% -aGJXU_T+D>U$-JH7(m)>e6q6cTAEQNbv6uvimG-BM;FvE|N9kL-WVRHLY1!Pm|y4BQr=+u1@ -n>WY$;P2Jd-{_eA<9$LGsJF=&&w<2>yA-O$G&PCXZJE_1p7m0CID-7FOK=K`MQ1*6xVE`;qYLn=B`!Z -p<2)|+*H!%$3*UYJahkr&#|sg!%FN5e+8C(s(Gi~=0!akVdMY)x -ZH@KVdpU0-Gb!W3GCXy{cq1N5dE$)9sTL5M`NBGVws=r -)&K<#{X@qfAD{fLJdx^W!&cw+p@aM6dbmnv{6Z+8teut7nThLBR}><7O#-9M29uN9Wskj&pcMF^7Bi; -AmoUkCVX9^g1`}!kFMfY$J0kl0%~a~RnME$E~=WXsNC5Jwzs!8cXl>+;S)UB+1Y;S^w88=Gq^fE^Ff< -5$dX_&8{uA?ulgy>*@jiPW@d}Yy!L{f9hVs3q0z&}NvFEoFPl#1jdGst=4YLA$m!cUR@qhInS1UEYsz -?d-YELPyPn0I)-t^{xP;ZyP~LEQ?PBTJV_kugMyaVNZVI8tfw8CDh&q_-ahq&x(avjgyCJ$kBP8KJwT -2_5%@N6LTN?txF%lz$VAOjV@`nSvlS)`T>T_0vt%l>}YaJn8cm*NR%NX_fypka@D>^CV+S8>$G0+PRM -yRUMRqMh(u?$!G0^vTERzl>rEfV)BT`QAeH>*S{Qp(|YlZe&!(xA# -s@EziVt=WiUi!I+0JgjBJQq>GcD#gC(*>LyW9FkDv$n~MOagVR74&7AI=t&NMCv+X>7p@UwA*&DB3vZ -?Bk&R>pfpb46sKW`y}9Y2qkyGs(>l)3BXGKExd!Q+Z$6j!@awUi9Q!v?qX?y!+6+D~_X4_gmXc)H4k( -5{bA=8nX8f>fK}gn&?*sK3gg#F%Z<(wGFOz*Feun=z{LK8J_!|GAP!i43#qx88(UPo&GN0MtO= -(Y1{;F3?Q{`eze!u>Rmx87y+Ng(A%FU3az*Swq`1-yqPeBvVhow)Dy`414c;KsKdQ}$V^^O&*RbRRGB -cL1AUOllYV;|R$ti{cSVK3CEXMiWo5;!Kl!ce -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 -E>|TJx&- -Yq0ax*Kb>EcvOA0y|dNDYTe-LufE#-8vgEl{iiQ?zxwj4uMMI4@TYll8>L7%GHuLYfHztK51i(KL~VY -Kj~b@jz~bW|(6?enGhAlaxt?BJ=38f1b~kD>OfQzUdAZBp&C=5I%DW@k0tW)NcD|Lno-3081kFWC!== -0yD%f(1WO>1MCD$(DT?$!kUKiqdPVXbYz02qd3Q)k}btHUaiQ9R5UcOR-q7f48P-dC=VK3a;-r4Q-dY -vG|Y)62nV5`&RYva9bx+mUq^6c9DgfP+)^wzp(`;tkm)pjj%x-Vb4vx%yfS|aXh&nlg;ZDw@U@Au*iZ -MXU~Dg8Vny~en2IP!Z8OX>Uy<-?jim>uI4%DvZeov9GiN{6WDj%lYWLWMIbVhZ*NO9%#%%+ORkjWCr28l4!MvxrlrVErSXQ}clQ94E^WrIgb~5Dk -Y#e49{gEY!6|AGJnKFy=hol9%gt@ -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 -Fm%^_Gp8W<KNl-XY+yS@l8B!mL{X566f5KV; -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 -#;!)To`D>#8EpDV(TuFD5Dt9py*t -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 -FoYs@4$WPUq@@c{Xn{q7FU -PA)5pRLvS7xk~kxXS1@Mpc4L1TRO!%!=w5qsRiZ3SMw_MRBVVrFYSyGBRld7QK)`)@&)9OSAV7m=d-Vi4xy&9K>-pJ;dMT(WlB4`3oyBICG$V=HV5SGEM~dh81YEku`?n&O&mtkut=A~_VUhDI9$!cvl}v=OIfp4TVx@PV9;3=gK1W0x)c@YZ5e-CnU_dR@*{l(Zjg^Q}D|JlUEvQ5?-fNhZ?!XVe;!l&J -9OteEl)%Cvy_h=fuf2~(gpQLx8}g;Dx3=9gOXo364V&ca03`1#5Ef;flP&?7m4=+%D%o|X)M`%#D7H! -dtTZ0mAQfIFkCK$c*%58gvL%9z+Ax^{Msq*7OV$#fczlc|Z5|cbRCW?|V)T1y -IY&jGe)`&*j5f)%&@>ue>k;5SfIQNB7p)0MVgh5wC68`Ku%Alh~GNM>D{TA{K38(cxTDn^!vEgGxFwS -|jL}HN!S>#-12PpCgaccUAO|=|XT=7r|VVsNd@;4x*nDP6UiWY^-7^Ftx76BXmCI|Z<=yf^ohalj?ANwVrMxxmNM{TCYJ-}Iln|(g0 -(nZy3uSUbgX37?(THS>OwzNNi}r;!bDN&t$k?)Lr7=np>~dm&Sl4N%^tLTNdwx(T%OT1*^>!KK=t8)J -XA5%!zur173{I0#8`~5ItBhiUKzmIal2 -hzn#25d#LH#%q%dzY7pVld56;;@(=*m}QTy9#fU+qAqH&@hjLRzSMmp1AsEu)681Y=(l&Q@T6KWStUVN_LV2P%;@9L!so3=xsXfOPu^sL7 -!nmypUaIQlVfB74w?~1*2ZE*2rwHFiVDGz+iP$MFV!Ls1GA5T7=pz9w#%3C^J!hj!rfwK;qH`@_Ezu+ -Y&mhi@$p+ty6s8W>PyoHXd7Tbk-^)VKq}RwDnjEz>-sUJRi+i!KBXo1LfyLQ4P|f!y26##tBLvDoFSV -3e^k)Us^vZcae+K#hyhTa{Qq>Pb>0L9p@Z(geJ(3<*KnNu3>8UWcG3j16PQJWAm&(f+d0tQLJ2f;KQ2 -d25nAr1|R48?GKj6LUEP7v7m0q+}q-qB!IKUB1pYl0yTr~)Ga3+5{y!9jLPo8Blz#z-Q9z4zui^#Acf -eFuH=tYk_k!eu#Sb=$Ag2l_T*Qb%JAO -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 -xo$mbKHBk%NGhni%jbieDWlktOVM -tdC8V%Q+h#u(UNfJwZFjg|AUN;0gMIB6E+$o0-6v0CxuSVwXZi1|9L0BlOI(hX1s0=uWI|c$n;gu3$prGgU+Q|<#?>qH#TlTYx94K9ZZO24;P#L>m -g%xY(oQ!^QpNHdHbRYnLX%AkkrH&-s_E|*`~xb)p|8JvV;aL!Q_-McPFn`N&99{WNQ2KC3CqV$W{?1b -v+WzUtYaryN(xb!vu3?*3w#1skQ1svJt-z^}Ba+zx^T-6~^Krdarjsy7JxV;wv&!YSkOmYu=uEW4bZb -(x2{v&lw}%$PL$mz^sn_pzN*-t`?9cdiRTe{1t;U-Bf*wurGxi^u)0ySykcu@PISBe1}$uU}JsS?s8twK7I4ZY_P0!yIGsx?f8-(G(RDn+g6~zSAh-^Co4yU0C -Rp3Y{)4G^Ik*l0v^lpW^v*kq`QS9QY -e1DG|+POY^%Lfp-Z%tx)xd6LAN4@ZaHqeN@o9h5=f}hzM(6>xbXikzxfk=r48sM2@P23awktXd8PU3; -(#gL$_hC1Zw!@5dj4!Y+aFykI*`(j-UX6St(A -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 -u(t!=A1A*l(rrN~{}N%1BJ -p~=uYhXb;;RE_b^s#vf&RYsYz;kFWKo=*h~0I*eEQQ$650<}c)&*TwV5{U|eAFd_U7P>UKM`7G62Ic! -XRoWZ(ll!gqgZ5|1=U;reD74jK70OnDFFdSr1^y1TRtV3(nyJ$BV$SYTxoUynP{O={EO9mFt<=MOVX) -I>ce8GgQO=W8;28TZh2ghEPul@jsRLwEG`O*i3kcfHO_oF&s(SRV<57on@;)I)$YwiuER03KESO#BsZ -W*?hk3#loBLaI#JQFLEl_VkF-EGuj*Vmyk0jUG&P(sAe -Ji)E^qHz`0Y?=ZPq`a(od3E_w7?h#562Q`Q5F&Ar@{q2y)*x1?;QuJ8=smJ3CeZRM9XHx6RU=xnPLCze1>M+FdUnq2zJN$_#zo -%b|!dmDfke}3-YmVd2q}~bHGFmc+72#Eqj(>Xx7?#9XZCg8OmX_oeR-`k(seuJ~k(#adJAJQrF;U>-D -X9hHtNdsl|C9ByVPN2W5=MeVFM00v|_A%xIYSk -BjbAk2b8!q+I$2Bc_Ezs-kX+l3{K-%n!EWNX_%g)Yap>1`v|1B>_;gJ@C(iQk26&dB;mGsZCd?1Gu2p?KnsFbCerv>kx;v$<;0r$TB?=incqluD2~7amA(`A -1lJ|2qu1|^s%Fao{)Up!?IeQ*8ApzBSKB~;wI$0BKW^cx-h?E?6#8U4shRyzo_DaY=r}%*n**iX+YAV -uQZTToh?4Vtfr)emy21k1Z>4wa}~8O3st)XJptKcSrZ&zqA}%_#qUbPs -M~KFIc_hFfZ>^ikNel#j+U+cBvbmBk5Lpx9ku#eUU%@wNvVjn89|aC~%IoC?}HkWH#AniN&*XM>F0b* -P(g*zS&cKIgI&E{yDSj&Uyed^QhCNK<0svWiiQlHjjo$Kq^o| -2XXs^OgwC1chhK5I&(Mt!)WaTCNd#?;EzPq78C*Fj}GaF|E*TSnkZ;Wlq4*!!ifXLOs&^kEut3J|^F! -{lOvL`bn0sxFYXx$o#f?V0cvsIF0jw78 -vDmbpR{Es?wMvq1KWcg9t^F&PX&3Q_)ZN??jB`Ztoq-v3t;`>!PSUrFo_mc$}VO*@N%9kN%cWK*r?Z%jZ8lQ)b8cc{+jFhM9CQU;X|2-IsgGqy3kUp!UJbeE<(%ULEYdOupOu -arfKj&ywel4)Cvnw9p}en?dr9beDc@Qp|PRe2t$ffRk}PvQ_X0)%cbYP+I?hcjPWmZB(_>z$HjMHL9Y -(5#lU7`b7ZgTa6!R#Pokr9fluN*fxPrDjv&&F@taoCN-W`GnEO#jO1Xs-R~dgBYfDwpAvPZc-T1)N>IRCQy$@xZSccD>Y`< -mzKOta2R;qRZ9r8`s2dR2jce{VM4g*4Obt1OEX~Rpp`F^x9O9x%Eh0HLhmzjymW)y4>`-+w_Nct1$Gd -PK+Ku*QM-oSRKE7x*nqA`#m2UpSl5kQHG1}_adkWDu#&ow19^%B52?)>*q4?TLCfNOw__l?8;00rI+| -??@*1Ls(ZNEpaD4a8BjicHry~D#PLIrBM1s=h6vgyAm~8^L8em$6iM|fsF4C?12Y6jO#djimC*U5n12 -BKgFFD8M|re}r@OhDQb~{f-^bqH<^k|<6OqB-z-n_g=_?KOf3xYB-9clWAlQc0xH1-vq -iGDX2eb^{Le#mQ>!udAwYD7@RtqU5Jo1 -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 -Ymg{)TiMYU{BBYW{46R-e!nDGl -oq&29pyV9|?MXb2oKEwqYSEM`=c4S9iyBN!K;m1Z6dy_z6T-Qa=<9M_f7o`m>ROPtq$qTr%KvDB^A{R -u(^!0Ty0U`q%ZMLXY$aqwu~24iaXx57!I#1nRO7%VEF6W)%!pDKn(Hv7%no$B5OD9o-=UMW3jSp5-6< -v-9T^tCljw^~2PNK?57H#NF7z_K}~$G#1Wx<(?RTh2F%p=SacciWeVL6uAbep(n8Pi?&iI^4QwpWGU? -0GIb0<-bMmiQ^U8f0}OCp5%bcCE{lRyt%M%cU}RKF9NMnd0$_z2StOvvMKQ&JmiNraQl_kRJhz<1a*m -a@HA>HhlQi^+Yz#zOTJZah@Ob%nJ|$g4g{6aTqv!aai&D<-qSwK$x8eOo&yVO@ZJ=W7q8A>ariypcbM -yXrK0ckHe}oZpbktW7FnjZ$&$)GOM%OnMGY@O6(&l00B0b|hkr6wNut6MQ>DV_Gj6KFnkWkjJk{NyL+HXD087Kj+YhKdZ8lHV5gqZWrlOK;G%DWE`!BkiXqnIm+ -alq;w8?I__`J9YNbif7Z(+|^>4t-qj+S?{?P;}c-KA%b+Zd?Qui3GJ@ly0WG{Gc9Yv-5H95!49n5)ca -M1c6?_;D?ZVzAB_9XK_iNylL2#JNwovpi7Gmwf}NZXTwQG?Y2aVBm1IA&SN@GB -A<8Ha**&a{6_UZG4VGyEK^#0%#OTx_$Nk!Z}lFQk?$b_-)qd&Cxy&ILJ3&PDs2civ96U6sc_HU-^`JjjY!#};jDgN`*X{DU}?(EoQQ!;j -1K_GLKLI*;;-biYPQiKGQK~MhhUnnbop*XM|zy79ld0Sh?x-#B_1Q0TV9`sIsdNatCvgOb`^7VM90W~ -{yyZe{g-ATGTJ=0T(Tk9KEMV{K5XSem40%39ns(9GEzv(3wMJ`fcCOgUfz|_dWmg?(AaeEHFC^O|<^O -rdY2fb`MZDEwMtX5GIcE?W69mM(_pc8kd_17cH(P=>8-S!CR0FdRnP}2pWaoJMi)S}5TctPe-R4~A^; -QFJ`TQ|3NU>-Afzjh}tb`)L*DLZJNFF=M5o|Z`r1Ms4liWCh?3OM-1%}HT72nu$M?ux7|T~!JqtKk1~ ->(=dCx0AEM_#_=9=hH}G3yW# -CLCUEM$H-x|3K#4INOZfoNuX$W)Nac3@*Ko>zHWV+jr=qh4K|UMAOfsORyXcc9qq)Y{a!C4`!U2ETLh -bdL8cZ&B_!0SsW8h1189zt0F5qPRYQuH@oP!X3iqofYryi -I#yyfE?8AWMZtbUxmi5KkN&52~DCXv)imf>Uj;SK&<$c`eqkzz{c#9|{}PxEZk(8PKCKDaq`ubalO5E -pW1uiVlut-88JbD-p#wi_{z4B@KKFrQ!?3h#Z -|^Y9hLBKz$BZ=;%a_U=HJ -t28_=SNXIx8c1AR4bQMISZ!`S!6NOsKFQ=)t?rrF#6c%Dl(&YHSQiPC%D#Sr&tKc}rb&ARACX8aAkq_ -RRf4XN$p#s5Sa4wAnUKq7|dzL#6$mNcoNsX#ibiq5rIy_Mi@GIvg%gBsn;M@>`m4466;(xcd*imot~k -)_Ho(f*whXA&6Y@5g3=$H$fSX1juXAab9mdZI@gsLn`|hgmmem$*K2>?WL*8sEUANp&`r&W=)utx;`o -TZmu&u};lUx?`LrHmUDrIYthqbYfJ|C@MV+&=J*29*-rT9CnkKMBuF7OgUR|ZklF$zMilDgXt=vZ<9; -XxF3vP(u`))2zA5IrSYq>vP&pz4+!5Zyksf!z?N;4%uJNLu*eKcAUkL_99~EhPa4VRD6J!vW>_osY -t?Cfz=bUSk5b%2Eo#^O;nBw)|ALbJ|<8VW?;>8CJy+5iH;^5PQna`$LfS5+Yrxu%^L8If#(}nBrb%l0 -~0%Q;r^!00K0`8GC*F)Gp-kbDvR2bScP(MfM#3clWpC#2#Jeb9aLHES*>a=ZW6L$u`!g>jX#d?bh5mg -PO_IEJG^^|Ieo>~SMZDp9uqFYn4^Gl`DXIZrsA+DP{*|L8Xs2F?FnA%w4kX0nu~WEN#`UVU~Is(M53J -dB`U8+l?K8wyk_`%#;<1;Z!F@ZC;T8V&CgFU{u`k?r*x+#cF8>Ffo^j&&(!2WE!VgGJnwhkSZaz&ql~ -0(<@KM)`IGYw9;R=^LX0o(#+c%AFb2%#vnG=(j8~~G8K)$#p=duk9iSPFjP4BfBX-}9F9Q7~hjcRS*G -XgzUqQ?68*GVP^iQC<1ja*dVsvdt6?8yVo-)jxFh%8IYJ4%FVRkvt>P9nK%AVj7DnVg0^HSRsHPFp;l -!aLZ0iTjbqnD7Wt8#Og>IR&d%>W!L0ufqD;yT7R9`=7pSs+0gZUCNTBeE8PVK5-Rp8ZOz_=x74f~H~| -*IXN98q_-2r6(FqK-TfGTX05OiQ~f$ -Kmt)VYz>HPfSL6$n9K#7loTF$4wDT%k@O1|xPwi|2fvq?y`8J(~mviP7umUG#!8FB~9c#arf?o6Kw4a -Y{Dqcy!>-x1uk8BOTLJR8o@e?Z#0Zw}(Np^R)H|{47x3+h8;rmW|W5IRW;bHY5Dj&AV#(abA5O;U&qm -BFghzf1dBY3My?U;(b?!p&V&8jMiM-e!qa*iO3qJja$Oje-j*_^DmQTfinVrb$T^q|xowbDHw_q&#Ev|}v;}N{Ov($i)7ebY7Fk2qNccE0KQs@Rlk1CL%NA@=5ThJvHaSDt9~6Dfln{a+)2x&Yhla7d`?}jpx?A2&(>*?fE{7-MfeKmfFDO1Iql?Ma#6l) -HxC}Z5WgnXb_A&}%K>kBBbDQm>Em*hvk`}=(RkubnUtNx7>AN4ejc{oSO2|lsJ6l -lqKO1viVYiMcM6^M=D}UR(V{I|^&tyq&B%_Qs+ccvCoPspj7)v&h7cLC#@OGP72Vk*e`w?58pO48*4v -?bKfCHA-9ZD%;T|e0(Azp@the068cjpvtfZreXeNi5{ho8jz&9|?$6@b3BH%n7Z(rz#zbfBpcGf6bs) -DV9{s72FF{BU%(@F^`w+Eg`YrRex~&Bmlyiz73ksxa{7Timm_CZx|n>-1W(be(uAv#{9M)Nna^nf)%_ -6teLu%^e`QmG|Pkcc^)KVdpQ0tQ&zZ6jYA~cC^SMK=eO$>1=Z_PU9nJo=31O`-a-DRSxguM|dr7Dwat -c@h++>v?`qc)u8be@a^};AZePlxE~+daA+UlcOal_&WghFddzvAQdD6gaTLc7-#Ff~gtRRrY$Co5Fv2 -CUaX>H{Y(yrjFGW20S@yE8ZhaA-vBTYUG4U -6KB=l80^+IrW4vQDnxKPT!zG02+nPM;OSCWfx$*@HD``#wJt*O>^(rD8#L!z&aSZ4f*qkzlb%adeA6}Z{jg9<_txYPzlY|a2$AkqYVI!=q`O{Mb$x0PvD#s*=vSsU!Vfjbl!x$!Ltxg{lLtxWXJ@^S|-ubn2v{qpw6%=UX4w -bM|J&XRVf}<5G-9goZPz#B7sgW>ux#fDx+**`zw_q!1Gj6F9SQI! -MOXBSUVVlTsxOo@Z`o9X*01A;j>Xs1F+xq4V^9%#>sJ>JNg3E7Oz!02Cp5WWffz^h<_AZT@apEtPaI2 -O*h4W28@Yzz?_{L8;vYKoV9{BHxvB`pdUrFMR+VI$YnQ!7}^u_{6Q;|@uXG_nyS_}3ooF-YF$oci==R -hRMYjKq>4Rju20n?eg4@xe9Fy7XD<9eTbRO@`x7(Zwd}R%4VD2H^csjdIuE^~{eZ=um|wc6Eh5GDUg)A))jF)$&rl*Vtv>J0imgI}pI@V?!}cbKr(Y0wW|alBhr{2!Pn2O+6=SsIz*Tu+kf;ZfCtr*xQfHu`)#b+Ll0@u-4{Gn+d}Wu0=FfA;woUsj4G)8;R(WO~vC$Y -x -ZzS>qR52mE|XIuj5CWpAQ0mDpxZ@|*ft%UCz<0EED+m{eKQhMDcuL+X;Nz@h|)V7LDfv=m*eBtO)(pU -4;hNQJ~}?GN5GzE@5(_RA6H+$t2#(Efb(vnjnenUc^?mCyt;={pHwxEP^W_H?{43ARZT`?)GU*CGaC{ -?-_b@;tPp4Ylm4JTyNq>o+?~O(v#_h{L08uwEbpqD16p&ef4MaFQR&I}Ed}3Xg$ntar%8{HjWK~&D&bc+o$x|a^r_yho^T}YB2M~-w>@Gzjo%QsPjLOX9`zQILSwjCcKc-;QwGi$2 -V-~kXgsKm3W5R0E|8E3av$SGOyIkRY4-EHKULOCq6A8fYggAq1oR{X)@A9Lp+ptgw10Yu{Q-i&@jpfs -03`P<;{jrg-(fk57!=&|#e+;JpcT*WjV%al@WMMiydWKUe536f<4=qrku%2xub5$G5em@@mlRV41WLY -PZbeWh#&-6KY>#kB$}|Z{L#B!pT+ym0jr;)8@F>R{0$v|mzC01U-8oKxX)6^g!ic724o)yb;^Ty&ZP) -cZ_9%6xXwUBLCeL<@ERjYg%*%Oel{Wdg -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 -K%+fp+PmMPgqZaB_tZr -`eJHPQo{^jyvS+*>27+-{+msKJ?@i1;hvrCq2ziAY -;Ha4W_!`5U{}gyl)xg70GaRn=)JV^fe3(dok$D1<;m??T6?_pR5Q1kL=3Ij<7ks0v@|E(Cm;qZY57hHFc{^@g5_kvnpaz|ZEP83=e_D`xVmOS4l6bDFv3i1l;XH^HY}sB>i<_BwY*<>e1gXt@fP -QDa^uEa4Ll+Z%fl?{B=D}7Tvpar*5|?8iyo?&4|(XZVipnekyM*PWYmNF1rCg0*@VN>%!FI`5Qaf*pT -G_whhqaj(#`WESKv#<{3f1du$4Z_ID=6_m$^FoLyzQOJgb-wmvdfB>Q|i?XjPKxR>y+n)ite-seX0*v -oSC8=6I1L+RNwH%jMYnCU~}u9R^{m8+wK`Z3tmcHVjZ{J=C?XUiI|qhPu@&Rd3S-mZjQSB#@fU=gVR; -^}b$9(qSmvT*_7lT`v`9d8g>Aqa^&2G-33dU6HtqAog!`b4Uc&gSVx1+!hgbFq|A;i$?rvPZ}p$MLI^ -5`MGg&snSp?-H}KWSuJHsOIn%nr|DEW$@Y=NX{#t)Kp-tK9Y#ICp5^JFt(rU`iW>l{>R>pAR#phwe+<}&nt#q2~Yi(afe -koRp`XLuMj_~(wyyI>Kj;$PKCEY55{(s4daej$6m*jk4*Qzztpcm))YVK`LkN9BIE{3~8Bai6rBgHUPtb-zS43QcL%fa`No}JJGNu8eVS*MWqYVCNIehNEtrkLA{f?+W5#Ga -7Ct0a^z(vDd9aL`8~<}(*wtCNG6BqKTS9(e5MQpxN;z56 -Nj|Xav{pG|(lXLHU+WDBCD1snUBTavY -US0(Zt1|AUdes5_E={Uw#dZhnqX5eb4rJY%n*nF*citwTu=LVc<6GXecVW6`ZCs~K!rPC$kz?Lno+^g -`Ve2w*Ei%a?u!Dv?39YzFyW~(*I9Blb#Y=K9N5@Pu;k;7+!Sy*WlYCXs+OJe`S%0B`*FLFU*l{s>Shg -IIi8Jmm|b>3pA!=?n`KHhx&uv_6fhc0az(DF^msO=(wJEl&PC?}$H=F$L}RnY>9Ef>I-6=Tt}#LLtc8 -gLEOE$FvdGM-75}NN9ZWUQaArP``f*f=&<+p9F(L?N3DYi5!A$k -d}Zc6O8GX+_S%huhl!5GIEr7emgK5aLDR@Z%M=Kn8;9SHYV+YSCo{EJ4>ReYdyN>5VTH&fTq?>s8LR3 -z|ceuxj9A$x=3)zFLBva}k(Ct(e4qx(8p>vJ~$!#7;#i3$w?1;E<0Ivd8@TKLyCe3ljnk!!U#|W`GUJ -l60Ggk7eH(n}xv=u~gI1SG9uxehg=SITh+CeWIned}3hW;rt~q-H*3W+uGV{8nA!vwe53nY@gfn&UF&n@KQ>knwX=Tq7d-qH?I^1moq8Icqa8g)HW3vzl4Hd=h&5GoU -0=Ht83dETkL<270sJ44C}RCLK -4xVHZCTU~PN;yb_>47!IGhE@a!S_9z+^pPpGvP+X}6V)nv}I{s*)s`^wjsA1{53*CL=1@#`;lzG7dQp -SVH**v8046THwUJ0*}V;U4Hu%mPGJy`wG64$s`bKW0X`>XLkSic)QozW+q--di!*_Qmt+?abryY&S$J -cySO2RWU-#?Mcr;B|H52V4B~qj0hrsVzmNH4hhrn1z~NzWzbKMy7M&tG;sAB9H=JSNTzFin=zp_TW-A -e+sJ3cYHn3YPxQ6e*=|ldhSbm6)wkQF|H6Kr(hq(MxZkGD~(5dcnsXfS<2aFT#tW0K1w~bw4^i72P^5 -m0>lqnmB6zTEdSCJ%)Tv?pRCxL -;zAd1?dp^dgsWbNf5jf0a&I4h0>T6FmrP<2h?rrERbqZ6^%`{i*?W!snk-dEJJoZro8@~PKaw@TnNU@szZ -NvSN1;N?$bvWqv3VJa6{N!~!3Q2(!p&S@D0vMEas@(qwGyjVc8a)PH|iH%5Fs{YLb1mSG3G=##@cKK4 -M*5o+DjN9ZBBiHHIV{_fwuso-y02lzh2vJxG8QVJ(rwEDvlSB#nYqku22oBu`H|cOrjdROhk3@QbPfU -hDqDTws)k2@kp#}uo|19CFY79j03V4 -`eUm%j@F(R*Qp2~%jnf?~;h|3efDj(&LZ4{)Q_L -`1eORroR?80j-h^D*SQ7A(PRA`b7EHpi0rilnf&+5im9ZnD{&6^pFtQl1#Jz+S41*+MVfS0NO1sAlSR -9s}Zw14EzTINOZ_rOhf)n@tMbBznagtv}s0ou3)f`$f_F0GWmrACpQ>2%=T3+I=LMx#;>8q=lq)Y4H^ -kT5dIJsw(0_YLuoNTo`PL6eT>M<({E`#!GP$OKHvR{>2gH7?Z^eVCrlpM2BhcXBw`SAcUsFgrHSS{BT -EtMXbWrHahPP9@1ZteF6{fdh6s>eP>;f44mKyS)Wn-uxorcTsl)jJTCqr{uei(-UUrq;H0m@9S^@<=B -=wULcanwV4SWl7ekMQC4}S{OR4P0SA#+Ywt$oCUcE=xiD{i)Qf<37?r{ph%u7!XE%$j_t~}=d6@8RZ9 -)?Di12XZA1wY7XvQhh=@uLKOnMqYa-x4A!pHq-27Fbczav!ch!S;%86c4M3dQr0$!lr`uID5-U*a -cZ5~hA63B;3aKQvdZhM%R1}4&U!nU*bPcIc>Rq{QGa8rEWb9@*S7lFp;tB~(hD3&b6u1)&r?Zj-G;Qq -pSIUT{kAA+%91vfgp&QvDwT93mJWZb6KYtDZreQ>!IoNU)}9mf=a!sxF1hQhyG#nm3YnPzsy;9N^^ov -yb%eOARjFi+Nl-$Tg*`MYO`?7lQIK^2HaB2AycP7{jmpkz(I_|y`T?1*w}NqSkNw51Q7aRTqzQ+RGDx -(Ni7!lVI7{PQ=Z$xxnb&xVG{N0G)QNXqhQKu@UPR88jU3Uu^EDRll9!_=;Pj;C39_(fl`N_~udoL#>Y -esxAO;6Manx#3{A+7OV(kJi3AWB-NwBraOWK=F4rd0XnRP{x;*oTNt!gZ9qjFFilo$ecdQ0G9MI&qfI -LK0x6eUo1?tv|=)^K18*8*-Ngliot*=Q!(XlE2OXCG^+_S^)W=OgX;8lNlJw~$IiH&!=!oz0XT@mk0h -P7h`!k!sIP;>H%C-$<#)weUWKObkXdcba-*ZZfubD@cJ4E`+247igAB*}+gT@cZI -TrCK7pfI;{VA`sr6c{GJsCA4X;X^86RipZ@AQ&!0a3CHwE`um0@$PwfD?%`*eUXFquMOZMMCJ^PnupF -R7wXN{qlpq|MG)>gP(u;{b`^{Xy=0d{y%>C!JmKm{$G6gzP&k9YBod5{_hWd^xr@K^nZT%Gb=3Xsryp6tz|1rYkSBlh;^XhVc=GcXPd<6^Mr`S8V)pT79`J64!#;B+kue@Jj?F7$2 -7eEb&1>UO{LU=-nrJHs-@ESYNSP#fkcE|PwP*oUJ_yEoo0J>}?Y#+V8@3*7i$o -HY=fz3F5&TdQs=;EKzzR7U1@L9wrqZZ?|#WG4Wdyj$wIf0BZjG=`Y)jc@W!dMc*0U>Ospx-tknMZv%0 -OiNJ@f{#d2F`XDQZGY3MAHS{HNusqUO0ltUow1Hu>>y40Hp4D9&Ftf-J%7}A_OVLQ?+A$6kaU3MSU*~ -Sf&#eJQr?@;{xvZNvClsxOlcgpy~9#-`<6YZ?8vjJT4)vkZ&kxw%}GGhz}&Fs~1@Th74U@gI^HSvXP* -gQQUzEIDANw-!e6%tU3}X=ZA*WqrCCWY*R-;)HhU5{*gK-wBK4$r0AC -3&#mS*yKmFrYvW{4=(c6jX1sbIv2g%HyDj$j_ymPfCmav96%`~*)lnTm;Lp3jCEJpD9V|+#}ngF4{Ft%}885U{K)~2hY?x2Yi1vrlDRUu*3l#zCp -getFGnpp#lcP*2MiJY-8 -=bA7HTGJ(z9DLT{wZ)|9HZLzLXkv%bW!8oEx&9;ht+e$*wGeK%Ts;7a!p_2`afCTQH~0JHoctm_+-Ob -uFg4(8+YjAB9Zeps(&V8`PQSE*LXeKLrtQP8X|zE`ct! -DEsGT;sgp`7AL%`;k*VBS#_LQa-*5NW1HB7n1%-Wm5+-F2~vVmh7hF9K^$U&npwqW7fj!NggMV2i~@A -l6UP34%aaH{7t#ntsrvfE$=DGjL5a3RFfhO}fZ7G`8f#K0`vw|4g!V+7$Dzq$zR(b8HngQKfEbO24|c -j;%HER{0p4=y4kuq?-mUFPZDI+A8ztb{8x|A&=(+8H{Eg8W{z|w(PGV -&Hh(&A99bI+uY+f}hY3Ws>7Rf0#JeLjYt?bH*3@dS5w29q%e!l09|bYuuH*TWp(!sXBa4rLV_na7i1H -;iQ_G!doC&Wf2MywBq}!`$^Ac;A~#JrP0Lq2>{oWB`KZi6dx5F&3N&Kf|oPHKp=K)Q8QWGOr%1-IT$7 -Fnqg?Y7lUhY{6w;ZrL(Zh{>d9ZgOmchj%a4yW2#63G!BzojU!&WW1a(0Y;9z1Lihd#VuYr5v14qa;P -aBEK(p+dy<_jl}v+8uk!GAzdn#9hZI0ceA%-17pOWJ^zk&#XMl -vvf-bkac6GYt!(?w=6#&BXrglE3#V5U}1&x43*N_;OGq7}-AcFccCU56AXZHhjRK! -}f&VQD198BY`n1#v79^Gx3a(QvcW`LUyxkvFSu*KH&g!pIf-et#9EJ(^~Tk&GDp(>mquh^f?ozYnKT4 -;B+%b+vzH;nr~{(S@=*KAvF=d1vg*X -y>W`EgYu}*#2qBZ$1<;X>@NW*8*c@T_n3Ok12?7iTm8D7bKgN*t@f1ph*C`pfA89gp|6m3CYKDen9sP -=&T%h_m2w_`BsJO+iA_j%B27Iq0-WN8W`6ld*t90rzC36cx22-A{~ww(Cd0VEWEso@e^&M81Qz(x)?` -AkbpW9wDEWylZFY)cUwRg>`r_#(CC!#r}ye5zI(uH}?AJzxtkYDtvt`I9K@wigz_@kZUWyW|vnzsC$2 --!JgXJ`6gOSiFP?e_nE6*N++gvAkAGHTcy`u#077Yjvn0a++ZvF-22W0#8iz^Lyf@)}TA-_2H5zf60= -cv+NYSf^e9ng?#G%w0^(SO+e-;Cg=p%gs&=@sUx5?F6e;vm;iGUOU0!krf${|aDMQvj%4R7`H_&3oeX -|tC7m6*KO|Z_j=(`+?+;%W6ly}jFY5N&c+*@lt~Ch;q7-7WY*Lj(1ukQ~fm%t@7Ic}PnZw`|_L^ZEPS -1Jv6uP4VLxg4^ON!O{SzkRZI?x|%ZA>&ui=7QeV?5!;Yr~)w@)cs)S)!HS-rYU#+jrl7`>q}#xWz@?! -SG-X8w(42*I&QByI-J<9rm123+WtT^#+Pmg^nhK=)tTZU$mVgLiA1JZ5_m{u1VC -Px`^bmR4ueIY%0P4yM+)SCaGpXvklT@1#C|ZtUrCi)=F>1p=Of_rZ45Fh_|BzmWDTpwO!!Ulp3VLzm2UxH49jWgMa -@}csFR4nb_9)JPW99>Ou`E@rveLO5hGHs}0`(Px#jITbOf4&j`F*!WD`dyrV6o+TOi@%T0K8(ciZx@r}%S&-S0b=9Ri<9v -&q>j&yPCkDapZy}<1LoQJl{gunj<101)j1WZh>edgk=*I=3IyoGlj>Lz<)5Bkm3F{o_U=0zFc@$qh9b+Pvc?kcGuEytQSec{qv -#SZbj-alSs{;AU`0{up4kzPF6wAlS`6*Tt1q(RmLJ}LQBliEWoR*py@?okL+$A5O;QF2^hTN%o_(-6%12Jg0Jn`dHCH=f4e(gB9{b> -Mb_mB@3&UkO^H$T{~p#~t0 c{Aypn=R#0;{Nu^Nm;45Y`aGKamu!Yz=7r*ZT3E<4uB!g%ce|>!|4i -3Z~Wbb!p?_>7c*8F#S*W#xGVeOB^+aZW-CIIx^j(|UOR?)3P592YtG=gS#1yHJ8PThLj}j2h{`L_(X4gyOZx&XQFX7Km8CS;JwGC?7q!%)~iZ=M4I!<0OfbD# -{Cx&?}HoC2_5cnsQ=VKfK+D)(`tNTD}a$Z@QhlWvb+l*k6Rf)Xu@PX`hrXrRQM26UMGD6Vs4uSy1j-9 -7%8vBxl$69f)cy^j;&3ZX0%J7`W7?hO1MC4j+XKshT4bu@8@>FyY2K`ojnq+c=f8u($8b57j9Rv%>QB{kb)n}cXDQ${xu2D>C -jNVG8xs)#=(-mNUrfG)K3rGW{-ULI!Hh>?T?uW@)Ut)W30#$zx -CjQ4{=gv7w9nHiQ@vqCx17gnJ(ceu|x*5;cO|LKFcE!I+y@^J;$g-qv -aiDDmHCtdP~4Iq(-`A859aUg;>!)WqT@>xkihiln_P(k$gBn(>IlxLziqBM49!WT~Mi3!tA$*T*p7gpdI3mMLK?s~riCu6QDGVq>UjTZN1QhfJ+{PLtk=N^LLn|8V6Wb -b^0kt=Nz!WTpS|6tuukDSoD0u7kh$Z*7Nk_H4{&tB_ZPZyiUE~2s*SQ-=^FdH8jNo6>8?<2RqMFsvq! -sMOPNuEa>nl`1bV;?&?^;(@_@7Q^rXf`;(0*uPv~!z%QQ=cAgU3?C9E)sq%AUn63!jn0`Y{NENXHv1g{R2EZ2TcvkttE+0zHL+CMc#t|v*Sm9Xdo-Y(JpN6XMwaAYtpU#w -Be<_sO`|DA(k^E!z5tk%YMvVUig`@4;2=$K-ARH|;Mgn#cP}Y9@B&#m@_qZQ04^o=32(nQrT|O^RVC+ -cTEx*ETB9HnTQ>xmhEXCYPA&DyV^VLKESo>`Ai>p?z!WTsr0CM5Z!+7%9oo6P2aFv0trfSr)NFRGUIO -2dS{mnxr-mC;8zh&an{v}4t*o<`NH>$!rikk3@bi^+3t)DwUCT#saP^+7+5o75?KnlOaL^uY=uv_7id~;vaaIG~YQs$AL@bC5Z9=B -4s#g#h(QsRF>fNL^!@ob4UkoLK!8Ddg`S$bHRT!2v%QekmhR#?I2p_84u1BZ#-5r8BN>=r-0*!PHIqI -)Itt?Sr^X2UG|Gf0pL#u39x?e!AGDtE&u1#ynO6OpSI(dPtzX!IM0-PAZlGeXi5|yB3>r3C)3W#CvJ2 -r*NFeI0OE%(QqpL{EhLM%(}5Z|zNH?CGKe_gcT@LsuZ?W6@Y98v&O`a={F%oYp)Z4_WZ_^sK5$kCw%P -J(t*`^*b1IV$fk(4L>h_1KeRQd>EOxm{q -|)?q<=}H0oIhPSw-Ngzc*bA&u`#I&JSyQdI3=~gJAS@ctek8P9Y1-k`g;|iwdy}o&HqE!T*I~%H_x&q -gdFi*ApdX^t~|b=gMk;!w#)1_6jIhNKGu-iMLy3Kp@ZwTnZm^cdS5}XQl!`gNGFlfygpr>o?H}fgud* -yHD*2xpajivrRJQ|hA9_r5ITzhVjx^sMHEW1PsgXn?<*+?WE0RDLeNLv)+mjuNkodl*3eqy&?cegS(V -;tGnr_X;A`kcanwN?!rU(E6{Ix<;97E(@Kq&+2og{mA(Z^5tE-DkncTApG*d{mDZjSSIQnH=MQ9|fqi -{*1iYiW9+B!PkMd00W7+^du`Kb-z&s^pN^I!DP29n5SqHG)=TVc}JyOUX3@=ouU-HaOk$?#M?ekZ%5A -QQo^)MF2r;|FdSlW+b|JgGce<@KTCCbyErM|f+JF<|;grCx#^iLcG{QoU;<)#NI0y#&cFO77TBn*3GZ -`UqwL#ba0eqtP+77c09XaP_0H;I$76I(OQjma-x%ubM`g*sI~lXBz6E&~DUgK)^v5C*C}cmB-KSjxgm -&r!;2x4ZaM_{vs9&-1Apz?pa1Uiz8)>AB!hNAnxeTH0lC|IncdKV`V+3{Qf -?!Sd#(OU#+@zXLnWm?v~wJd(V>){1|urKw3{o(400cY1lOgg~9PaA4!R`IK3n_geJ-F-6Z>n_2%_(nZ -(mLD+8a}t76PdZKL(L2$Q8}gg~62ItYI03xgDXTI=S1J3X0P9F-lNB2;#D*SMS0_r*gH`SC+ab6UYH@ -IIO}<{YdW&C}+5M2Y}k+Qf?HePBZf+ -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 -AXG@6aW -AK2mt$eR#QEaJPHp1006K7000&M003}la4%nWWo~3|axZpeZe(wAE_8TwEl^8#!ypXYa|+(;p65@Tbi -*p^9)e)Srok4GeY_eusqKx9;p3Gs}{PecQU$EIgGE9~CsNGxZ*(v`*f4Ixz*x -&|Z$53Ud&(UB*PL3^mgk;Y7f^>l@7&h~EObz8He%1}rZgg^QY%gD5X>MtBUt -cb8d2NqD3c@fDMfW+ykOKsD;Z9JB3qj9NhB&pMZ6-`oi?=tmDY)qNzxRhfTI&jJOBMSh+=CkeOM;tEB -n?_JNc!Jc4cm4Z*nhVVPj}zV{dMBa&K%eUt?`#E^v8;Q$cRqFc7@!6|8!p07J -PO&>>BV=8zgi~GXJ>~qvYM=MfsJMulpd<;=*+~dvaNUq-5bYL0y -ub@!3X~u(H`XC1P7yoGh-`zX#*@{H?enCT_`%=$yO{!MHbKly7c=A6QWMDlwwo;1yD-^1QY-O -00;p4c~(c!Jc4cm4Z*nhVVPj}zV{dMBa&K%eVPs)&bY*fbaCx;@QE%H -e5PtWsARG*n3qwfIJr(dnm!{o-Z747oeH3WvY!jhHjilnZ!~Xm3NQn|9I_VxJAc#aBk9YTdcRX608l^ -3>mj=v2_>X;CmsTkm2EIwLTP*fkomeB9ym*4TQaac0R}03PoL9WDpbhKff7xz7zxQL^vbt*gh=(Svctm_N*vGEo@O_ZiF5Ka=#8=&WFt(s)or}0hS-kW~ziZZnm-%045nasc8`d~(%ZywL?qYH=Iv&ArGiSVtRr>i_fz$+m&YLSZgo+H!sh1O1Bpruoa#s&fdCFy5=k*%t)EB0AjwX2xFW6DEeLnN_$e5Q -4%61@Mf(VAj`enTM96PXFdrnMT#dEj$CO0V-1Eh3Y(MR`BE+qG|lYdfcja$xo4iLHu2Wi`AESAQ(^;! -S>A*@H|MkZL;qFD$)ecB{aTv=AgCG-?I^67K!yKGf2(VXt1rWy^*_C$U!!X=`l>K8CBH6S1PJUqx7KX -hT%l{$dCZcvC~1us|wSRSFY6W8CF}Z8YHTXZSJGriRNvC)fKW?5LI0XA{UoMCmJF-GKWki2wtm6E-8Y -)z9KCqAtGHv8ckvqSMwpWk}>=KjPstq#}{LH8d3RDj$5=9s2<<#~_$V;4IRWRZI%EDUNQOG*WzB4gz*nJ=%uP?(XU@#`!vXeL*E%VIkIX -O|V16&N0;x3Y^59B1h;sYm@U2z!*qB-(kUb23EmO*aX84u5m(7uiieoXD?_2kBK+v0JyVQ+7Ln%i^3z -5+Y7$dV&}0s`IQChTF$4?`(;94S?cR67B$6G~hZ&+sFv*a6huMkEE78l!n3ggX&tu-6)~XVBpcz`dzL -Y&*oJR-nm -5`Rj$6o>4+<7**c6YKJgK>Q>YvoiU~^6xb6yVQ`0XL_jf~Sl%dO1f}j!WE4W0l?s6rgo)SX%Sb|jcf* -xT*F@}EM+7{wX=VrP?)LTvEXf*HS8&xJfp`4`!cs%&=ufS>BLogNc*U4Q8`6Nh%PJ^Ug)kzp$a7zHS1 -+w`yOMmJB|sYyW&++cc?kh|<`*++X5QZ31r2ffg1nZ%cXJjbn~B7cJrtIBs}!@)V7&aqEf?LEJbO6nZ -%sa>>Ffw@C3KAfbv}ac?u*9&yOv?r)uK(N@;!JW9FT}#7Z-8!8+{lzqIxTZ5Lt?2b2CrIzjBM`P$VVU1PJC~P-nZ8 -`aU0PJ1T)7Ql|p7G5|W-5UB}S4-zM_kUB|G+_v@gi6ugG;e{VFewb+S9;0Etl{oThL-Eka#Zh&!fH;V -JTMRIXW{!HOpD%0fZF^0m^z18yC<%Q;Y({p@}p&xp?yuy--&3jlBdG|>?#aBiiHGI};D)y*=HJ}!h03`ze03iSX0B~t=F -JE?LZe(wAFJob2Xk}w>Zgg^QY%gPBV`ybAaCx0k%Wi`(5WM>@0zaAb}16r4~` -v2*%wt*!a+cHj0dQGW4a$cu5_6bAcu`miB?*#jB$bLRlFjJjjTTX-hu(DAqy%AKnIgq&%XnS8&9h2XH -w-nuxITwEgjl`Z)w2@?s*#Ie7)2(suY)4UHBF|L;LrA4mF(*>db_{3mW~jDIa+-au6uXQ-+!7KV>h@@ -D7VJWU#xmV1TGP%oQUjI<=lqbncyqEF`=nnvn^n>5M<9Qqs#+}!q;p~GZy`oAz(b}hd!Ke5DmlsMmZ8 -NbP!I)OR*v}g%fdT4!{0v6&MP)h>@6aWAK2mt$eR#SzRWD^nr006fF001HY003}la4%nWWo~3|axY_H -V`yb#Z*FvQZ)`7PZ*6d4bS`jtosqFl12GJS_dJCq_FLGH7?H|?s;c`bm$+PLE>0b%v~Q1lz0yOX2*?m -6j{W)fAK~MJ0bLuW0V>BBx+YsL2w}*?a*DlCNCpoMv%vEhePSm5TKH{|F>+}zy|`s?+lXKbVty=4i0|&cnw2p9wlX7iHwJJ@Jkrp{Bm*p -?cwe7$;IT`@c0<+@%F0;+}->_SK+_#(mXp1Bm5fxy@Ql5wJ7FIEkgJ=xj10(19|^|k0py4F4{=g6H~x -l%~$-9t$_1_iGsbVDv+3XWZ$uJ$v(clVrPH<`aC)yD)n>k*m*?H(*S9E-B#WhFq_(TFP< -5GQpn-DnD^FA>npeoW;`K64I8!B)9F)4W=E~%{Y_gP*HKYs-x+qoDGD8xt?G0`4u42v1K=rtI|OY|75?>g?pjL<{pDq_v!beWjgrn>s7XIuDafPFZK -!Mh)M?EA5SF)IWZFhk8tE4iN!NUdBt#?)Zkn5e}eTT2uTp%o=|nf3OUuDH%KI=ylsA%Z>%;=FMZmD<# -Sz3_cXuhQ$k6W{pAyuEAAn*n5_h4?K;KV_5l_4A?W`zVk@4-le&SaDuCrb_Xz|y>3VKcaKc_Tg4|ZH8 ->8s!H4XMd?E#B*%T^%WD5wRDJzT`Vq^*5`pr -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 -5IGKO7UspI;V>g|;e2dQ#R_O7EYI<ChV8( -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 -R8J3x-?*;i3x7>f(#0&2?UOmFbIR>x -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 -JV{LU}Q2?hWFIS>Ei*9)~vwD9v -WB9ugC9IUaQjN?mwN`otPbt)vt#}1)!S8#)>iw(u)AMK8Ig75>?s3W1AKk-ZHk)OTv&2B!Xh>8IGTCW -iAiwBW@KR*&z8})m|q_Ql>)TGUjYsi6G! -z0oPL#seZR89P;_*TdV#@n6Fzn}`83u?H`FabWdh`vLCGtbN3L&Osa&aKsvLI~&UWVOCp-`uS4=OOFU -INBYp`)kSWh0N5>p!r`B@rvthPYK2mTOqmLAF=rEdiD19^KpA#-dcMHxza8RE+p0HK)gZTGAIFQ$7LV -~!R1_WnG`0d!enWmz^>ZphekD2>ankbg_DK4n-)@k^aZXouN~n(+c<_2EXFK##nf0)ihofHD^(zTfZnk{KZO9a;YL`L;2;X?R#lS;>WxkIPZo{rc{ -ArulG{G8dqa~foGk-EeBJMWx#cYu1VOHPD5T{D9o-|~PdVft$QAV!bQ?KJb@oMu8QafZ*S}KEN$`-u` -1QYh(w4_c#?b=-cQFl`yr`%@Opc5g5O_HcPZlKJU-CHI!8dQ4UFHwm+f2Pu1z0)6e5@TGVF;-r!QVtJO5xa_H>hn -Pd*2hCuU5=sy^gr@^>C`FQo$BBs` -4^Lz9$jko4`}r5<2$8L@1XR}b5s)dWiJlgdP8ep~*cANLtKXxiIBZ=k;sTyB@c_m~i$O=qW#oDgbGPH -LQ+j^hQG@s9+dfZKn-*L?xPgEgJkAMwREG_iZUN+7JkaBR9|6u-VQp>BPEH<#Et(Au&(#UuSsG(LapO -#wrcZ=14VaTRuRApq7tj^qAPYhD7driFuC0R*FX_nd|8n-n*9H5HeethfpSd?`Zj1Pv5*~)04omjZDI -8#heJEhHh5Z03a-;Q(@FeN+R<*;}-EnJ`OXCW -62(=C;u#*K|Vk5EzaA16oI~G!tj1Pa0DCYUBCk-YJtZWGUqe79F@!BFw`GSYd-&N(R45$)R#9ySWg=e -ent2X92^38Y8zg%NmFz>pZUllZ7!bWxFZ}*E9TRm^w{^Q@#F6QXBjp`s>)%z8e5%YBV;6u$Ky&Gfc_J -H^|-qa&_mnrl`}QXG=(s+VDe&_}Hw%aJo_o -Kv>fJvAgm@i%j{DE~1pOl4e8_E)wQlzC{s&M?0|XQR000O8`*~JVsK-IyX8`~JSOWk6E&u=kaA|NaUv -_0~WN&gWV_{=xWn*t{baHQOFJob2Xk~LRa%E&`b6;a&V`ybAaCvo-!EVDK42JJKh1XLeWjw&7Ubf3lJ -51UcF>pFG3K1r4_x3YMN>Zuil-T}$ejE2G9zm&o%ws~Oz#WH}GFW$VyhVL2rzOAPi?YUK` -?7vvZJHbg>hGOVC1g{5RgX^VDn(tgAa@G`iBwEu_!H+rsz5c4&=#&6t7nlD+z+FFI4@RnJGxS#9SbDg -vtvkCFsw2yDW%Y2Uuxmg5cj#+y}(3yh{XfFVZD_ -B(!O2#6MPoE4^K)c!Jc4cm4Z*nhV -WpZ?BW@#^9UukY>bYEXCaCu8B%Fk8MOU^G!RmjXO$S*2UNY2kINzE%M)=?OVaA|NaUv_0~WN&gWV`X -x5X=Z6JUteuuX>MO%E^v8Gj=>GXFbGBOp2G4emJ$z8sfTXR4I;}SDNX`toEUVn^mqh(vwb8kxL4@-#CG{!^g+>l&v-7uU$o -njI!`RZ5-!6>wW^F|Y_^n=mbPGRL5ocF67PR7MptxBi|&RyrdfhWVi)?VL*4^T@31QY-O00;p4c~(;w -9HPC@B?176^#cGN0001RX>c!Jc4cm4Z*nhVWpZ?BW@#^DVPj=-bS`jZZOpydvZKhhrhCs*gjL<_GM&9 -d-^grV&boM0%JB1r~@2^7XbA59>>NyRlw+FM&(!9cKNgA*G9-5|sUW;O(}VbD#2Y(i>7#2bv-FzF^lHZ-+Cn -GGE*x7o7D&({FQ!)U{h8#>&?_$I|SOuQk{4H|3$dcy=88rh_8cR-d~1^5PuH#E9oqPOdz1o`t7lh=h1 -c!R^61mA?=28=VJHaNP8m`#E$SB$X@k}TIe##bm<}%&k*v1mYxC>D=4<-HPqj(lxy*@(4rzo(8w?3W4A?g5yn2{N<_>^bXe>-v)EFVG_xF~(GwiQ*ojYW8 -_G-pdG6B+-1wo$1iC-#xn{DXki9zX#W6mGC<7~^&S_>h%Eqf5+*^H}yqpWYPL|Z+{jH2yCKehC%lzps -pOg?243T3En`Lo@6GnD9zGEBkVH-jqJOUrOMC1jBSG#l=o&~N|!Rn@L*W$g~*N$kM7+OA>cpcaCbB^a -{uk2Q&UU+2CPBrTX!wq^)@M$8u$l9{o#)5a620IsfjbrgivpGAW5aIFUDQtbHPV`M;a2a!Si$9!8eWu -Ip4MN=Z(lsUNTuM9~J-U#qa93i|4LRYVa@Fc}Nl%Y6Q>6CksH%9?hfyq(mML$|3wQ-}SNo7ZGQa4ZKG -Ypty14}zWxK;+m{d+bM##w$ahsH>G12$#p7SOm#vf1gNoQrysgRA5#(eFtcwdbsAr`i)xk*4i+fuA~E -%SGbB;U)6>+|h7o2~VpZ@G}Ggj1GC1f$asQ%DW9YA5@~tV=>TvY7^+WcgxTz$3-tGI&yBVHCObXo$8t8dyDr1kq;%uFo^?GJbGplo1dC$v2W=5EUz|UA ->*#i&0BrI}0UQ>}lVFg`N`+G55ENLg~ko5TMuKd^zMxEFc)1L2*dD@2+zdOv(bU|_})@44_RwF4~Jj4 -#^wjEmQnj2J!H!E)_3Xh^l&TxjS%nvX=N5(#qWV<3V5yEIat#sR&Oq086W*obQBczO@9VYp*rgdtX2o -Da?`!J`2d}a2zs`zS~RViOnQ+2!a=m)|7GET -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 -fSLmLi#G|eGy)`pa~J$qdlbyYkvS -sus?lVr-;C}P55(hh#$UH+x~2bq3@1Z%Yrlb>k=0Ltr7eMcFQS0O)&tN4~_u{rtj-!3j|&0&EzS7x84 -sXZ_G}h^Zp|C8sB|TvHX7gGw@zPiv#|h0e*)$ssZDx&)_B(4c4j#v?-r$kZ!#i$1G%jUI(rmOEbMuVn -2*9^4$pYoEp~JLjytp(#Lo0F>}BRM-TsJ3pyAGX8{vDe;Q#hI&bl=JXZjh0+fO4SHKm6gZ6X#Ze*5ET -Pj{r0^p)q^oJn9=TByd(!Na35*<*#clcZf2l4`l9slN`68JCkP(%rdWuO)63Qnx=3Ih5|39%2HzSQ|u -=l8psQ82>P4o)Odolc0h)Ij(;l1yO1KjJI}OdShrFXGoeD -&-R)8w!uJ&$S*bL^(V?1d&B*T@FF%JSb46f&SZC#+wicCG`oqX1cFlWtkv)tfe}TO@9T;c_JibEM=e_ -7ATa*AQ5Z2D>Wp&!6k^;xkc|ab-lZ2T%mtZdtTynR&gxkx`6ISY3R-iT{=?nQhRpsMudsr#XqECv1eq -|nAhqlfjRCW~I$mF^*urnNIdd)qMvL5s8intvMh{1)@Pdiv2@msd#^mZ=f{f$^VRbUG!W9iq!^7He4=Y-d5@2Ob|rd?P>n~ll6{2HY<8>l>( -*W1pwGA)t)yY*yXfJpqrgx1ds34sdUDtW4HT-fGyP||6XE<17ISyFvyi=S@1imm -0yuYwO)SQf8Qy&&h1HFqH3laDc8MSQJ3xu~SOo6qtyF>fawV$m1^h6onT_I*5pGJ*8Cexs}&GpRm&;i --ayMDML%1QIq-f@~du5`GLk!Tyg>F~%M5>2=nIAvx}a~_rQq(^qrOm#1{bW97$x -*4vv4Vj>ofdrrs7l@F#1t-xVw%Ez-BPGvT=i2y-l^MkJwl9YM0|;v!|vW)r`A|w%o@F7_b!1)HIha2V -x3&95S+&p>77>N`4;uzGA{2AZM7QKQ1T$9(>*q_BY!=McY9yaQf)m?Mk46Fd_1!DfxIfm=z_8ojojgW -`*@()!#HfuLuMj11P}2!5`uYHk4=k-Rqk}wIw#g{YtVmN)UYi31GC?v)kTzaKsxYE-NenZPW^RE{vV$ -DAs60{EoXn8(f;DQk*|~iKs3cH>(ao{CgCClfM*466{lWd=xA}VOA=SOEIh|whs9%LITgj0%qcQnOgq -pUbh?R|4N7cCd=mi?fGmH2TLwo#OH3~U0{@*d0FFNZf8b3TE_#JZ76pJIOC*2-H)EFUEIe6;w-|0}5i -Uf!#K9L?6D)y#&;b~Hpb3_kc(Ozhl3+tJ8wT{p|56!fudw+2Lm6=F*Jhjg#cXelix@&0_a+m->~u1L3 -tfN}6`bw?E(bDzo9s+KBtUHiHnxDbt^xQ<&1^4zI=k9|HD@^){`7d0T_zCAN6_nZy2xv%v -qOfeYT+{O2wn`kBE5dri#;p4`y<`P5DObQsD|$;KcaPwh(PRtJLJlQh}Kw$iwo0VXr97o>>a>!%Z9d- -Q~cbuRPB@jk(0Iqsj6^=O|`K*5gMocK;ByNjOT{atlZt#FUW1j4lSkl{gbQO)BOy-c;r$vK~H<92mAIHw#kt;Ag&aQ_r -^F?o@I>_+5)}0f54b~vc!IQ`@S$2@3g#z2Wa_n{JfIke1C{0;u6*4sNY$*5rCeSb2 -#3zE|cX`Tiw)U%&suJOO>I$@5Z3vjZQJ$5pxNed<}B*tuMb&(vdGZL0frv -`=m(#*a%)RanPnOolXw0lo`#ZDrdF(ef45uIN}YxnYgbVtd(INDCJ(E2Supadta1!!`x<4cB -tnX*Y?BU`0;O-->!R+*8svZf(jJ)!e5ko;S* -;ol4PosbbN&BTv#ysh!XyEY=3BBP(n2k?jw+X}XD=)*%O5QSV&=Rj7cM5&dB>oxJ{AlR!dY+LSy3`*s}`=r`KI3jFZ?HK^2Vcaf*6?dk9 -;u{p!KuyYbgKSb3SPN$NoQY7^Ru7P)xj5k*RR8=PA8Jlapx4YEE82Ceft@AdbKn=Z=P7qBsgRSg=qtN -|)#~@aY)2#GG*ecyuw%=qvn9!wVq6-2m(vY%$g;^&1q^#KPhDwVn;|3(g1jJft=$;4QI?6N{H2E$$m$ -vd*H{A)HtYd-_rW6!psZ#+HTbG+6=>z`{rPf?}HdD-=upGSL&Cv2UI;$}jWHOzUTZZj|s7`YBynl8_M -bbAR!ZKVavltmx{8KGOHp=?qB!WgU3bUpZyQ^B@2_@Rk(~==s?;YqK6`qW$THq4%_C{gC!V9(ovD$JOc~+kpBuxz&E847Wk=)_7dny<*qTdzi;I!eh!1%1|*Gaje##Zj0! -VS|@Sz71uI>n`>?5mDLcfA63_Gw+%A({&jutYg_62oy@ir6K5lxSc3wiWvZ`NdS{CCDT=X> -gW5LYgGM6nwkzP-xqdiHT5@tcZKaFy7|*_uI7K<67upZ_hdbg3lLX6J;jxxT*t$V0)|~0bb#+z?jX58 -NfHGHeY({L+YllRhD&OvnCOsqJ!7!+}6W7}WOG9y`lxz5WMxe4gn22{*U{&Za8B2;tts+0z3&xST?wL -@!PbYNMu9EZAljItcx*?oTc1xIRs=loY$c{$R9-j)4k<1jDC|B)k(L6{|4IZNdE9ym;$;Q5kcTw7RmF -o{2qQ=kbXb`!l6f<<}fm7;c~ -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 -n)2$KX6-$ic?6#I;ZGPWBdWiF`N-tVrf>?Z -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 -8~x6C*F)*Twv78%rc -KOODYZ4+SqGTQV{?*{lOsQc`0{t&lqA0qoTZkx}dluoX~UXgHF4= -+O1m)#S)hEj|K%}H8<+Y_wnobVAG%<=m~-Ksg30GxJC1l-L9<SLP -*XStgM<6C8&H(cv&&i@CDsAAdT){lKalsG0;`X#ND72LGaC`$>tG&B7EGkhS69Td3*+~Jkj>$*tOrP$ -)&tlM_y4>D8-yaW&E2?vAcWWr83~!?1mX^EE5<|_)B@?EACFMh8Nr2=qqh)lxj(T<|hGf1}hjgi*AjD -15@sav!*!QpQW@cg}O6r^PEzlmD+bq~mTXZs@(uL>9AO(YxvedV6oHiPKx+2yrXNfLYw=Pd2hdEti5F&dw!(~8p7rN}9mUiA+Qha*9aZG&t)C)<)$$af0@o -sybZDi=bj3XrfKLcY--8zjbBVbIZ?gkGjN6cjMK9IYdUDcs)+ziDH0!LhUDN$H!oKz0J7I@Vpgq&ECKxj4V6G$DJ$ve(zU0+<{0 -$m;sk<^t}`CJg585)Ne?W~I;>X~=ggP=&Nh30`K7JOYvAQTMta84-sh7pFM?y}@*}Jc?9&J3@Rg+I)p|~ -|aA5GM_Z9vK7+QbjX_(U3qtvGX)9J&L@zbz7SxvNM2jmui>IOC`#z{L5Iu`VI!4!-DzlVO~AAo``T5v -SsEQ}BQ`7NWmO=BiY4KLkb1|1JyO&LEm#W_49F6S#UI1rh>#1SK2l}LR94^g{-xjzKxo1tUkWi>d&5r~zF07p-R;-F?e8DL-C -8{_~tZ^KK!Z=RiFo?jPc0R;gRb&$_BeojI8f;<2)|apbNFcTKwx_!$@YYlS|4w>_BsPJwc#ot&bW3g_ -d&MPzx9E?8w-;;tg```Mh@xAP4?(wl9`w7kTLx|24$+=%TSLQe55_hT9Auwa-;I!?cq`+q7eK}Ri8UfksjX(Ui4bqv!+$L7Jhvjnz<%$$KeNV_&b>Z_*zCyNmAMGO&qwIw&pIFSH3I -vZ^tyhLS!uqPRla!(ZbLjX@MD#SYd*AF0{6qg@Rvr0X%mFm+USiBMqOJCL7S9b(e2Ql(_on4UJ@&S;KEBN^+>NuH?BM|>GK4|UG(bOr%KYt2!!h6%0NkrwPO@977C -}XDk!(NumPVrV*_OU(l5v?u}ioDQrNRf?CC4D3|=XX9-zt9QU_(xPH`yI1IKE`F`CQ_rcLjrLM@GtgxVYdL;smucLQ6w^pZAsd2l#Uy5E?gre%Gqg<{%?YGiBs4rEJ`C=B_w2P -3?uUqgw+W@;_9UkIpcHk$9OAg1_f5Ro5S{yw^1}Y&B89eIh(L4t#B?cnX*ezMfC;#nFA@;HWMBc0hMG -W#(vl}=tm|XPaX<;-u-a!kr4jX2juyxEXR4G8m>0Wd%17wMc|NHHPAL#UhK-xb;!oPXo&sg~Vpr6_th -^>1b-v-YR2*e-^LtzAlAPOZR6vHWk#Hdf(09VY)c5RdmZP1PSukeUtTfqmVHhA2iCj57l?G>fZ`{|Kz -gE(T(UaZiy$&Z4Yz43-s_=ZO4#}wEu(H5gy-$S<7qoCjAGf;RNHrq;1=(b->Zuqv%n@3wi#tNyM_t7q -(kAYjw3#N7trf7Q$P`ssQHXy|}#nMem7257B*|B(YcKUOd!)pga`x_WCkYD!3%67X(r=^X*T-qPQ>qaVf2fIrNRy> -y%K@8ax+W-w!x-mQzm8P=x4I##pKl?qUr=l+%Usnuu-A}o;itPsoYSbPx*=v^<4Mo1amAz6OFwZ1B(A -bnM27Qda+BdIY2j)L?EwaAPHi&S*+X^y3L5w$0J#-L+g-mYN%Y*G!=VB3%eE30+oc2?EkSc{k&#tKl~ -WI^g)i6wii`CycZfpSvkuc~srQ^S?1hqEvZvFy`O&M>T(2I$I`!~XH=8q2xKfYe^G`sfX$tHy1RQ@!t -p9Cuj`>xCKGcO_1w3B2<|Qdwi!yK!)|9LhW@th7gNIs`BZ%jI@DmsbX*706JfkU0R+k(Mq8R;L#zMNc%h;|Y&ipENw%K7%z$v6}9q -WlQ{Il9w{N;)JVgvml%MR(%`XD`hL>aa1cI{DIqbifhtJq!tyIBs=zi<6xDlsqUEVRF=?Efgjhz#hJbp0z4kr>e={l6UJRRnJ(AMkXQHowZ^W8+tmOhurh*|Nt$eEx8f8L -r6qDqx?F~9)CZ!qESVD>;v38LhG>y^4;8UMQrH=N)jg%PfQ*4H2NnI1PY&aQ)l1Aco-Py@UM)BPUuOn -gTKbhjXk>AZ%lHtKBZjcP{=x$(p(7tCbsj%CEWbQtEEnZy(2}bi&4cR-H{miIbTg}=tLN)TJqh~Q-@H -W#<5_Y;d7{WU1~O9RT4ugGMM&dFj|La7q?zHh#>;NNS@KgkVPs^Pr%XA^^D&H0Q)U@bA|r+6k+Ju;0g -*K{gERhvAnDf~erLz~KRUrTQ{Ml0@XzySD1xmpiQp&+qtptO6uQz$5(CNg2Z^94j$*{81pqAGM!k~F! -EZ~ZZxY$@mc^$&ym;eX!35eEAqoDQ_`EF;$G6_S6;r9*O&8r?MEJ(?5^QT<-xSpLZop^waUQ&pyFA#GaR=|YCK}%?`Y=kRdx=lMM -vG%s%LoWTu-i!TlCuwZ-x8rQR9kV~(8U&1&6~!*k$#qVb|CxcdlMk}BOrZJ~{b~ -KlXYW+aQLh?NOc88EUlU8%dcdv>OG>ec@74v -i?tuKkTo4{QpNz_!Zax=E;5ncnDufF9oek7^b%2^7a2K<`Wo(BP32jFiH~ZhELPO$W}97L3pLWo0@mH -q2^|IPwgff$(G0b8i}{EKaOq^Yn!SmwyTV%zf)7 -gHmKeliNWo>D~%<>{XXNZU33#RB{l;P2;A-}+^6N%kb&5z;wfU|_~3rA&Gchu(_6o2vbzLt;l>{sF}8 -JOj`%l#cgb;yNF^4Yfg}`JRdYgzex4pKKV!r{i8nf}3nnY&4Bw0s!85e0i`}%`VKGguwI{F*fU94p$= -`_)TrFfp!n-W2Ymwyc>rVXsi+lYyc{1R;w3@$3P0c?^vaFPOXUvrb8O{Y;8FTsT9RvULmVdot;Gf>|u -lnJCpwwLbl5krS5fPTrJ4>tq)RVq74p&ItHeB4hL=4`B|kS*vgEg8o=;KR2&Pzu3X-_R -)w;!ePCnQ{;jUI@>WZA>xAPe=*hy6K29IAEe&JMf>d#IX%GFYf^I|xzv_w`}-O)-smx9prgo!vA{OX? -cxpClo1GJ*v+q8F3U*z(5wpY))p~2ifYgqIw=IX?E`uGxiir|Z56&v^A#@{b+!(0{{8r7QsF!`My`73 -0)(6hX_#Mv3!YK=XyM76R=wxILF*UD3vYq$7lE`0`}TtlJ+&DfHH&?qqY*mHa*a4SeTlWN5<-`&EN$y -4No!syY%*(Sn;J*%M4c6VNrpPciZQpX#mHh*TrEI+-oH39%}AMVoO^k8r5U7)ov(LR%I0hr%uw+QkV> -TIbRUr!d!2pw;T<}W(oz~&PZ!_*if`uGKlzkMGr>-J<1>agW!xjy%&@4^Si{wV)7?p;rT2-_Qvs_Du+>L19w8czR9c1-93ky?DZQxqmb4s3abp#5 -Cj`I~bQSL;)a9XvzrxwImDGD#IoWsugPw1%%k)%)qw3Me`^k8}_#j#k>G1-lQ!0~3N>Xo=u@7tr(%#q_vOsPrrLcCsAIN9z^Y6(G* -?j%P-v5+J^DO({)9HVHl3ytGe>lO9sEa}vLBiyUumni101U5SyCN9{Ll{XBAVDD{NPRjALF{&~S+)z7 -CL24(kWDcKCpT%+l@t@?)-wmC8*7Gt(^j`1jiy^$-R6ar?8@T%@ZNg9l^$c@E**+*nP@WIp?pW^-_~S -T2E6so!5h}D&<&y65Z{WXD{l^yZ89C+HByn?XExi9r$nMz{?#gWIeXKaY-p2~zNwnS5%J;X!i9d -lZTW+j6|E`55f$3{#WjYp^i$jZP3md_yqEAeoub^8Q=PgOLuuSo*b@o4H)E|9${{u$7Rl&YZ>&f34wP -1qU-_$Tzk{N>LjGfaND=cY-^x -n9yMj9bX~O5&Ck{40^9wgm_e`|kHQ=-kLEGhz%a9j>U^)n^7|7-NlUJ5Xt+B~MZZKOpJp^ty09Kf>kg -SHb`)K%7}8iY?5?8J8syJw`0~V%;X{-BH!BTRAPTa`3v5AXJBWw$)Aqe-RRc;xJx=la{;%$Y5TwtkEn -v!ER3aHyXWwA3D8$epDyD+hqIbil>Q5kJ90dq;&s&pjCDxTv4Fw(_!FRATuj*UFj+DR4gw$I{EZFS^7 -3xpy{j`;z8h~fv)HarD&ub#!K2WqDAOcW*L!lPhNn?rS#=UB(gz6Z53JbBx2ZONysCas9fi{RBIcF|M -v;gz8L1WaGOvr%J$to%JJ8=_w!x8t*l>u{S#{;w}d1JuWSVdDFlZh;?vROa8Ki{6a|a+{*v8o585Rg6 -LJ%egz-(s8QUGMv(GKEH!ibM7-Un9M6umD3Eh&0P`Gh~m6Lq*3u7O%Xy`^X)|2gq+`sb-M79NR>zRqR -QzyH;B}#5eUF$lzE}`3?8oaqP#l$wNws9(STU1v{ltsJZ7POTOHz$aF0BwC=8Md`Yha2zuBSjguqFv8 -^9~?UqF*@<*_oX_1IylCezYdNC_WE3J)%ZQP#?cKQip?YSC@{QgGw*zaZtFv?v0u|@Fjj2MDFC+>;yx -@BAkEPx{bL0A*KL#jOLL0V$TPn#3c#1{6pg)DUFbnrgjjvN@Ya{2%o;sT@|Wf17PbG#@{g0lv!pJDc@GW5plAnrY#snx}vQq6USojvt#jJ{6{UZIyt1HxgloW-OdU>!IqLG)07}LlfvBgqjI -&Zq=Es68(4diBgHWWGlXX#Mfo%iNxAoY=+i;H0_qVss2a`4o;{0(l}alXH0%PT9R$KJAgnWG6R -W8dY2&n855#pV~nS5;w-t~Z4E4WDInYTwk+d?O7CzKR*WUX*Cb(Xs+z6Mzu#EvlNTio>z0$P4e+Id_X -M4+p-ma)u}t;f^g$6CydBSoe9VP~V^9=Dc9M%tSDtjCpg<-P6NJD)nq=LZct9=g|`Lim}>LCf}=h>Yy -GuceqDK**9_!O;aA#6YzIhT*W7HFrC(XuvYNOrT69G3RKf!2*Xc6K!0g*6~M_u>Hn~^SQsZ0{_!#1o? -_sC`WWYVO&=di1<>YEvCrfuLLa=StzSUaV?~=5#RI=RTsyC!ndL8HXy3DZg{h_WO@htgSah(M;>R=19 -ZwCU4=(ly4Aj2xxqXGcjU6FLrVxiKqy|! -rXz+3P7iE(KLA&{dZ&&Y@JT5jNTGuikAJV3LZ*oeMEm+4l$T)pf9=4{ -2%>v&ex>fBVyIBGZZv-Nax5)mtUV#+8 -TN7rxYaz0=TdqGp27tC|9JSTewn6HZI_wSxNw{Ge6>n)bJl#!=pzV_DTiDzKk*(im1$KnmYIxzF8#cR -*9CYnhxKr0Ntc9hE-NyS5QmO5{ci5tXj9eIKtD`pT}V;@ytNVG1S!CbyQeOcKB-E&~u+h=w -e?#VcZ@nl?>YE_#AnO0&`;wfY3X^)@UOZHw&s!Fzh(y -n>Er@??%p{{$zQr7zE5a-CJ$as=T@275o`@w=98ex11cGF1?+zW}tDC -Sn-m+2uimFN&`za4{K>;5P`0nf3OZ?bmu3+T>o(g%Ir5Jpi7pSUPwRepkr%*OD%>BDYKWy>5<;4{K%S -oFw?%GWy>i_=Niu$`BPkKJdUsR(0j=j%+f3mM|_{V*JL`ImTFdQVw6%+{sgGdU;Fc1N;b+yCR4KRU&D -Di14C$!V96^&LX+XSL^)x-@;lFhbAjw=-<)jIkXk0R@efQt!I)*w=CKEb7h$;zg#hQWr!=z -VtWxQ-ldk;)30zGZ<1KQbL0beyB~D#D@8VXxY50Rau(Z7hTy%?WyQ$N10mcRYLfIz`wX&Y*j5g>LOZr -+;nJPquKR$yj@I88O&vE@h<&%z72=W#E=hbR$}24pHS;);x6fNS(_af$7kA;SHD>{8!UYtyNte -KU?uzahcw0Vx7p__tuo(mb7M)KO-F*=s)@08$ZLt0(AN8k36JYN&;rRFFkMdEZN_JV~U$7ZTRN_rw=rl?{bJOMv|jBKD$>a)1qQ{lZZm(^up&$xk!Gj>#Zb -BBC-X937ZSQphe -YtS7;~$3OCyhOFd1=o}3?TYlEyP?De{)-BY;ic10M4OhMfV@5Am_H<=f?qW*_?|@*@5RI+D*W8yOz+r -ArlvdJsF5o0vp>L%PmM}HlcibnX@s?>^CQE~ -AcLn7+t7h(12+ehh45n1HoR!Juxm_sN&7p&%nZtV$D$%roO@rwguOSCQZQ{o7R8}-nYKHPa4!K#dxee -haQ2HhNT;q_th=4hXfeOv$JdK0LN&RG!~=DF#0U=8dh&5ZhNJQpFp#d4J)oD*(;Dkww{+3UyM!u!6MJ -|6RWZ~fwe8!bt+;}_mO%$qTkoOsaR|tN1PLIJZF9sAPV8d -_oKorY(S^~*n^9ZB6t|e8=pY`iAx>rrl!b)zkGz?Vukm&V -%0%@WNRKcmc#7D(@Y<@&h|X+}fOQBtdJW(vqu -07}a|`OJ>X45Yj{t-& -!YF9#^{&g+}$9pQN~4;)bF=0TRPh+TpA+Zi0#ywUMaLiynDTPtt+*|sd|TS)fpwPhI2zgYU`aPAL#ev -Q?B{ngJ}+b{&eFae_!gnimO1ntfeYsiz}TS5Q{w?WK|+8hxO#+O6!-2J4wga;r+tXOp@*yZ3 -36KZI*DP403tDLqkOy?crHRPY=-vl}Xv$#Y*+Wvq5A~vpv$;=#ccHU%3=14S%$r!r;XpZG9=GM1=*PIw -1&+I2u+DbrlhLE^2_4m{O)B~E6pHBuD4lGgPrYgAj-Lbqxw(v8=BJ>K(-n**6eqPSiNor4 -$arC-|GLMN__x}w*siul{`q-1iM -j9dA8n;R*%r47$gM-=_RGA>JilwW!kpsgtIBePf`JKu|PqVf2XkzXg8CzMf7|FUl(d7oEi;kS4pb4;a -oR9;6X0& -15Ct>OmUspqFOtfT_3)C<#<3Z!LZ}yu@%JDE2JXN!EOko5zcBDh>^wWH=sXoK}{TE`uo-1x9K=;=?Ph -^$94Cw>89?oZ*JsEtEL*v7e@%H?*vc0to4wsz|SdzBpa(1{9tq1yjl@=7xdG(Ojbg)Dxe)8dEI6@~_~ -D`zJlFY8Ti4qVM7D{jdM)!(Vrk@cdu@68`rSgfQq4-v0NTh2#I8)8z45e?qq}9jg0*r|hv)f9I3k>M~ -5$jYz$o_$Q9PV#lkD)A4`(wL$j(=ezl#yY_!@CqLpM31TP;ZwY(|hdy;}im`nrlh`|akS#5q>~MmhTS -a(9&6UfqNQuN-vVtT(w>rePBmt7`x(a(TV?8B^Zq2SZwHL2m4X`rC-9!WNVoLI3X>GNLGh;4nryfpliS4UiiqnyZEZs629W4JcS`-)p;*{5_^)qf -6QX_LM{H#Tzb+)~wBn*H-hTB7lwIe0d6?PC!#B6~uS^po=8yVS8pmTz!p|=s9&;NGz?J7CfW}?E68)z -u3eSn?mkEc!WHgHC*4#r+s3$_d%WU!2G=;Dm4Q?CU<+N3|IqhqadfZxL9g(WSH^XeU4yjFdCOj80qJ0 -rGt#fPiEFk&IRloHB&Dw4Sb(suKoxTsI&K>W1D|;+oY -pm)IpT2ojwzrv|zW6F{HubMaG<%tkg!=l|tNy9N0sL6vm{}{02~3SFhwCz@YB1)x6_TZDc?MKdqs(y1 -$ZmRf=StS3isJ4L&ixyXBO=ztNVcYHa$f* -CpsOQ~PQ#Tb6!?52Jd^CyO3~o+LFl>TisKZ7SM;P<97{y57lx_(plb1erTTj8u -orM;xk;B^yG}az74!#)@`TB7u9iP~s%g&*FXUhJnoH))E0(O{Q#FQd@s|7b3=zgBhY@>=jaY_1*_Uv^ -?P;wgMHjw;_|k9&h13Ka&yaQ?4|_0HZwahYOR?oFl1y1KmTEMH#Bw>p8gQWw9EAD5#);}T!H3i1NEDMd1QU5N*?eMnnF-SX -Ye}XQtOAH$A|{@o(>=o(lQk~t@d0Hl+1apM=f12`o-|7$j`~h@&$GVloJZ(Cj7tzZ1=hkWaA3(MR^>t -&h4;leXsTxLwWfy^aX(Y1r0LVA4`!n<)&V5g`VgURrODg`1!#7%78Z!soAq4S^p%V`UyYcbOaRORl`KRCfg7ts@sIQp(nvvcf>xfy1v+~55ujv -4I}@KKlgRoz&YGh$zgC~f-8ouFt`FPy4y%>nnT$pwy>f%o^IV@7`gpiNg_u6PTh#w2qdv9nXj;mL0g? --<+AZ^LvCefYdU{;L -wC&5tu6RP^PW6y&9NMQw7AU~J_<%xi<%pN#|m^7|b4ZWck$Ukc{S3cnjlY<*#~{!uXhx((DF`E9t}x4 -Qv;yq*8a-2gw{&QEvqHw);Sx)FUplj!5wv~+!|y1P@<=Z3D38;0H??3kY@qO@(vR$;hv-(f~v&(rOK1 -R_NDiwRWjy>MgQEbeu$tdIbQ+#H%(w_ZSyOUUb&$E9)o(zD2Q`6u(U2bcOCtvXeBJ|g}GoR`L7)8QUI -3ve8j<|^aIOw-XqgHz$PKx$3qr@YLP<5`eKJG``hY70cg)8Sd}s{#moG`iY)YY=6lHIiHmLoo!{G3gz -)Z{LWSaqgS;QS4!A=u0CU5i%>y<`j~53ZbTe+sy?ZkDR?$gPD#DtKZZytdW=?O&Xas--XCF)hds@HOT -9jz8*-3_2!coAkf6yV*w1<#k#^E*+H|OL$CKZx2Iel)#B^0=i8qY?yvCj1zkAcGkBRl^Y#4Fc*WI`dp -;3mK{aXak1Wvgyj<~b5`Mg%%KT(8xEBZ}R2|gINkW-k?%+Mg{u!m1Si;2ZRN~$^*9%VA*`Ue@K&;5o5 -dQPg#y98`D94q?2G=k(%VR9J@6b6aq2RQ0oW%D_T0A6W4chEG*!cCQDv -5V27IrP9zeIR!22hiS0Bnfy8!XFma{apE&fW_hqr%yPSEy_s$YjH|BKW7tReK%DSiT#&k$uLQrOOIQg -|ad>$3a*Quk&{j%r)7=sRDrAI~vv6@9nO&I5=>AQ~Y$NpJKB0g?dGef?z;nYl7^MefX9RmZN~hqWR?f -dDBL(}#~a+?_?YEFm1sI;AAQl9kJ?KLX=C`g4n*2rwgVOOnr6&{bS%;L6;%c++$wn -klo^meieiSTA&$dldgP>OF^r3MImg{iU6?LatPPkW4R6R7=VH+h^-K^l1OUvN?w74%m7jXgS^eIFAM= -jV)Q?;-y=!{$XoTz@j&1|-$TCqw1s@|M5%?oCgSZ1CpyTeuK5g~t&6CMr`1H}-Al8V*AVz>3vN7 -6YT}wMV7Jo`nKnl}k4pq_HG& -;y-{o291?0W7wa+?o^{x%@|lZVjP9nW7m*+KkaS6ITe)?Z1mpOsh`9tjiOZw!daY=fjLU1eAPxxy?@>MPVpE>FKx&y% -Eo|sadGP?_t=jrkM+S70!(^NKv_d{@tH3!iniyZ^^|yBz{E)xoAy(BX3{%>SOlr_VcBFX`H&#oEk>rC -kO1sjCz+{5D4)~k$7<}zYpHXaP3ukU%0qLt2MdB0VoHN+UDCl{7&K0?`_V8>QMR7{1yBG0D%g8Wa?tL -+5ExA5&Msq_=YM+XCl*Xz&)enLdl|pSowv%YK#iZ2Dl&oYf)IOwnME!G!GH!AE=+gKf>3=xO3J;HOtO -G|;ZiSg|2Y?BvpMdG#e)3m1`qxkV0hve?BN+^57#ia!h9$|*?Ri!lA-PTNPJqc^k_4QBC4iC63O)2@+ -7D)PNziYMeqrv)fqo>=@TUMOz{#?0{21M!g#fx3W)rWhEQ!NFz#fVLIAMRK-X~&UdIz$Q(tm~&*inmE -phyIQ{A?pTn^G~%!1i0pg5KlG*RsuwXXRkwHlCbP;KJC4#}Bdj@kJYw{=U<=1M2;d3n&kdVPQL@}Ph9`u^nQLC -L?9g=;lriI`=ZycGP9<@PWfp?gBt+Hku*19NPzEAo{lp9Odjy(H^oF@;3oo?RWn;T*`xZhnXb?)JrvD -A(^7q>rIEYC`77JhBIJ*C%{_?Snn8rv>E>$J%=;sA^oZcvncp>_=XU;znAzuJJD1icij@2W2Y17CYkJ -4pQ==3Wa~Tc5Z>>jZN<9xTXy_gGbqW==_0yP7T)dlkbhzIUN%fp&AA<$;W+?8^aTXvV6UsO1qPkM?{^ -O_v9(`9kp8nJInBTwiW$-H@a!rJP$RtpOUtZcF@XXD#6}g8Gl72$E1*Bt7@v{vyA`p?7f|E(NlWuDdl -lw^5RW(T81Y1WwFsZN5(e>!L9erb2b+R_?Kw+sdNV23b!w#&aN)9QD)zd#xZ_l&dw^7=2ais4h=;<+c -4NMBt_(d{JUeJPY#Yg_@X4qD|gIK!ZWI`gS~$8;xB(#dR<_D^N|35=O5dXqK4X3s`qE3BSv^eMh+R#M -|Cx%Sx3Uj!&H#*f(21wZ3?mj4Irl+A*R4qJ*FGpbWR+f$RwLb$mbG_MXhz>Qtm@_uh%wSw$?P -VaTLQl{=Iq@0_e`uZte>A+#@CaiAZC?U9Rw&60@pFfS)nChX;AhIQo5IR5Rdx?|RmS;sIG)X&msS>I{ -D*)@`;rpn?7`xhQ7ogp19bazQEsW6zC^~mM}BFhErajm)E`=rB#G!sMkvBni#8+Qwe&z?RTeL`Q-$iW -w4t?wGG=9Vas+;gZrx%F{VT7MWv|y@eoG+}A&Qx!Re7`fbL -YGdoy&rI=W{UeW@Zy@SiB+^N&%rMwP`u=O -_q;sn{Gt9jkJ8e-3@dFuSSuht#^q7wi<1= -us8!Y9KrZ82JVpE6t(N=)+;4jcM;!AW6{lD9XR1_9=FU^6EK@Yd%VehfU@=Xb1-L+6MIkprr1&a!rdM -IwzPxZ|LBSzR;|#NDUpT4#SclR%P8QBB~M1J>6FWR?(?tF^~o0_*y%fl_W^UdBRK1mT_79$Dmps+@Dj?8Uw$F9KmG1U$bx>vIxv$)Vmwg -PQcP-ut8K3G}&WremTwUl>Z2JWc{xBq~BBMjV4@kdz_x6t2aBYP*j=2%1H~U!?`bAfx+vdP~hZdgsW# -Fi*2c5i{sd8Sk!+HH+hRT7Dm$@G5M6YRnhrgeE>vGwC63G`5U+qZsKj2*Jft#6aZ`*~ehS@Y~fOrhT}_M#4 -=?Mtpo5f-;-ZN)2(I*mKXm{{7H&rWd{Eo+{Y)Tj!5a%iUS{fL7lCigTqLLc~Ew@_TOuP3IxKU2|2>ZO -mLdngdO?2qAzfA^2}mW0ValJu`$V+?wY@~y4IXZy&~Zv7EBldJMScp9e(5~bOnQ0l*Pp08l_Up~k8a0 -XN~I78wbgM-izf@MF|)G!3-EHWQSx-r;w0;Z}ombDRKPHt8wwX+<|^Vz%hE@KkHiapd|XwH2WQpDKc1hk@;KPn+J~!GrVU<{S%w6=S5MhfPV`RamN3+bwJXdef&#T=R+lJkTZG#GI8&+Tw4^{oP=%HlTP{qhSXW(y9^!kK@`a)FsieBuwm0bKa&&8A+v -?nQ@p3lHfhu#r(l-eDseq}Vd<>-iaq7?mXG!lv1wgJUq$9ZS+C~#aU_{(#kPQ&QbyHR&TFA{~L{_aim -8P9{Pn>l%ZF^E{~GwvmeXqRRRq;qN?Trhffr-RT}cds(MFvo?KRl_hRM7x&>f{{{B#STS^B#P7-daR= -pik$cbqi&GNbE8m_xgxg~N2rk&%x)q$?t`!<@%7qEJM(&;d0FwYK3u_6>YfQ=UfS-dvZ{d;(3xiQ;U( -Z15mk)vQZB9mQ^XrR()_V}9jLH|sbXH4P%x{{O60=Y(7CG-C{umx-gpGnY;AM4WDwJx6Bm+H+51qA%Z -VSz$WN{os-kR*!gm&-heL13&H1iA;*};vPCRS78M-3&MSdDIB4ro!d<%>dOdrN6J74SmLRO)+6L8!_x -ED&->g$!9-S7^=q9-^GV^@^9|QvD`V4RqF`b7E@m!V-kPxM^%ahg84pj5ydUm* -_8#{Uw?|y)pt4io5AJLq>R#3l(KBOg)>E|_K5+WLHS_HyCcey02E9*8K2arZ=PR3mUG-Is8hFlnjRGVYe=4#;?U)s@67$V6o<-42zV0piNgAORbFPrnk%Ep&LryKqaeL<{mwyQO|dm+^@wo -{9`N;kI4>vC&C~ogAQ~PF@O1H~9W@nJfFa21eqyk@(GIwFac&x_W00zgJ89xi>z|MRz8~yEuSw+fQqz -m9N(1`BySuUwcKY%LznDuZbsgt;qa^wm9$k{fpyM1YRx*``r_ic0B6)>oRrC02HT+E6%lUyv(bylvD! -kTDimXKVt{aqG?W$J9ecOSE5E0#+%(RL$3EeUoGZ12van?T$nZ+lI^CoSr)=vnh>YBG!qm?2}_rs9$? -h)NncgRWInV|Kq^A`S45KXVk)uVG*_FAH^hTmTphs4X?fbaaxkh{}!;AOG9?0EFn7SFc~5y_CyFAdN4 -CdXYzX_5kyV(#&cVqO?=KE>@mO+2@;8{+2UJx6FYd%PGA`muA%*f*eSERT7VlZa$Y-Ea|2DV^J6cN4l -UB~HnHFLU)fF~UdxICY`Js%&!~9vSk!Iyfw&Ipp-GjAF1yil1ls#5mUL-qT6ZYKQq8NT+@4-!H5qSL^ -#?nab{AANR}g;EIQxWgxeBfoyb_%gfTz<$H`@Vfpr+`nkBOoPJYmG|>T>HJ&BdS --uhImZ^DQa5h2)U$pZCh*Jf-gqEKHFcC4@^IAfKh$!srJ}l|%`^Q^puD#Jk9w8Nqx28wKlz{c9{$B)U -kk>+4*4N62V*FjKxvBPP=e)f5~DbZB0zE+$6+M8)`g#Lhp(kygl;mKO{1KIV2=RA(2$@uBsfq{;Tfp2 -Xy%{H=OrQV!(u@nAB4AU1JEI}>2j!TY-@|J!DFy7z{Q~30hHSr*wn_jUzCLFiPmZ^j)7#G+e*LnIVcW -#EeQvnyq2hIod{Yl(KZylbz<=C8?d`Ufga0R-g4WfII(#o0v!_xL_pYYOG23i?P>0t8;Q;9J0e&=H)j -SZW+c$Dsj0KSZIO2}FY@HiGHcmeB<~Au0GYM!_)pob1X-1hmFE7?#xQpGiiWNMHC~-9SL;h3BL)-13+ -VS-;+JHc<;!DYKAVe*1tm}ZyvOlxU-|2WA#i0-+Wlc=r=di95GT5OlF>u1{TI~5YIJh7sY)7Hk_V%O( -<-%<<1IcO*vg+cB83ymUE-JBoy=Q$e|Dbr+BcfUA9ht}K~s{~m9@V*mee2n$06?VvOb-|JUr~VegD>) -R0|G7K#%impPx%q6H(d}0&U+^siF6fUSCcZeDdIBPU3FI>I1UtIn+$v28|&p8tU|oOD!SlWF^9IlavU ->mjg{HjcM1)XXqVThuS!H*5b;QCqJ|1ygTkh`4T-(Cz{XinpNDTVR4&vZStKEwY}Poc9?rYm!Y#oXx{ -nkcoriIu{GVd8|N?%WbAauuGH`M$+g_LWK2VVQ(iksn0YnCLiAsIwViu~KEjaJdOk~MY#}sH=AfwkS` -QcMak(WjD!dwF$3;8hmdf5PI>=0^rH>@gg@>@OXIP?98)|j$&bhbKBf0AMpfG4p?fXn&^Xz&JS0M1<+ -LYt|Fw@m=vb@NSk)n8I2UK}wgEX6vP@3W6ajx!SkYB1Xa0jt`3=dIh?JkbYi@pNFj_X1Z9#ZwhD#>^e -JEIxuY~kM?t3$=DgweKnk=&~q(c~$;#b^bI>kY=3*o=4B8{aFtyc2dOBY{uqt!NdCYCVx{=_FAF$d+G -b8o~O44#!s!303O!uKIvwYVUP{UghtA7Y??VJ#YkX;G0M#N6|D)n9EMoS%aayQ4k3B4n-z?3tm*QVID -A2dYAvI?C`a+Th{t&{JQgyhbyxQ13|>AH*~EzK0H6Gs1d5op*VDeySo$0Qr;Vq_w|np{^^ekGz&$?yW -~K!HyMjTtc-5deQo6dTB%pp=LbcU`V%gLfQ)B)6Fg&Od8Q*ME62*cC7{Gfe>?j;2<4h_$bKNgNbzy$M0g*X~tZTi99 -e5Tj-M@m;Fs>S?78RgGrW;0C$z-c}F409?7Ukhxn%jGR+%ujkEID%Nve)hYmh&j2Xeb=(*K}*%p;R -@7%HPvc3M{{e+0*zYf>CQTbs8_4^^OK=XxUfcBD{St=pdV}&N4nA&3vQsIe3eg8?84-e(-O5%F|~WW8 -V9cztwR^ps`& -1;>PwSIXb$Z<~Td#1m3F67TcTG&S_bThqaikA?%vT?p?%>LCBsY0d8k`4t$6$YKfJarvQ?|t7rCOkdK ->0P-)dSOK-Pt_?}w^uE*Pn}6MXU)FQ;mQ(1&QXYqbumX;JdZ-6yawwS&Ak4~j`$AmvgTuDd2EGLV1AJ^xJQ`{!A&uJ -j2H)W5k4;1FjZIkQ6MsG6?_lstwrwnuM{ey?0CNx-gudhq1lViq6YAoXmPx|JV&9hfXS6G0{cw($CwBUq9>TmzYYxGlfZC0!d(13$ -bMi#UvBNFVgC0nZST1;|V@D#BPeWENV6zLQ+DuLM@{>7*}*%By1P?&?Vs}@WWkj6N7(W$};!R4)U%qaLL6VmYCq=$rfd@XI_+KX$U1 -`+EuW=2ChBGJ$CqPmoRL0+~c!mk5-EBsf+k^0 -D7t}=f<(4F-VG@Cg!;|z(J!C!B>S_V~bKE{)xXKq(ac`ueU|5z-P!xVvA8SA@GGNWK&hHpLEan@qtq} -hG}DR)OGck|PxE+s*abbftUhuDi>X_f5TFl|cyV8zUIQ$@zX2*Y^K^5t$+Ca(qBsGOi)?8igNy}R@l- -*}m8IG4gPv%hFKqFc%3#T40TSgc9t#3SFdjw{EZ7cQjqu^k8f+gkRYDiTN*UtwX#_I1AD3=(pJC%;ug -@$ei;f4v>IyW$u|VOf2Kf*=+znG9RwWl9{pP*v)9y`(5dhw)&RPn{E^;LRKpby|9{*x|X)z`TaNk9gH -3=^NtuER|TGC8rOkUOB^U{60)dd)N6UKQ@mBYo8_sx0K0s9qFALm$+&YN8R0Xa30+j%2lhabG7eJB8q -F`Uf7#Fb5^Uzq0ShMZbRX4_1W+!l@INyV-jLte3MloN<5T|TRtFMaTk40ddP~|4ND*bo}cmgU|TzkE) -&^fgQh$$$3t3T%G~2nA6LYq1?g*9;~og2OEb%*OQ}Ymp3wP7mUyZNt1;4$=k+zETyF>6- -Oxmdz@PXVUAa~@H!l?;-to!R&H;s$tQ9bh8uLK7FbEo|#IQY|3{uWL_ -xFH8-C}VolBcxy-j^;Lpl^9H!&@_;Tp%hReaG%Q%qRrw2rNIaXm_>V37;Nap5kS;z)5(DmQ4F+RxJ{?N!Z(fq!P|PzF#&XPz!WwH7fVubH#ECVc4D9|0Lf6;Hdq ->M+kn5dw-X5fr(N({I4%FD;8b(@@*AA)WfQ%ewK@#^?z#h}a^@qdxh}$!=k;u?oU3|YW)dj3$FFlI2m -i0Uyd^Lm$6q}agnuY^K{8X|>j*4Oiw0K!#Ag -a2AiR6jiFXSjuahqu3jTj&FCiGH+?EcZZIzLkf7;#B;KQRM4D@VFc(hPm(i>~M6w_su&JkD!i-$0I~e -Mh-RlNlO;>=?^%?s7s;_&zaaIMFlf>@Nm1RsHvN?#DzTg$+2(GY@j_)f^+k(KBpHLw70GZB|C2iDg3y -KcS@F@W4~$lw|t+hQ0?ec*B_mGcQo%NQ9GkGDdz?$BHuJFFT{lQV_&|PR{|pw#cM^87;K&2g_32oIV| -obuqPFpvtc4{{T9pD^#+hLV`f)ZB%e%TLP_;VlZr$~>lXOW`p_W)@-%Rc5Y;JL`8FxDCs=vE9Dh00SN -HUIn`JOoc<*0tq}QX+{u+5&Z>xgb*fmdDRL`&@JZAT!wEGk580dF+EAL0cDBuQn1!US|j|YBuJYu>Pu -&x1c3w@5a{3*yDl{=b4rQMmjfOm!4cu5|3=eP8@*_1AHvuBQ3yPt#^U#l -ME06lfYu15bbSdYtrKiGqRB6p5Fp`6U^lY7t|s^XaN2Y#os6EWz!;3V&sp3k#{%S0W6YU6n+8}!9Ig= ->tvl-Zn8E!<<*+s7-N7}*h&CHkVp(Pbt{amL -=wOz2TWX6dYEpGDeI3R{&`pv4yYuX0YVIf5DA!QNzu(qWwUspL!kWz!{(p(Y*1}U?k$7bzKOpgUa>IWgm7#yddcUFaHUA|lL;s5P{s}5W-^23fsN4bL%@0&wv>#C!_h% -?sU+4N}{o9QA&jwUY&#*fC1$)emi^yw*!PZT-ru~BG3&rLVjy@r)%a5765Fv3PFWeo!9B$FM8u?sThv -1Ch1#+)!GHSw8@6=DRh|}Xmw=pY1BGpN19Ac+u(B9s7BDn2>bTv7>=>7Hb4$J;hsny;e*y^E~qemXYV -sCpRo~DA5(*#NJL4h40x8uaSJ2ZZO;bWtBn5O62vTX)&Po1)8cQIHuu+=sD|pGKyn)nig3#9-SpvFy_4<>4)C8LJGjKPF?8&cZ!`ZjyM+yi0;i%$|%HMCAqreqg@)y6-47KAlPi -MIwwjI3e>2Sd(?H>#w&oWF!|jtex61Bv!{F;JN@+VAAHbglqD#NV;GJmF)& -j4sTmskA)^n0)*#&!e{2YlE!>oS)TUo&NnrDt02i~*O+vS@uW0j4At?ZvAXp>?GpZD`X$?_exDG-|Qh -=`@aSPx4V1AX}g%X=~BKi``6@T2u7E_#VvB4{-qg%9ax|zbK8}?J%pejKFd=5A4=e8a<&|0FKI3fdg6 -aF^qP;|jkSvuclN)sYh*4qEsoySjw5hwAdnNonG@)z@rEk_HL>veUXBmW~jIX)dp(g2dI5}Y`YWaXGH&Z?*#gKD}TF_f5)MPEBfPgjN8ZQj0rb -g-;c^n5~eKNPnxdxns#!7rL&0FJRPU|%1q^qmR?CDz7I!eKhcU3rmxcM+N1k>|d$dueu%oG0BdT8PrG=spmDywK?^rmbGQobT9dn|+G9aEbEE>q$g0B^9OPh7>2E$2zdi!D=jXFrEa(2&^V5MNJL17GK7$~$%-`eigLjGvn4m_qMXP7vcPi!EW -lHe)I_!koS=6X~-vQOt1i$Kjp9;s{X4?7pHRQBKM&Vr9Q^r`umeCs&gww9PTk+J14{Sln!noIH9HIN# -o7f`}Iv(36*E6~|HD#eY2V@9nr4|ZQsBpsfwR=vhvm(T`Ezcm?*gqiLnznIHYUc3xbEasF8-PJ6iAzH -}GWf!jW-Lf0;_Bx}Ac1D{=Mh%Q^q?);6IOc`C*kQdmTrmtCk&EQsvbE5<4Lx*a9c%Wi`K)yQ%|5V~VY -1lPTVAa;pyTW2Sx4N0^Y_WnqicD48rW5312i&7)GS3VhfIoa=Nf~7hf70f_-%S;@qjs|C(HcvGVJw37 -^C;1^Q!&HOHl@Ypi*3!bvIpm?y`fL>S-{;I?|9q?RdjR+n -Nxv?je+)7I;whg&%wLE905KfJ5G>B(3=SsO7>4>(wg8;ID9{sKAz@v#xlLU_ZX0!M3=$ZDE-%>zyjCL -jc|hdKgVuE&-&&b$v_;e>;QNdMB<&;1F~K&c>jV(1k3C3W>-8&!hK8Vh3m_^42)ME@W>ev0oD8+V)FUJ2<&@^*&h3!hnOGlOx$09@e{Dh}v_lz{|Bp_k)vky>QKSU9CxMB -%JIr)TATrZ^R5;G<0;kSt?SsTP8Y1%CI$v7(1CcRuvD6FXm;Q;CU*ToAB6>%+O%-C98^qB&gzh`D -t=m*h)Y#ZpFxA`$`h^)Sq4)UlX|C(&&Dg)xa&Mkqt#hDjTY5*qSs!kv+ILPmWJ}JZa9J@v&OA}@$T7v -qwS1mb>=+MtaE`c!viFLaMYxX{H2jdsM&%gp+4HrS^G}FgCWRHTd{{Ue?Z?{ol~fkjxAb6+KAlGejP` -Idyi2|T>Q3y2XW#G$Aq9BcXp*}5~P6>occSD -JP7zXdVhY*=`;I(NmDei=H4^>tJ)?yk}8S(fnTX(HY5N?}+!Mt3)}pk7qpp6rFDN8vUhQxA7-Yv=o)X#|)k)~&tN*;+pUWGvFX)8BF-lkW0<&cr!yWP}Hs+k-1LJj~b4c^rR(L`Q -O7lfkfma;!`=DMk`Mgj4E2QTSBr?krmwH;SOe+;@bJ@f9Nq`f9b`6PuaQK6W*60l$m-PNFqdZenxtbp -SqbJ|cqk41gR8n_YG-J6THGCytY?=$plS7>;iG(|SZG8^8%0u2N@J>5MvXo7aGTWc7BnwSMI$RGomq4 -!1oYZpmzH$}|3wha*_re9g=Ti*7!hYbo{~E*`+qAyD>p#YqUmgD$xO_SGdt_l)j$i<$pbSBRqyy~J-d -{|D^%>pVR6yEq45ViS3i1HZZP*2L09oKt!md2zbMlg~8`mKKF-cLNfLmEYgn@zTmGpo-;tZ&+=`D19# -W8%NIqP-sUy&DP^GG2wpkgB!;NHBl4|H?9T<;Tr3k8N{)@xuSkixB~2mHfQ&@NRBA-AbXr{j*OLQ5%Q>)awH~BNN7yw!9p9OKhU(f$?)Di{rZ^&ZDwcm$lzyBMmxcu@ -#TRq$cLDu5s(gMI&=*ws>n?tg1@&1J)FkK`^zsqdPKboN!UqxwJJOxVx?L{po}(Y4@gh_-4`{ -NF;MB$4>loJMv8K$krnpjD%vB}4%)FqcDKSFbZ%E6=<9T0Zd -Y>-Uyqb(uMk|8=}C@#D!Uo8S*c`>2HJ$Cvuep&l+v%t7Z%>QIWvALS4g{sTs^@Ja)9q_Yqk#5vBx6RW -)0pyc)w>*|LS)z^Sl$mK_^DACib^w7lt+SeqA1>(eqc@F!JXBql$Y?DQIul(}DA|K{gzjny>y`)d?;Y -h8@2(uTGSs{$}TXD)oT2>_khDWze4oTezUTP9zM(pcZ1TD2X4dA*>NBo{H8(s#wTL(V=r$vY}a9c4StR2^ -7X;XUj?ul-eNB&|BW>~yqaF}Pp)x0WqE7tFr1AKv|B@LI;mn%5W%^03iFLG#;Hu6d?z7zd)>C`rFZVP -8(Ff4{H)03*R`v?_MOLIzw-pS8Uqk~U>`;(UGXcx92RJylFsC$O2QAVq~p7WOa4&hd2nnU+y5SV_w9QyC*%$+fB2#J%Gt0$ek6sD@@1|drW%KNro6zD$g9Va~+?qxpqV{_xk>S3(>tEX5Izq_}CkD1 -S0eRfgS8cZ3OA#v=bsW(NCrubU<%Qb$R -6}Eus-;b>1-)~)DlvqX)v}Ybw6;SupioeX -@8{ra_*(98zf=vyWKRQ~qDM^|8>%vo)agXx~Q*{zDFk@H}ekm6&3YQ3-L -WgvRzxnE+oW_g&?~Db1R@g|p-M?K7efFGxQ4oRSf1ukWis3lPp$to7>lggtJ{K@CL%`PItcftoA^PB{lR+rXKj;gJhGSt*m^#|Fz^|xGgP|-Bf5Ppw+ej_bTG`_+zo?)nT1s?XKaKI^{U`fO+zLl`HG4VdXOnIis_@K(R1O&OQ0{!?ivdVQTfMLiFKnx -54LxPZ2@jr1rnDM1cW{b;a1$k2Lw(uLIUu*8lOE+miN=A0{(4R -VuLK`trm-2qFHzdX=xs;4iQ6Ln<-DG32LKU+YSjZey7w3i@;y`(-ARqyfESIbg!Pt~h80+?APMMEfvX -+71r>k8cJj>r+9-4+oaQl?1NbFWelOi4Z&(e#~Uj|Kxr(mI)-eEVu{uV@HhwG>_aCHHO^gNO3T$8Ep+ -q3I^uWO$uNG|_r+}-}?*j~Ya3`ZT+Zxj -8MAJr9oJ)ee^XI2-yct`j&T=5EwX}J1bdK*qWYW7z%E=q*J+(HtdpIJ(OKf^Gs6fOTc45E13tOr=7zV -JE4zpEg+W}ggk-2pqVztVX`bp|tz=3)?EJi?|eiTKN7P*4i+PpV$q6{VZ8R_tGFz^;BkJ=5Apqxo;+D -mmoMl%SVVW9+=I0ek88a`s?J|4kAx5Gkl%$1kT!{Q7=;tjMS8`dtGQc&B~V0EK?#Lro$*_aF}RIEb38 -W>`4N78y{yz&v{m!w?SZ`VMRNkmtkgQ#{m{ONvK1te_>o63_A+`e~tvRT9=GYRctnD~^*>7A%^(jx(1 -JRc0B1)(yx#A0w8gjuRI7!7B}JS-ZUO(o@OnRbyFAW9BYu4&q)EW&+;b=tM7)y>}g)SDH?~2MNM(wwJ -p1ol+)?7sDCiy)6sPU?;q|veYR#+?zpNf4e6$%kDEI=+1B#G>p`vQENbVPb1vjS-XqHfFq&-lVn<9cN -hxj}6cbRG`11$`9@!wwiQ=E+Fik>&_+{9o%nDG4h*Sdj%w -!Keff^o1b*fZ;wx#ubRT6tq(5O=|~={up#M@huy6g{Bq3*8i`Se28xI8{7t$#6QVjCGkWVn5bOuD#ZZ -sVcT|Kn1VS+76oF&wfbL43q^w&Pc#MK4m>0_I+i3g7%G|1~;(1-s2a!Vu%x6p -aP?aFXD^pq+K6%TdUh4wp-cC*-^ -97x-L123+ZvC4>hme@jZ7 -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 -juX`?R23SEGykk7+MKyVT -rE?vUzET)GX&w`y+n8%sBd{af+VFF>VXm~s{#wC|6#Yz}pii^&Mog+Z7S>#w%V8YBH{#qeKqPVGRJ)_ -@#QrCDU}uD{BmdC6)k^{;$=hlNt*Zlq^qBqU|t$TX5%de{@nKtgP^>BfN$SQ8-73`V2X#(fa;Q$i#(z -qRG_DTB(@fcz2fx)?Mov~|;P|BRViR-Wrv4Z{<~onpA)HXwYYReE8J^+coh?L?d=diK(j2*SQCI*rrL ->Y^eF8>?&i_OTeQ>T`_M3~0geyF~-Q)j>x{I$8}@5SJUoYT{{xHA=c`c`B8*#8(OYT5qP-cSPWyqP8d -?+MqpIyn#2W0s6URS;r%3wPQ=ku(W}pVMicbv&ZRy^YFD~l(qurl-V&f?6n1m70K{K&oPUxYXn5#j-I -a`ey-OBmM-zc!>h@kl2ZlA7wL9K-xl@E*cg{s%Z*+kmVhcZ4kL-eXZ&Q|fFgZMZ6rP5r$H{MhGvS)i) -cLbtm4^PJe+R}yqzasA84pUF6U$l?c~Ycm8Xho8sPvc8c{vm$WzI`hHwF;q}tlNm_{;fNt)4VO*xI_q -)C=V`wH4lIV~g2^?u&+7EDGTxU!p~5P^2?X4S#p|2N*Jsw@=Xi-r -oQR~eqWFNe4vw<=NX{=#{NlDvs4~G4o-Pxe=Q4+f3ura%VmmLLde7p;Ba~~vrzb<&?-j%zpmo;sT36` -ifc_h2Ep5e7?Ln-9{L=9xAW!g`acWex9;t#D5za(72Klv{B-(R9AJL=i7&knkF9E5!o!H%E`c?2-_7x1qGFer7j&=VhK&FoY3xxYn;59}ik)~ -y}wrJv^ZJUI$lc9;dhM?V7fJ5ewy{)nf8N7Q6TdPI7JbauRkWrr|q{*7-e-H|5piTRMn{Sy3WoH&Ym( -vKE8bo7M9=uu$`=10>R^m#y({7c9d9$q;p{tC$k1@TC&j6hnzEw~9b{Al&C~OBr@mCNt -Iepm|=}m9aMds-InOT+KWv);ulW&zc{g&AJUaQ --lP2(g+|3U3(e%nq_b!{h$;i>42K=c(jm@HVzC^|m7a2Z(Ir%%!q3pW@|WmEc|kigRz$ODTDvmjV-R#x!M63283?ua-*iYeKRMwNddIQQ{0AjPmw~SyeLApiIs#?9CkeK- -?s1w|OCnx1dglK3@wf05sHui^=M!EB?Azokw0_Hy -G~fJmj%dMTHY63~r_VmUXL=qqKQBFIX>jh%zrer_3U8|@!oC9bJ*%UxC?9P!jAPpil#jnaZ*+$+$D~J -Bm;nUgFHOmdBA#Cf2g08BtjjGLCbGRTK>SLLX`f`pV?f2NxwCD4wQ{t=RSV*nB&$y|*!1&LqUk_Dp+` -w6bGMZ8<_HfcZC`*tAiVQ9r(S832s1e7Gs -q>D|y8khSZ1$j=`lC0EK)~Ki#E*9`(VeH>fi`VaTc4F$=LxY@t+kokv=`)3+4;;jPliB*u#pUCf@U#z -+6+k7=*=%J@Q`9-3IfWq6l9-dV?TNmZoO50!Mov1fZr@B4S}|g=2WctAuYwVYJ)RrK{%ivwdE)OU89K{hYp@ip;DtmY&Hjf7z?7E~g0w -IN4=2nR(flhdi588&jt%KTIkV;QL1^l}cH{t{&kg!$apa!r__*gX?kWZ5zc72c7r?V~OLkzVx9Gh1(Buo4jB9mHV=di@Q#Jz;#wchxr9Ss()%)63;@n&NDs;9hou@ziq>={bqbQVFSrvl~nT)kT?#=CEFvo_!YL -2+oxZ*)O!&n&UokHk}>g#2&7(%(4w*I)_x+hA!&9f$t>M^ftG{{A4PoIG+< -B>4#-?< -rm57g+7D66{N64pQn8*$&1%U_lIwf`DO$n -L8PhjcGw*S|`lKy`QmeeR_dq4C|Gay?$%bMM*?w$$hFz`gP@fgVP<~q`#kQd;ptugg}1$kE6m)l%x6p -C6|Qa;UFsu?57@6J>#@Ig8ho4)&v4*}F7lZlx1F;gkR0|t^lJg0WB*E#Ut(4s~32_qA}o~&7=-(c3)R -gGrB&^)}emY$R?H|5EEcro?jYV(RH?%BH%B&8j^yE%te7&RR4fI~2<_4 -4v{0UbyVNx}(V^kO_nAuchtbadA_$uo4jSp%61=1sM9q&JenZ_^pJtzo0JYjZNd5J|&(Q0+;&HdaY*K -o@Uy7dQ!QxpWNaRGZQha?8#I0Q3IrUKU;2anC+Vw8LCeL7E%8f?dT(K6h;3~XXOu$5Y9c{VPQd7aflD -NctO_Y_6@j}zX>_b{AEeVac3z(2^JF_2LVdW%>=G|#Xg=_6KdXdJZdvpZKJz%QUnD&8$b!4-Qf5~u5XKu`h6uQz4wePuX^jTf-sbAL+bi^Ds_Gmrt9h}(%mY;sdvKC0#u4y;A-2~ -$wqZUWn2xWxr|WBLG`o2MUshw`(q%J?4|NoP0-Ciqjl{yH~hd3T8#X2u*B!+R-D`9M4-bozX7i#FePD -4OMG+x6fDs!-2k@d^EX|gFL&;ZHh~UprvYtTds2mW_I`xI(~*sJ>uIy -=3$t-{NYx?*!nR^nHRky2i))`xy&*<~bK2!GNQyRN_60%I8i1Y^+#mBK1{$%mZm8 -X-vCnbo~@VrgwTfi2j|K0&oK#DIyph!nEjc-=ZRY@@KBy!@um;%r5ip`!cei@chS3Sojr$7NZIcp8E3 -4z5_nFqby~B1r5C=hvvPq!!;&7N1inRvxlU^U!-;n)O7u4R5%QONyUlSalN!9vhzQf@(wX3CQk-n%|i -Ah8C#wVD)MWMd(4AXF(q>W*J_bXKmdKV)NQE5ML1axZUZn=3G_YaQ(&uw?uXfhCksW!-)>b-7s1|Lp& -9n9VVG1?V%B`HMEQ*-EJo?e#Xkak_2#+^0qOSCGmf -?fm4;}by7B{5Np0m0ttC&c~v9An&*Cg!zl@5QLu{wz(`^dIyX{~s@Jb2m@_CAN>?G*8zNJnM(A4h#hs+X4HptGC$~%i2SG_-)~HOXoU|V{Ol-Ihj)DUmspIOKlB~be` -!+wDpL9r=y7;|0o0A|zo8zvwZ}g-a|uc3v}wnzab&cz@Tn&5FL1B}oou}9#!%Ym>?`xopbI~q{GA^DN -YGs)`A>aWx-JR!7>L1e4UG1v9Fh`|sxRU44a|LmQGcTFII3po)%1M$OH;G{l^^Ly6@I#!fBk%a=kb95 -_I!Wm@qqvKeE;q7zI8zZf7J#}+boq`t@%RF-q@&?d&zv)teO4E@@UVP*&gk5~2=WKUh^0H -!xW5ireI^MJHw)#}W3A~E})iZ+Yb->`Kz6C~yr_~FQD?hCj;Z?v%JHtxK&Apbe7&cWLS``D7t~fTDLD{)f2Lvg -=)NK75F&FiwVAbLAj61@`9q^j59<o33r_$S@MEHz{6KPv{@=7R4mT -!f&=_I3HwX10A^}D>~VuQuQB8DAT=7vs!-|88ssMkd+f5zSvoZRqn@tbW``sYp@Fz|5VX?x{Pb)L_smZ~Q5J-+5l5A9doRS -}uAV))1{0V9tbX%Va;X}X$`gi&_ifIp%xO_K>9DA|0U*)HD9{j1O)DZyP@n?Si#-F$F+E*68Z)V|-X5 -oFMzwIl%paeejhgK$#eBv4O5fEUG%*i1rYI6bsbNR}) -%2(mEp{BqH$Mc|Z3lt8Pi!$<27#LkijkcC%1ZrRU)@G@v-e!oa@$f>|GF_>0M49!NUJy;UdcA#)>LaH -;JE%v*LtK+9DEBGrC2mg(!{rVrPVcvbdEAXae^cx=vCsnYz!;e&ljlkgio*aby&nf2U_77v_@ -~=-_p5v$GaYTim7si|OAY_f2@*d -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 -Azqfm&l1NF02bC1X=okva#T_&3FdV& -sG&{Z&1$uWp#-y0#Z;v(@5tCLt_p~VO&}BBQETV#_c2#ukeE`M|A3MmN<ytp& -_ct*8dy+N8flJJ8V*J&gqO3h5|82@E$}p6B2gL7_xbwjrBac)^?6jdCfjKdfDAcZ6zs7^?yIRxLb!{U -cU#l@uc+Y^DxXgUaV=8B?od(_gv#mGlQwVVh)lHwJzxpwtsu0Q_^Mg%EqK}BB=X3bDwmR($`+T%@PhP -wkvJSU1_pn5*64-!XG%t23UdF{QT2DVi~jD3Kd2S`c+#JJ5d_A6*@;zrc>aj|5WLF|IlZIjn?SxcKT# -hI^+V3DIO=Ap-y}}cBP@!Nhf8tr5uqQwQRMJ8_SZRiG(qjJ4+T^3&_XS+Pd|C_Zyfs_(-3vAI)Ax)@z -LEoBI=-}vV&F#k7{{AFzR(_yrJ= -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 -)qU+`&UW9FE1k=>(1_g)a|DzneAUZ&Xu@J%?p# -wxm#c9`Y-r)_j4HE)g(z2dd)P0wjk?U!qPuuK_&egA@313*Xbr+P2a7u!t1b1z~?H6#X3V%jNVUnAge -0wc=D=ef1dt;_TlUK2-MzxY4k24_P1#qEC(S>Oc_g(jc%5sIhN6!aK$TWTKdGEYgKdT8Um8oB$-LpDh -X^Jxdcuri)KE1Gzr#YI~zWEWu(I>G>U{S!d}*-C(smnE`s`*`k7;x0k}804T;%rZ9|P6PLl`VRz0R5m -LTD7o*rU*3XPLZ(My!2c&_Jq-BDrb!p{(MHeoIZz}Ls+-SD%xWm#M)Jss(wCc-?DTB0Aq&p{R^VCe3d -sP*?I_c?T=ehX21-7T9MpLnj5F4jF!skRDPpUNu&7_}#C$=Bq0A0Wtvq<0dpK{9spo(dGa;(1c%nri^>tz+G>$-&APZ --+T}sj4om5zzq?5pkGd)j3+2mb%bH=A6e3guf%nT;W?eU-3VebM4DluFn -@zm;t!~fkpE-UP5%Q|`U})e$e&U-9VOZLsM%)NF~342$7~7&9YjoU^iz@?*pVbZBP{t(MD%@7`;h@g( -ofx#I@(4Lw(#tzo08d)N!)=fiX0O=JJd|_`kca<}a~wXHF?S?NQXf_e>X@U=K6p;$FQ-RS -DgIHTp+0m?NqP(bVfaz1jXw&)@P}0s!am8@561 -7e&ic=iPw2UA%zz(ZVE6@du!otlOE5vuA&r|Ttb`eUQuvTWMeL#_ar1^HW@@;@I7_{og0(e_m -kX^SwQN~h&yo`+SZiJPx}F!&yIm!QL*o#bWth6>`kE;C`&2xyh)2Q8puBZy1#NyugH`7v3XF9nP? -6N8C=OuqC#Mcs4)ZZ?B3Qo92IWvFz24N?`mXs@&V2h>fPc`XBT%+o^vRUg9%bdd}!)dv;`w~a!`4tx% -^RbvCuQ-X_b)AOqLxqYQ;yYIn!(L@l)>`=Zmm^1KtFNF*STf4DDw1s6xK)j4uL6}`)6&pIm)6k&N^SA -zBgR(9AGF|ZMQRbuYmO^g^Wd3m^n4hQR#^CYbXHyZU{w!o=Bjxq^p;5GG -L*4VDkvCKPJ+G&ZeWjZIjv%&vJ*#a2x9bk#9DT4F17Pj!$`itC{yX9u%=Gx#XJzTfC;hB~ -dK~|DLgJ54{W>D?z0-a(EmqLEXSBM_f!8rTW(y-(8(Pv~dMvu -MW=;7k%&#VMR{H(mTyF2PbRe^rmn2s{uhokWD)zrbKm}7?|9%#4=jKJKn -B*(BlHdwZjB0o93#UvI{@hsfMcMP4@rYxm-)f`n&h4xF_zT{{E1Z`fuMhH4y^a2@^?<&AIYo5_pHqi9 -5d(^KhQE~|r}kjXsPjkG$7e;xU|OxUlYF7$L|B6|qlxxC;Mkx69!Hq21~RbXlf>nAhFM3LcB`#;c|3X -?GKy?{^ZS0AuV_3Y;9BADRJuH$74u9G6$I>1D0b%va(C@XBab`E$R6imUi99o`zXRo2-oM^THV6Cq)< -uRlv9yP!f2i(R1azsz!Y-yA|92Ws>CwxXrTxsU7Aw81&?*ei~4k{8|E6m{If7+p-enr^Q3p#CQ=eNK^ -GwDk=?upQsYn$?u`nmOFM99N7LDSRd?0e&{${}+)I@OlVfSV -&-o4Q3EBol2!vY^DxY$c-vyx`H@WMw=MB%*02LV_V?yaPNGlv=hvGX!&n~+jlF*%XObDy+Wpq?xxaiy -2N-d_m>R!-_P^t|#NWizz&Er8ZMr#e!<(|L%x5I8-4OTcgR`8V!sS0?9`4++ -3n|#AeKY>L}@M`hg-m=c(Oblc6(gn(n+xA_xui_TXYn~uHWo&x@d%|l2;Cf+5t+TBs=><-+(RpxPk~FQ;jCoV;!w4181p -MqMAt;~D(s)|nJuisZjR*%cZ9yb~s9@&1APPQ?m`aO6a_nj`vpBiUUaEBNc$xsMsMjy?lAt2mvZq(B8 -d;-cfkJ=5Axiv$1l6%-kr)fZs~8l ->=zgH-Ix4!qK|~IqkDuv2!fyy93s&}(i1{S0wQo6BM=NCDH4GobT<{>twcm0=}tWU3C)iN4)jBXkbjZ -Ngg%i|bi_J`$mj0e;_v#eJtEp&M;0A?2Q+bv*&XrTrwFhgjH#pLm4c6h -1fNk7AeByms)?g1eFMHLkJknrM1Tgras?mn|mV0X|ZAjh9Qay*2);iG|N_k9OX5q6X?_B|a=`A_|q5F -9-dQ2C4haxnGy)HY7X&zl!y-I{G5MvBZw(aQ% -#R!TWm?Y>ykDL4@7~tCX0wvEeeJ$IbhMW}WI=V#I{X_1e0xyi@88fCDf+J{7gT;>t@K9Uf}3@yZfZbu -gZsnnY8JMe&|i2NykDR6Rh2>J_`9`ff2-s6{%#nvdUb2epCMLRg_k3#=ozi~sOAlJ@_;!@cM}WMlV-e -&l+Z3$i8WI2jMv~~Vd+Q;lQO%5_rgwNK`f_x{}f3M;#yB;q3V_agFehI)wG6gkIY?)_B$q@j3N$%nP8 -`4yGapB-wBh8{duD{0!%^;Pp-T9ed@ygbpZ&<&tEf<%rYWj*c~fwIbn?1fU(zvwaQ?_4R{Py=yW!mjr -Ms3>h4{&#%Famf%c;SFk{A_jS=Pk?#u6u^A;e1)xg4jfS2?k>yNy$m&HPFID@d57#sJPa2vc@qJTL){ -T^_O$(}+PPwtu$49M;~!dE{*&3xBeG}m~@Srdymf<`+@~MQ?vq=DS^zr(6sf2666Ajyo>P -h5Qx`#HK^Mie9B!gtr2fZnd4aJt63^15l~;TPlwO##$s)Tl^>A=1(t8Y(0k;mZ@ix((!O|6FWa%BjEuX7FaLN0w@&c4U(0XG?;0!`^B-jPu8v66D*cb9}vuY -glSH3*Y6d%;(c2)c{gM6EygK2zT(6m(ziY2mCTD|tEQhmQIK5V(3 -)!(Z=i_F_nhR*dmna}GP(40u#vW{FB`)11{yI>Hz!Q;mE`+M4%sRN?yG_(+Rle7RO{Y#f=O))=Zq!MSz6UoYPA%`cg+l;}KQYk`9y3Fiwsl(A%((8^fNl$tvkS9JRPQk)tsgYIC8HbLr~9(;v^0+L>3Fy38 -_97&oVQ6UpQZu)gT`ektVu(r#mNSI-9VARc?v+iX&L77Z@7Y#a#A_vwaIcj4Kz_fog$Y;q7zj5DxBJN -G@5Y;1rYgq3wYS0K6&O9!RPV-b0{^ThcL#E6e08#>C_lF^2JSzEU8bYTe%2k)O(R@;It$+hFRarX5cdGfof={E3r(J56pj>glg -Yc4P1%qg23>%!Lfb_HlEZz#Rr(Ib|b3cMPfJ=nYPzThRkg7$Zn&*&~dEELGch!HnFKGavKN}&=r)ZR -_`qX_o>g=-bQ~=rPy8DH9keH@N`!19njV0J;Vrs`+F?QfGnA-30Z=)( -pNFYUcv1K^I)JS0JHpTrKU?e*#sNzeiP9&c(G&`7NsY<=}rARmp!FRTV>dTN$fLJnVk5UT=H=FN2 -IqWjhH)SPD7td{Uj8c`tE^-HQ*KqoAX2|4sx~rQX9ss_5T0@j*`C|Zv#%&%u1XluMR_F95h)W&)J$$|kKhG=f%Mt}2`-vJ6mY_sTX8I_N)?$N@ -^7C)_1(CGYUvT&wGeE3kZt{DfeV8;aS{2m)8~iNP4df`mWZNJH>j=BY*Jw+`O#*B*Rm<-EU( -bqe9c)sGjgy^@0179=TLTf#D7;6b1CyRy4ZV@zm6JlR7L;3W_#|^qj4&X+idBK^Z2*;-=4jtuaHx_z#cLSXMbmD@r) -b=RXt}sz8jNpJo|VGu#mRDRL2GS!>W+R449yDQt-F?p?s#&p?xy+Xh-JT9)eMnH9R^VD+$I>9$72-E4 -*rJZ^Lvfu<#D>lRYAKe+5lu&Zq03nu%{ZPbx>Yz3|x;a6TDKwioJql -5J8j{+kpmhd@M_HM0f#*+B?LAF@I`I6qnJ|c~@oFrb_L5lZbizo3x8zE_b=n2FA?+<&8O?;m2)LcXPz -Au07@YE`v;7-6tbugBe$Naj7CwH?a`9Mu -OOzhPdg4oE0zTjn^-+$1(4T1XAn*KiuYQ02i8@f%m!{SoUPaWWcJpbU&B&in=%a%1Nn>T7+3_U*j4fs -Rm4Ae)s6zniP>;$EhR*%e{ptrG!jGT#&E(XPO(T(`C-+E_<;UgW{6nbyq4PYrvd|AtCw-KCQtHT=5%e -(vg%N)nRbAe2t~+?Z2_mHIrYhNBnKVo{Gl(G*AQ(|4BjWlQBOi|^|>+KJe5CB?5uAg^c0$=2zm4 -XXYEgoj4z?mGazaD(beIN2~MsaZ3|t%O2F5(cT(+wOLNwd<$^N>q3q6;Couil3GZ?-x;uoyd-Z;4yo$V)7mB-SNFL>*-A#KSOMJaV9xR=gQ!#A)6cgc|)TocMdj;T^X!wZ&36~x`sM4S`kJBidky|jP|qCQoLo2oJp_6f -*!n!$$C=?^-_eMFFvOsH7DM%_=h1)#RRHG2%Q>|TeLuCd%)Z78RNZlFRLV!;-T -JSP&s&6SWKwSkr3)ldAvx8*H6_h^m$%H{WXYWNKp-=P2~;mnmPmBW3XisEV@+#EW45k$O&wRJtRGFT# -|PVabEr<$!YAf^tJkP>CbVAwRcws}6zp0bymvJ;6B<6f2UDOh+7iw)!yOWNw4J2hpR ->s4i7c_S`_#ajzp<~elL5hHi?zGl5_Pku6O-U_&{G=ujzq(p?6WG|#3+A6!K8KP^!?Ue6?5BEy;wTa! -Ef(3@L2pObHbbwacYQ?Y(jQOJLOHsZ(4#FXj+CAsbkzB-Li1ecZ=c`?@J!me!7T(a0gx|HrTCXaz15- -+_>G$k8JF`LM}Da7SX#4&Aqs!Z{Lb#^iOm;UGZuVl2d5=#`uT~0Ai}sOA6e0A{zx>2Zz{rGf+E^zX%d6trm>;XpdF(UVGbhlPC -w?k$1dd_+`*DjBjO-TCt%$_uD2T(+{Bw#xsFAnG^p&5V|Z{d3$2LJ -FN>$tynTWLFsz`zE>BY4%VQMiU(9}N$kKnw+!xp*e&H?u`L}YH96guwJpZqBKbGvr{#Wv7zZEO_PZ#? -tUGn3Fe$%;$qZE!}6ih%A2JMiGMkxYAArvK$9eiOlgkv!MJ@j<|Ch9<62cAKXR1k$6MYv-Gh&<%Pvd; -+k4uq)uCp!Hu`r5%7_TlE)fzOVN4vNv}$S7gZL9c`52PDg&5AK2vl295wn9cF@Cp7;7`q}~E4xf(174 -ex;M-L_*DmmJ2_E#PkppU`!{e*pQd301A@y}E`jD6(Nk)t$-(Vsu~F~}5sgw+p~(fvm7KkamB4x+n`_ -yv8LXIx9JQ-Fuy9%7IuZomh~{vPE9$2Pz>(&?VN@t($NUwTUlO|!(Ni`=92(Y)I?y!sUmy6iXz1wM*M -$4R%(Nv{B*C(S-+xPHMw6Cd5u-aJNa&McdtI-Im#E%Ockb$s30es1M-wE6mJ$92Is$gh*ppB^dcZ2kT;f{YP@g -U%k5-JKlJE2IR$TwQT~DG5vuw{WZ*$Rk1CypKNr1RiJneS#->K$91zuA0`xy&o&A?2p89{WNjr-ang} -~2onW7VOVeMQ6t@y%YJT3JAf3Fzk!X)Y*HK!!)~7CMnX!=`#_$fS|hWHdR3_ -Vd(DWgX$skmBsfHds&i)oEIWNTh)k;7h}LqEjnMjb@xM7*Er&5Ive;ib@1czH0s;#m|HQ*#CqYKgo}d>B%yQVcsFRH0Ty4&*Hp7fvu#cyrQE?VWW88CuwCd4@?QNS4?)(O>I{kjDS5UQ??uj}lepN8EPi@a4xXCl`3`o3Y -p1z?~uFWX;WX~B1p&1rTqn|7D1CR@Si#4jHgGZ15Epg2sx*zm^(kcGM#s1tW{#J<^$MA!doTgw1qHr7 -~QJ6+3l)`9y$DKHa5afq#?sra+{PcV7UJ!*p6`wE3i1aA#pxI9}eRR+qqqym3IQRSQGdX??6ht2}utS -H0I_7$lkDuKSLeN2uvOBdj`6L9W!{P2u3WW}Glpi?7FBP0m(FjWpV(i@uLSJ4+90epe{<))bcdhYJyE -!r``|3wciuka=;77$OhClNg@NtjWhtnH9(iVT_6wlV-6cgm%PLU8Iut4+2#ZxTxq$?^rk{qM5_xt{x- -oL%O&1beRo(1?%9OB_wfd9lH9-aj_G6etBAs)L1{u76I>>BuI4pE!a57}Sx1im~~nPu#Z{+2>IoGK2c_>fN;<#IS_VOj;C{M`SoSfLg(Bgx^G_gSggyKWJAvT{%59vfUg)njU7|30ZAjJ2bLh^*FEa+bzN|c>_crR#EE-b?>s%-YPWt(q -4^($7yo3T-&xV$F7j&&iXk*gV!Ne;AdH~N-GY9i43fqn6oC)~-+%SpoNk64mWYau!ZnT^P3<^xXdPk1 -r%Ih3dDA0j`)Q%au0yGg~PkFs(66Q2KRR8f7Ris- -8WWwv3#pNaL1&ZH{Zi)3~L{@p<`w)k&F70fJswMOZ$tNOJJ^GASK#V|(%rV*5^fYAkA<}S%Xr0WbV~i+8eEuado -U8{0x%_(~pr2_2%nxk<`=JfkA?It$bzNV`h-%kJIZt-ypRD0gobRPhTaCNsFBw2i*B4bimD*Vd{5gtZ -`!ovK)Gt}ZHKlTxBh0$}%sw4vQDc6aLE(>Mq;IQyuMs+H_q1EDh?W<}Lrr)R7DK9$BO -@|v#NFPUF=r~SqNJrfWl1zU4?OBPd#`4&QoFhnr}xi{xVQ+1b4#Yq`wSW+t_ypU7!B7IlBilGndA|Ry -Gqdjq(rn{h)sd}N1RXGh&Q(-VqcsXS>vn#QZX$ijQl1xQl;-a{Yuu`$IJ1~G6#A_P~Z=Mv`BqwBbEDZ -k4dhkhet<1k$hVv|Ncdcg`#}-{2(rzPI^LqKJR8&>#E0-$W0eq9umyeh)&S-47x(OcHw>fgU1+6 -owKQjD0tFIOuWGBiV(27#s8WBZre6gIYTlhtk7W(ZrEm!}oyT`vcSDXC`@fK@@#BnLX&(eIJp2dcuf< -x^eeZ=tpxWr4K>Fga0}``r_&A2flB2cF|{kZFjr*XIlDbq^FLCu>JoB!BBSW8AU!4iRe*0JvzpsqvpA -982V@f9nlW`(;gimpGkl$Kbp+;mD4}%5<45m>{xR9>ibyFyS)u1>p2&m3Dbb1Nl2>S1rNo4D|q%qD>9e2LBesZ#9{jtgl`EE-YHj5~&zz|!}HgdR=q>W~4ptp4Z+vx;67+|m!oen7ZmfZYJ__e1&nLr|9zwR^GNH@b_)9stmZ#j>33%Hn^k -^eH!&Q>AqdAQnj}#YB1w`)ce{D0lR+>+z!-vlKj}h!=6sSv>Ga_6&yRF{j2%h-W9sM&u><;P+Q5Iro| -dMEy**^j(3iXv3P!e026qo=8VTXlZ<_-yqI%LmYfHyHj@TxJ#DWgbH>sE&qC2|&c -G&NMhau}zP^=>LPniu^U?2Hvdd*#Em{xGa!)9)0C#?&4w1}=7r)kSN-+Xe9GGnMELm3i>G;!ntuQW{d -ND8+7N5~dng)rVZja!~XeLmlm8%oxi}ewi4rGv5g!u{I&Zcwmk=feRmarIqE*#yahGOJW55~f&CN$TM -H0XQ+&UzZzk#okM?LDNCS|QiM*;<(Q?moR*iS?jsniBd_ih{I)L*G@90&D%q?`7(GE&;xn+O~64v>r2 -j#;f5KyCkmK*B+x$hN~_6rc8a$i}nQ0iVZ=nFcF&(E-8Is8BPOoYxQRAgvznH@^MDyr;Arf%xK=u&+( -=Omf}WftAO<5JcD3S_myFlW?sDGyfq0k0A6Z{G8u0)%v?yZ;;Q=<$r|r4lMr5R(SNBO-{F|krE8ZDkMzkuHa1ixvK&Jh>k>oNJh>!H~1@jk$k7q(G85ylY@osn{ -jSd}$pTb^k<4(zh-82ZN8ZXS=yN?K9doUk>=c!YIAS}j;b6}jf!2{el7*uVn37ub($HDGF&$=%$h5?vEk)O-jiIp;o{Seam9?oNROmv1dLVSX7_kiF15mYoPWNESm -{4`84=EEcB{5XzME-ob)pjzWmJ@l<2!7-y1cL$4y^;p`6_b!klfRYru8<@nRlgG7Gu8RgDjad^kQ(GJ -qxnsvgd2zX%oKKY5fpjR>uci2O$48LfSx@al!S`;LNkfJL`^C>QiD6?a` -SBtWt&e{Tq3%65CgVnDRUNx)rfnYaNO%iu5*;q1%nthRWC>(Y`uBfU;02hrzPtJU`v=fJ98`lQg6rwaq8ph@4dS4z&sew-(l410f0-vq -Bw;e<`9h*9iRW}XX{NO^oVEpS)4n>1W0Zl;~oZl6SJ`(3Jc{&0o?#W#vi79=m?#)M -zOzq`ZN=^EaIgW~J%+=LyK70p3tl2l*BpMcK`5A`QR7{iucLynIQ=8B&|lx0G+?m(9DX`cO6@9!_nUB -D4!%C&AdF1;HD)^;KOZvINcZ~yxsc4brFdc>&Gr&9eh%wrBnhXV(m`v&J>2XWXhykUQKi@=}VR(Jx2MypFhD2VT_yIh5btPtswUTQrbPbNxq;zpq4`3%sk1%ffj_a$xQcypT -arQ1VRnq#L`|CxSYGc&5KjdilT=xkhSz2!_G~PT9b|#T6+}+O-OYx$BPL$7CZGZOm!eACM9Le9X`I -6*HJ?e)`(cp2v9`tBYkBxhw>^6O$ONc=9!+Zn6Q#cg2Cv}1-Y$=(JxLG&|>3c$YXrjP((&Q`RU61yIH#Zjj56DQPoRBg**n8Fh-XCjG55`6y10O(QPNoSBx$1{4+~w{alVNS!tzxwgH8X*z -yOgS+@Vtx;b-fn->Gdr~f4JesEqtC^WUNe-N;3X*IWaC6gVXMs2Vn_>8`P!Bk;-ix3?rb3DS%~OR;Ok -`CCq+YMu^dkn@f0u)S-_(P@0}Ma7kQo-(jrfVctrxPcj?24@qHCS@Zh%>EjK}$QW!@bW+gH}zTGwtzKier$Ud7|d&$rQfhQRq%I2UE#pji -j0xinAdeUZr|n#)p{m-h)ePbEoM;VLt%BgGmt59>`b-=8ig}*J4HD)optxWgcNt!^p}C5Ywy4RFtq+LtaF -O&U!QX=*LMD;XVd%`>TmGjf4bH$F!3*~^V^9p4Bz1)Nnj)eLpViJBu>K=3Gc@V0w<~c#~ -PKafj6AxL6Z!zKNPY}yMbXg@kbTIA(2u$m^`W`lPoO@yl)#RP3yJ+iCr5dP`W%Vj;|P%+a56!U-hd?D -0qvjSMD%D0pnla7ki(N^tkv3x50PAIu5!7{k8oo7&4`!hX$!wG)-NFHT;ol;vTEonTjr|U2q%A-v4%GeqT&V-$^?zmx>v`;zY&s9LrOFVQ2kYSn -mgs6ZoV1{4017_!%%VeCzu9o#c?Y+#q%FIX|8@J9iuvHt&d3K1)_V+i-nOw` -&=mG$ZOpdOWcJYDmtQhNQmUEfvb6YZ6h{qJ?}SqVo``0!T&Dx7uIVmjzyxH*}qEvcf3F>+(poIshg`a -)Z{1b3zlT;2uOhQyP~I-!zSxX^w*1fP+EO5OYd}${fjFs&K2t5?MJ@KkXQ5TNI8q#AC -4wBoVcW(c%5`?zR=w;0ugLt!H084NhO{dcpbP)7%i4nllQ5j!k!ijLL9MYs6Y*gk@HD6_D=rA%81J=4 -GtW$?hyXkitw?TKy%mgwvZPXorq+icdXI-Nt9V+qcpra#k`l-;Kq*iZ}l__3Vf*9?+F%1%HL!3{WVuy -orO-aId58U^8-Ko635D@HCs4kI-g9BW}dFa(@NKQSe5}UOZ%RB2T#6LT}Z(U&91%i^2=O$Z8Dg~f;)m`2?yQ* -g58|iaU#I%l2hU07ps@?fOMxT_|=rJDa<{aeEM4O*gu-hHNIa}qFWs$N!X3j-M2rHD|qI7%HCm90Z_NNP|fg#&bF;Z(@hhw|J$Zm -om*v2`NnsYTt+k@2!1vv@2Im>puwA1zVm<#7%mhx_@MIp6^Tvi#Q?C1G}zMt3T;@J;T7GWQqPh9%z~(CrgEcXkg6;zq5|P&oBW;L)H5kyr -FA_C)d*0AC7eoiBUbPVO^pzkKHDJRBkfe9#Aak5DVmhYg!Wc -A{z}Za0#*;LtRe)Ri1Uzjtf0_H2E6k$F@uL~Z<^p>5s)AM3n@$wuU5xx`V?m6$OjcM>lWm6nljd`uY% -Lb>{8hi1s)ydxbRN3btt_BIy~PvGNgR{sbWkHHce$zmo4GgJb`x#31@C!^ysR3kvqj(hcl1IC5TfUfz -7dTiI>FNyu)LhM)6Q=1Q>Q#UyksOkKCz^T84)vPDB{%7W7hBscTz(Vg=^>r1`Ke)W%fy_y=ue3>!NyxCQ;ZAiQ%kqhP1R!}}DhVCVMlv>BY(>cA`BOu>!Fxvd&)Bza=J4)_bih^8CE9tXFz&~ -OKV_zpDFZL8_tHV45QtrXoSb$#eqBKx!GPEifx4P>rot*^m@t*{)!o04;|Wi|hjNKPlTy#s@ZzeE8MH>#(2DH68Qof~M&B7{^UD)5LF;h5T-blF@ZSG;k4X|CGwQoYU2J}jwuJ{F}}ChixSKLdXF=@06@j-z@lLJH -|m4@Mm%koh4*WR5{J8w^k)70b>!zej8uoGCVb$ecO$>9g@B`{746rQ2Q_qEadxkAdmu -`vuCQj+&J|w84zqDh~N$0n=+&c?o6`gy?wQwqhvuuE<7hOXKxT*d?c^V$t1$9;{$IxEmTRw}Kmp`?8c -zgQ2Iy`Xhv%gPu{NNNCMb@ka_1phg-~Mm2pP#ZS|LeCK`h&s#*Nc2w>OXnaclH@YZ~{U}m_jKS -rf?J`K@y`t7>0HoJc=Mlia?2k#rilSl<8CcWO4= -golvN-+@DD0Q9)o&%d!CO10&{=TRthQn|BEJubru+5EpmI|tUjO74K2E@wxhH1e#KSjFa@&*NJ@+wka -9-`_#%$besv@*5z?sTUbJR?FQA~YOp?2Te-`yGTtF8UrodJq)-}(f;r7wQos5RV(I6b5JLGdvVrMk0@ -rkIsP5J<1i2iU;Z8PIjv4<6~*lxY9>5nRIpz8$~;kQI6e;`?Ig!SG=@_2v<&l$|~# -c0{E+Z_IIP!zMa)37*m2n0x)Q2Iz4uBI1UGeypt_z+6vu@Ec+A<1*LXd2xhdxTdbsW(fuiAJRV+Yf~( -)G=mPg^YuPi%5!(8NgbDd6zTr{6B*P?SXS@*3ixb*iP?Go5=wURZGunODNiF!N7Vy -evo)eYl7)s7Ed;Fh@hMCTjA~|g_Ukp#1z}{C$x1v#rvq%0Nb*cDZa5Lw|YCc_pegxUjGtUp0-qEV0<- -Oo=}}p)*A-M#n>j1qqLG?ar%*5`>)Iv^#5P8^@pwfeY5ow*7dif>l9?-n>-RxqZYdP1~Erubk?bwrV>hcMB#rQHUZREsea2s{sLR0dd0J%2antN^Jm~ -rB`r(o1U+Ss*e`f<**zWgOm^NcYLpF;ysM>_+-d7zwvF2)I<8af15A4FA -*&eFPtn5>4$uZP<^&>r^?G3p=VJGD24D~to7uoMLJ!LUeLfe$)61kOie;#TmZx?Q)(@rA#=b<%!a^Au -sm83_vf6ogTa5<1r;YHAH*TT)_tdG -}|0(@g+uMzqRp;qj*8ZMaH*N(TB{Sv03yYMgtZH*{sv=xU|q=xT=oYit|ZI?$YrNT~Sz~`0NDI+M(Sg1)t9kldIOWf~FTEdV$r@mOGunvFCfh=8x##McFWU1K6 -K+34Q5e>rgJRsL^B49p$1-Lt3OTTvocqpgsqOXWx$_+C?mK99v%TMq<>$1cZL-lek69Q-1F -s&1NR!Ly_L^#_}?N)nG#xvFQ-HzN3)Xo{b(G=!`r~B&e^vin>*xB`0gQ8lpg~PD-y$hCkCA<9isYmtRXSrACHXZL8Unz!tfSN=i -%@Ib5d`wjgY?mN2aSM;)b=QE9Yh?)$iA%b791JQR16N@dxlq)g*AOTI4WBo`oaP_AxUBu}RQ-DrCa*OFW3j3xrg_E#j_2$FQ`;`UcBi0z5T4BWWGo(J(j7 -%Xg(sEEdn)HQw5Xg=3OsRLoq^B#c^WpH?~zu+p7iGkx6KJ>;(On27-!yM#Vu -3Mv)@-Da@Mmy}9{rPkin;e>$Gda_pix4z<{ISCSY1^uS??v$j5^mHn|FFsL3%t8ZQ|ne=^j05pDBcQX -5-$#Mh1TLUsvGqrs#UY7x6@SrZ)@urr -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 -0{Ct}+XlC++X -K-rlJlDg{XUFk3#s>ci)%;`cyEMHU(o;;gl^f -eDCpKd4B^+x!U5-gXJH$_*Xjw{%VVVwL@Ur;@?-M<6PGnRh}bL#%^rJ0ui9oiLPb`O}HWW9qin!5Zt8P6FzBN(R+!r^eG@O-|58N|A6&Q@kmh|=kJ#Nh{+!Pt@7DvvKG|AzKu7p8p|<|*3p(#?9rSNhPL*_dOAcTr-&bHxd#F13NHeC;bzNz -`kL9`sB@NhX%j>&>oT=l+>n*1QJ(bwp(i1%lTx?=w3aTrMoL5#ynJ1R`hs(B>>QS^Swmq|{#o^oV&v` -8h)dR;Crbq662?y_=7k-?A -C*#fLVg7?E$=5;$K(nqm)Z;5B$0RFj=bAG710MKjx+aa$Ux$J@j;CT?uw)XkWr2|UKv@M0j-6X*p$R1 -AvA5n=9WAJ55_PmRm-VS!;gDDJQ{!J%F6e#os_A+lz0-7nnI{Tm{n5G}%;)+m14?b_t(u)5boexf5e( -i9g&(Z|Wvyr3FDKm$k@=(xIak|S>|HcmZw}dNcNhX?R!9W!M%L2D<7mMi{jge4sD1LN*Z0>0Ys8vjSh -y2;Y?Hy@g`PdAh2t%<>sxhxLeIzN3|z5Owq#&PE_Cn;$-=#j<~Un|nTUqi9Q3r2?QtC3?i -&H8I;;ImR#1I9%rB3BN(j>6wObrS@lA##5uQ2*#svl2H1ii -S}@YCfmbG-$Gp*_p8$(_U3(DQ!7E*Fca2`9p?$C -$RQ>Et1Y(Fd?596?qiG`zw(%3|dRKDU^6HA=TJ?-x3bz|(T@IvnYSzYV`M$uRlf9LY%JWd44B4)f90^ -!eNxvwyr;Xon{LfKq%Dx3BIGa>xH;i+;khzgY15F`)#3li2$EfN+c?Q3yq07{Vx+ASoOtuoch|82(cd -O0Z9-2-hGU+9qLLk -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 -d$1*DvQY+6rm%+xoYgBNBb>4p!Pygn&9fL= -=-`CR`Bg4E?TuVTgy3yml@JX9t4?!ORV#)XGiwpl~2ua%a^b8-2uPSx}QKGfEtG;mB~iEbdS^@;ALtg -ELP;Blvlp1%g9NITm_=L+hX;tp_l@94J5pE0)jn*I199;8w*!GJrZXG@}lusW7L^Vn|IgM*x(-gDK25 -&QW5jv`)1`O5801OILZZFlETskD0U%XmoZGFKu2?W6`12u;q?|MWG0+2#TAZCad08k<9(HiZtNwl$6l -3}R&{Ca#Q|BGxicogZY-AY$qG@gYw+eF=_w?LdeEP-N}F8fgoI9-*@}UMqGAZ(TYslzsrWeHVg5xn87 -=3rLbOqGc_jM7B^ew;vV%075ySDWz6__s(I;);mKtC^i<1?V9^pg2AAq8u^r1vmH3YUX9(UejvhlHLwh;45X$o>S%0)Lk?^KE@ -9y4lUFl;_d4V<76+GsI_1BK+N^yI*o<=#xW)_U-b_8Shq_v)4zCT7eSa_I5W*3DvWhNR}rw)gEl`D~= -t{ImgHuTnh&d%Do7_&F@1ll>Pi53-9=DYNfLk2~bdRvUBZ#RYC-xJJ|$>$gUI?w~jF9a8J`Z+xTe;CF -5j@E2@o;GJ3xX?fX~-$Fc1J>T*0&mK82#rpsj_CxHf{Q`O~OU>?!hIU@yA!fAPHZdTdf)gU>`6c2&)@ -i4~+*amzoy?2rG8sWCYRl`E>dh2z!3p#QhT2wEwVGd_eG!u~Il)_SPhW@HvH!*lo@(Pm!pd`gk?Cgew -Ne~<#i-#_w@M7Bs?>!G%XAmbDB#zlDZQsTu4ZMB5kJmx9pX$5r;wA@Ns#r?l4myGHGU1sxy~{8TMsLP -0YQ^O_mqp?U>_z^{+hYq0F4xO4DqtLOdo*CB0~LrMT;<2p*?z9^8YjZ#nKE9{4SOECju6gCiPJ28sihRDp&hbXx*B_Z|DP>eYxI+bhhaY|~TahpS{0_Z;t6F*r+vUp6NhNQgp&U-Gp!( -U_k=gHH(SdE_2~>#>-|rO$#+99R^5o_*ZUk@~W)hLgjm71X-EBZI(LQ_siI>huAeU!j*bFP!|}0%bo{ -&;L)u+5hQ%{2NI7!yf(;){@XZJQCfM+>>zQL#NorxL%Ph7;n9^E1=!v@8At6Ch1RMEtPI9xCFFy`cnJ -EAZi~yLB#vaM`Bw(j5gr8Vq~1$@MainC>Q?~tVKWiZ?Jvp&nC~e5vs9ZYwcZeID~CU*{$6;+6s-E+Iz -I&+mPA|p23E&acs-9hS2unP3k?`lD^w}A^(FQf7xI&ZLs$K{1(=J-%ztcxaUtGE%(vx_wOO?hTwpI3g -K=X!+s(-ZAEavzlF3nOXb!bj6jIoJ~FObkIYBY45V<;3T-#iPV`pxo*8{hwr-s{pU0*wX!lws@H?3&H -WYoQer|ct6F>P(EUqB4QNG!ByKnp3T?7AQ+j~Zq{XY2$D3(6ypV`-?mwe~lZFO94W*83^?!E(UkMpqJ -dSdLwcZAu4v&?X?WnCBq;ytB{En6RqK7X)-bdcY>a@0TRi`m;^&Tw&An+o8yKaxJ7+Q#?T)Y$L -X^T|LM^AJ&iT{T`kO=Jr%s;8)tae@lAzq89**%Kn+=wdOM3xe-1duqeA@_vatxa(wNJNj0IC(DwN*Zb -9+@D#N73B8il8T!SD7e#ZjJ1T(rob9UoLL&_3>>-(b -J@fZCit$D3uIU5IF6q2gUqp4t~Ce__Wdd^7yXp7Vfr9o4 -N?hU|lXKLo^rt;z=Pot?qXx7;dm$gYvUi+e%A){CmGfQIXytxh^dA2;|*fIgdtaY)2Gc^b`MEpq&=UmpunX|jiE1l&LSWuh^VJX^e(-JtLD4 -ysYK#PE{>!93`dVP=Q2d|a88yy-?jkSRE{?|K#v0B9=Ya4#XmZPJb?kNn8z^?hWD)_(`qqxS -3hnwIZr#KDJ@g$t%|btV&qfgX_B#Vvrwj^+)E$=b?F>yV+ZJ1K{#GJsa)mJ$Q@eLI3&p(c|$Nw*#%OP -V~ihXIKn*UeYg}+cp7RM{F -cb8UQ+FB8qk|O;>Q#|?s=9a}iMx2luztAL1zEpiT1Ryv`G2WuktxNQtr|>H?7HL?e(u -z#V-f`bIQQU!ctNcBa$6?SJn&=TwAq#WF%4d@O0sF}pDklhgV4gwjNTE}+Hj3*pfq_z6_tj(U%v)W3L -a~yNysq-yibDkNgH&7!mTbDtOj_Uh-2`OmX5|>q}~5&lmzNC$+g*zZDVlW&;U~p(ebeDt~!z0x_}11z -pZB^uDe2qXvqMV)sWi!~D2ty&+D+jvDwy8Zn8it{&}5UEZt}w<#T+&PLEQ>e=wcC!1V?xesQb6IVfp{ -N~Pe5-R$B)KiJyS+wAudi|!o&>`*5M$?>5Nq}WG2Xl%c!)UfU92ZA93Fs}dLfnB^MyR%)0$x_@a_wsq;L3WBd~#K0doAkqtCo3z^n>)P>W! -sA*vM0YZ$tI+o~MQ+m(y*f{YgFv?V#q<$$`P@-Q=wPJ|WJjd)8-sqwv;-i?jcOgnrt!=MSp<5+52i?X3d;eIGOv~=;)0+K -lZSB!D@v|BQ4Ogs2J+^*1g4vw0hw2a-R2*sW#nS*EH)|&nb9cL|Yw|6&@_X6F6KMX?l+lIf2>|=0jC4x}U}oA4es*^4_d}#-vT%Y`iE} -NAC`UzR+B%Ko3h~K_UUNveV3_A)J&!0F9l`M9#sHNmOtmGlVg0(^bYU>;RVb*+k#*Dp%mkF=#qK;1gJ6xYG5`U(^p`M2y=Q$4Egv*yenm-XrHIhtwJGqZdG(2v9|>mOJDA -prg7S3fQe{OhEDfPj>CNyS#pD*ObjtTxW7M~e*S6zi-?lb+*Oy;_GOHC^=TqCZQ)#%#b-MqU-qE!awc#zbmGZX3d9 -a>TIE~g1iw98*fz9(85TFo{u=FnoQv#|uXh(FGw1;*i4~e<#Z0+@qn`)W-SM5bn$KGe<6JXAl$$KV+A -ZeNF@}9rXoa8s6p~B0Y9omQq~zs}a{xcu^4(oW)ryOo#TI--r8swAP2#UA2lj7GG<5n(#;K;)oS+Yd{@4gN;M$uNyV-}kp2dw}Y(k -@GT?NAZkc4_ldm3sy0#+x -&|tn@GATpa{5fJF4d|*h*pyR6Sd>I=h$sjp+`idX;+#95)DzVe#y4aQF;vBGH`noO&PhDS%QP&34A*R -rnZjcp0wrD-^U%*?v-Q{tB&FoV}tawRpVGXQ1Z!vcl)Q*@0!2XxsHUYNu4pJ7X!BfVz0ZGphI#&6qcW -l-rze18}UAd+=HDSs?&M -FRRP7;ARDn_I8Y)9yO8bEEo3B=3y?><*G=>6i$vPIdz##XmsrfMhD|v>+3Lhgr_2N(Nogyyi#t-~jw* -_*As!)^6*xrBk7_FkP3a#-@+Q=#>Be;uIHWr33aRBeri52S1LRhj#J9y>=iYh{`!SD&_*o3v~Yu^v~zRMtOQo@-wx$75ow+&8?f$ql2Q|*nsidX!wz40s58iy9sZWQw^8v+aej -Q!mDnc3gK?N*zR5Qk8t8U4SrHRi`XMlX4FeJBYihZbMpes?jrSc;G1Ukgn2c3VkS#<3uiM?f<3#TytBd1S2R+A}9pm7>-gXv74x$ipOH;r?DmZ)sK%{P!b>QGDz}il -p%hlsG}x;emLnv>era~ImKx7A@8J*#Qp)c;!pDbL$d`XNAjPD5Bjd;BV`zdN9O9F(~5tE|E$J9#YeMC -{J8-vIk?2A5476h)DF}955H)L9+%ksFP87pyz}hcie7mEsH3lR79Yj00fpK1Y$UR1~kV<`h8a(_+fAQ1T>Q7|Re^E*lz7Gxt_P8FP8}j5KDWo*H{>ICKv~>l^mnput^DM -Sk;CfYu2~qC$|^5_mLg?EcJWbd2$+NF&!HUZ+Mv+StC@lgVRAO^<9qL|PYCn}sWSmo&5xrhT-7c_ce72yK)v6p!rI^J5Ruxvr%vt4U=F=wtTc3&<7xBBcouD|K6$cul{j(2a>5In -6%kxQ)Ar?ARAq!U}JA-`~bwdYHE%iF*umaF1A39ZiB3g@ExBS|d3>MIDQh7?j5{9v@^C%`H(ivYxiFQ -v0$Da1j-X*=vpuQRvP^7!5kb9|1p}&;u`xyiPjxpTtFo0yZn`A+xGOML*OdMZ@3n>|ow2^)mF965X8D -nrRRYbnwahWjeUh+B16+PV=j^%*XBJ1eC=SJIwP^wwF97{?dFK3N@N -Q}6tt(}d#i@9VRa1NTZ&az5hNSQzoEv6*mCM&6vtpq=mVidpl>+Ef!Di>#d^#LH%-Zj2~)Vu3jauXY9c&M}jqG3v -$}1gyiKkJ}#i-i`NL!2i1r1^Jr}1^F8e1^J0Xp%99~X#&M@n1U%3+uaL9;1rGE5VX6S{dW{We;TnLZU -R55&0%~5*4Rhs_Rs@?j_L&-{z`_&cn$J#I>gT%3ho~Pb(|bk=7{{-j){K?6y)b6DmZw?jtT~T=!=BOk -p#ek-7Wl?8hP*$S3y2S>+oH?VuXJ*215Vz_N6qip=$vH!F~DgH^#0(sUe65e0 -S6-b@Ztm5^$B4cWqI`w!$!bxGIB7(e&(2H_9!-&ehC20>w6(Pus+xbb4S3)hYml$}!6FLYUGt>%vY(Wg~2^@T)UJu1?@M -z6g6x==UrN^qD*_W)u!kLkoF83<9zxe}1#6qBNC6KJ)Yw0D#!93iSCg}x{_8mJp6riov8rkO|C6Rcuj -J8`+~z?AH^Z-spD8)i(`6Qo%tJAg11yx9?4e2lD^%kp|tiha8KLti?C|9+pHzyv6rgto@7F@bDHp~_I -CMpVjcl{S3T-BTR+&`2MNY>_&docufFZH92W--EF`KM1b?Dr+r4n0!l5laP3N@FdG#x1@D<|9Z`Wkhx -@!eAjm?wmFE9rL>f>o1Ka8{b -SO!YtXRX2@VMWbgZ?3@PyQ%#vMy7JSl2wMkyJhJ*rLh9?yeJkFGY8S*O4lx%=$cm(GRkM`k@k34kVeN --F9E@|l`ocn&F -ECh`{j5kyW-xQ3JE0$^H0jjRq%bX6nQMm%rdDq5lHT`PJgKXb0vWNLo`L#nD;c@IorWr?G#T@r;NUU% -SyYkdo*?;WgNTt1&SMm7b0;)|#3xuidi8sUR$fHGXdIJSjGaxGipsd(P;3%1TBKu(LQ)-hBseS3$Ybc -uRel)~rox;3yeRMpF#o$jS*@GUp%p+3c|Ks6P*U9QNwrWFv3Rk=QT$uPB)hB=k@i8U}ATPPz8Z>0`-K -8>`X2c`q3OVQM2;a=bc$x}gZtCLs)Q*Tnd!s=^qNg|%Y=u+&eQ!_M~Xmxj}7GBS#CuKb}Q093Fp1u(% -7IlbCj_hXfAdpz|EfHlrE^EzsHF_^kZ6#`WqCB8YsW5h4*BzW-3j#`w$JO+;$KOau&}?J78G!;diP6K -odV9p>PL-)4*y>qjCroH+q*f4z!H9xO)F@xYWzlO9ce+Ye$>puG#T2p8TD -SHBv{eq*{ialv6WnY>8EStUO@3JzhpIZdx$)lI3@ygVVA9@UhVe@<`Husi8Pn3>M@QK!_%#sl<2gW(+ -ov?BAI6qOmt#5?=?#~3p^wb0J{ycE6o4xz#9FVI0*e4I0*e24%+|G5Dk$8fzTMagCBGcr}y9*qW2)1+ -F!?Th(qiR-lN-}&!j{L%!#4{vY_Et8MysFoI1ut2<&+4 -ml+o%K2|+uRnVg(Li`z5$Y=iL7=BJZoLC9`(c<{fUdQMGq5|sZ;D|qQ{~kJ{@GJ(&x!X;yjMh+j=j4Z0ck1pDDWiZ47ikAmDcxX#1 -xa=nwqd{}clq5D55SppD&6Qon0Z`_{0vhjJnVNes(XPD^=gLNQtermO%50##mjtCJ(^Wh(B;@vPUKd22&k&zjwCBUx_R5pD8mG;B%f%|-8xL;?I0N{gR(r5+f^x5Ks=+MhXr#rb)`w~G+)* -!*K3W~#}|nH8A)vWJn+d$Yu7k-U0+7+3y=Lx`(#tlSe>xbk2T8!J-cv@qP~WY2OWhUiM6ggEN7`jPXuiu@im##6EH09(RSGp}FLvynQr~A; -&b^VHZDi|NBPB4~fHH84v0(9!~zf@t_3qa+zBLwD-@~Fsy9Po_{nR+ke=2K>yzv5AFOH-L2ZS&x>dWL -8^8dglh~R8OR_mY=TLym*&;;}LkK!u$E_UGu|$HQJIfb$> -KqbzK%+d4I-on -fUBbRe$U0P9oUi?ThwYA0aFrQ5IK2A>nzOIUojPj*aw?0Z<@BBfHMvK7LCv{=|fi-$qtn^^6zg+!l`s -J?va=$CTT$|TnMGiWbmp|mc{Eyu(^;7jJ3_Pa(m9OzHY+mNo5SPEa<6j=}>tKZ`^4Bj+?+;_%SN<>8< -1N$v-fvgXt^f7qBEPM??xstP`&Yaj*Z6t|KUl&4Ki=Or>-&%H?K{)_Gjkh-2iyA&ruJWsQQPE0L6dw~ -PlFFhO!N_4gFl?|Kj(GZk?S^VO%LFNesCa_9D^G%cHpahu^rgO@vq^Z<*cv|v3`g -iQ0xm4e)O53z&^xrJKoyOJLZ3=XrhnY?kBHB1xIdehjg)j)I<>axXeLKM;xug`#J|~B#+X|UzppAdvu -vV)4v8S{=ii8Wqug?Q8J8|7T{krY~kY!MWtJ}<)MvnN9e+R+Jpb#4=eG_{NlD~uRc=Z-ks(A=h6a!qm -=>2BKrZ$dyZ3pEN{Omd+EZju5%0-$fJ8_mB^9dz_AXY63ez^*}m|KdWUA<`c1K8H5K;>!$%wNS5EqVU -jRRh`r~a?%ENWrR`dNathkZ22K&Qt4~MnAZ;v^cem+|U_B}7kgXz -C<2+7|b(lOQ{l$HM+_XR%Gw3@eH)pOeR%b)qna{?ZR{_)xP=F~=;x4FA*viFt!_E*vNe~$-uY-jj>Vd -Rwp`#v9^zwuEB`v=6YJ!;J=(UE{p>z=|&Xm;?7;xPjuWPQR%qkIrXX?q;Jl$6}uwb#30#Yc0)Dgzs_l -_I19&(|IBD%}m%K$9u5w2WO17Pt2)xEB0U2H2=0SM#d(!DXYw=p2$rPH*77+up2A6A}bVS`TRG9L{e7 -X28YS`V|^OXJi=qu{#!xzEEm)EAP}|pD;))D>C8$t{)l`|1^>r8|5v7P%xp97k0- -W$d*z?!t^tBCXg#km6oN>v-qiu0o-dkC6*B -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 -Twg9qrrGD!5;YNdgUeNNgNkjZ%t9dIg4A$ -s_Ek=zboj(jBGiR4$rCZl@vc}DA>;A0Wpkn6jTrvk5 -Ymdkc!Mi)2eUa(Q9$#&k)Xedns?gLmU!LfE>xeDQu%l3-r$q>Lf`yg+~RwhxeUVdHR{;sVtJJTJw7xQ -Na@YF$h2|_1I*|VA`w*%yV3M!zccu{WHFrB_j(Lq$#!KJIf2}m_E)CrGb58J4+W}1XKp+$+)l}P@O43 -@g2qV^T)VHGIS4#cKbb}!Uf}ZVa3MMcb$Ftg`&B^~5?i7SpfpJC?q}(pRxe1zFEc64_ag7Qcy@O$%w6 -dTtJ!=+CRX5y?dE(h#QeQpGLm4KArMyGuffe*=!#=J+?{8)m;L~%zMTvF?Bss}s(?Rbm6dV6PG@qQPM -JPr6l5^;1}YufKUcjUggcTE5T`ppbPWfJDcsoZoo92uBJ6dC{Uz;O;Cb;KyaS&_?$!~L^aGmI?W;|K; -o@-=t>sIlEf{oq;#1kQI751GWUdw9pv2+N -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 -5xT7)}Kl;35Q8eXd=hbn16 -J>@2?)}5MS=#(5(2hM19DO|GMVL6$jYJ?!Q69C=Qa9;+rRt&RptB2LRa$=rEJt!NLO-0{;#yJWwI<@4 -&(X6#_qjg}ec&KfuCt(ay~FJuLik4ET3o;eiT)e+L#Gs1W#Du<%GhZ9ktj27VhizSvV|pD%5$B6UeMy -B|=>Tz=i?g(xyquHnfcgD0KUQ? -oQQ*VvTx-VTQKZ*92tHp>Xk>&6eLiTw9~+JTOnB#16Y$Gyh4ak~xMvJPJb0%bz$@BDJXs8X;RUl-L&; -$Ci6qZBSZFL><7Z=x!h*bXsWgk^N^qbw%eRpVT-N5=7YL*bwBb*`7zmwgqo-2Luc0kr5PLHsCuAd3FK -tks68zbg|JXQR?7xn9Dt1$ge -~fAJG1PlF4S$hci*}p)Q**rs%eyT<((cHS_l}@nQRHB(Pq4#w96!MPeNh$0n|s3XE)&S7eYJYu-oK^;GTewDIfpSH?jx(d#sKmN6a66rizZ(K0fGZb{9ayM^gT;+e3N -cm;P4#eWcVe^< -iw2CCEbAO^N=wJrVdvJJ4elbG@ik**_EdzKV=!hON_;y#ihnLZ}fFzI1wm4?=7xH~8zR+hvWqA>V)*Lj#uO(#4zcZC``r_}JO%Q3 -MKp$qVLTt1`k7U#4z%ghz6VW{WBFq!j1ak+cF>0*q!FIIAE7xV1@C1gPjhOCNpMf6+k3J&S7cddyib6 -2e&swDC_^!B5pRnuTCuvH&KKxWvQjsdnOYhth+EV>IH~AM)G!L{-cR<`Bp%_rfXZ%>vVT_u*>=1kI62 -bzq@C~^LGA|&{7Jt>@+Ff0trqdXUFB_Iv<1<90kaKLw&*P|tE#DgkO2mt90}_;Tf6xfKpQ?I`_YC#Uf -apoKO8z@pTK~T3fIDN(Sxr=7Dz7(dCSXS5VZHWvx_P7L3Ff!y}d#6>e59{=TK)GJH8_xS$@KSdxeV5+ -jhXP-JjqUG-nD~BDx#t5uRURnUMA3p0Y-z0XT5qD9^MObUbwb6J7}4Z9?kQiN7lTPTG4 -U7l1>sq`n(PU1c;XRosyVVtrX5$R61$cce*AfHc!Rl4-%!YEQ-&2GNBcZeGl}^Wr>xh(99rRaWx|K%T}B?89M-P5_)-DagcPhwAO -`1ln&&mdpG#MsDQkT(&(eKtPrn)*Xvtm(YUUE5V+G9-Qt{RomG{msx!l6x@IBQ97JcPH7n&}o>ttd%Pbzb2RDrM#R;0hp|e? -rIfw+RtGz(5@j#yE?SC`(X3K7xYq#(@SFz5izeL}xs_%eCq6djq=#0JxNr?99H^`oj?M%nXT3_vdCyp -H -pf@6#f}+#Oa+SBFI~R1$_@#!#j11pgkzw9)|SWAvfCX8foZL-iSrJPzw(4;dKn}{T|4hI>Oja=ptfoy -+GfMB6|k~{BBz&_O*#$^>A$W6CisQn@wcvW{FW5N --j%47e&;U8o!TN_+#spnhWYhgGRVIf^-3z&MBW^}*qig`Fn|5O5$12v_wp^whyMukP1pb16`;>UeOvl -ds1Ljkmh9pGMg<7cc~$tx -8`vqhrP7{Han=wKdFM$K8jUZaTh7c>WNu(qC9Dw9Oy=&|sTPTfg#Kn*seHC=Cqo0`GOE3XE?YuSJu5R -g)YX?ZcS5_}TwpGM_6Tob4h9&3=jZYKdju1Z*gw=?i7fsvoXeUG{)cuZPZ@YEk1TsdKQi|)e}z^h-)7 -5^OWe^ny8a0*^=vJQ@S*gW5Ww^^I&d67nfd<55If -vc}eK8Ia>z(~uG@KA#@FuBNDwglsR3&~356L9=*d9d71Q|tKrj2{IRVUi>W0w!q^#W0NCmFPE6hEND5U>ZlsPrXgj9a6WTIKlRid&l -Q@CU%SQp|?TFE>0ipX#2JY`P|zC+J`uZ_k?EnM!o=k`<%b~;^6ly?567JuI;fWUD3A}$`<&;zk=mxPu -gy;n}Ozz+vYR?{qI60@&TUvn&m0$uptYzJQaO*wHFPE;+{(#xa -0y@lh4>X`DwTiBlbc#fHWe(sO*IPhEDdf5>P@vPj&wm)fi@_deT?kqs&rSG$+DFO5F3eO6`562BWW~& -O(!(HiDp_lUK90oEJ1z?PsK#w>N6epYh<25YbwiPg(|fKmpCh^&4EDl$y^u`Kd1Ub|-oKdS4 -3$q>?cv{Q_y#ci;m&|LFUUEJCr%GoZw2Eq+5dx_NKBY<^{rcpeico6o|C5I2jEkBhs3dfCEV;3dVFPw -d~nUjIiMmq3RjXB-LnmDip$?mXyl(S*sR6W0C^`pP|#BKX$Iy3MgY6C@rf}22G@8YQ9-M497<&N#C_AY@x;rKcA)>6Cs!1Fe9= -CvO9F1lS$)_exMH{k_Hb4H`dX))yhs$G&vFyfZ5H~t9Uu05 -fw2I->ckzJn3`Pr-v*k7`|OVZZO*z==hA#K-^HcaTy$ECx#s}pLGOpJaXPF67MU}iSd2jg4GmLC2_=k -Vac8kVR>B}lwo6;e1QXVC>->VDPf?@+%-B6bhJj)iG`tZBg?IEBUzmu*rq|ytdp%%BNAxu_~N~KG&N7 -+qG%hFk9zCqEQr=5r-W)HaD{DY^wJ)_RZ -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 -d7Sw&Cxq -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 -JC3QHx_zACb>0Sb^<}t5#G{?w~0pbW -PHDW0L{Nf+7a9@*t3vXCZ3Qfh&B%~Y;;$xa4mMp^xdq^=VRjKmzbfV-3KclH3-13zv-v-m_F^|c$x73 -P+vw#Er-2#-TyO)h4=mGHExjt8r0ZLzIbHw|mG@*E5nfPO|?-;KSLI83Mdm3sn(IP6hHl<8TUe!%CHw -Awy&Guh(y@|f#<$*v2ebz-Ijy59pZSdR)VE9~t|wGI8e9YBe4h&piYmOfn{a~|kWZ`~kYdKzR0$7yUj -Zn-W@8sqAP2htyFdp|S+Bi9RoZ&4DTMiwgK0ehnoU4EjNdHGiF1O8L*xGzmnfezHv%$>Z;UB~-I)kE@ -`yLVOS17VEQ*I0@6seNpGKpFy48w#92H6HFclrab}jTpxSf;&~))A1e|=bAUfhu5x=6^^V}JXqSisnK -1Ox>CR&uP4=-hxO_(-0?6QA`>e8Y+V4Cjc&*MIg50p1@04xt|e59h{<7OU!5#t6u{h8Mk*F;I=Vfv%O -%Ozn^>LYXV$C|4n#!%fW1~qa@rrU3 -XVuU6OmzHm2WvFC2mEL%76=bzjkAV#LVMv{s;`-hxls&C01+HaSu@zNjt1XQgU;#^w_3&T2mq1Jp&l;!R6}6*ZEvI%<4Qv4mRDXXpI+kz_kKm&@(q#0W(Z5P9z}xx!e -|{U%;L)c#lCiU^Z#NNsLI)7QMmH-b!AuqkEciYL%F`gOcg<$4ov1@G2fm2b|dC0WSI=SD^}y%?w1%>% -K0^kwc{OQ?G7}Y!z|2I_mBDVrA^bHqu3hr6|8jp$uNNxfFXHoJ;T4tKv1(fV5PCVvFJQS|QQ)mc~^rK -d&Htv$$o6j?XyHkroc-zs;oH!V_5=d>eH({)f+tX6arv9BFSHj2-)<{pxaYkhdZ$i!u=7Fs-CGsB2fVix-lIYPCVs1cA$YsF-tn7qn#pL-yMVQ`d( -rd`ys=?qm-pdMl*BM=X+FMF%wLosILGV)55_~(yhrOX(%4S;Y6EM3NqiR!Wbj8wFSW_w- -#MmLFON5kjXqLFm#+$9ZP*;YDPVkzrMxJ*Jh;)d|eB5GZW3 -lb(c1`^vrIQy@8qFe}dYBW6)hEVOJ-ZA7$W9;bvf8exmE7kh&%iqc= -l&}}2jZHA}(xalR!CaAvxQkRs|T@k{up2!Cw=C~8i;)B-GvjYv@?LguZDPG}(_2YZXv*|j8-AyG|Dx= -`uvjMIIlCzQYY8fXlc5Aob*D}BESX%fc_q7X02R?BniMv&z%)u2PdqX36O&Gjj$o -7V7QVH~9PVtJ3rE(2%HNKt@ka*;d%Yr>bnBl3z%bV8V?3LsU9LT4Vo0)d5G5?_)(IVFPV3yVp1(o~+w -}yPGAuYZi4mSHj;x!Kjk-B~`F-&3sVGq&FU$#|!+Jg0catk=Ih3F3K5QA8y+p`hjT?!9kkzBFopXAIx -IT!eL^6~QJWadI^d_Nf%|G2$M#_-Hra+c$IVfz-I*bVl!$f1ixbBfk(1XMKM{*4Cz57ZfMtu*>q7kX< -J3ht_|?CVP@iaOp(w$eB-Qv>Fi7j*~hD;Whf`l#Hi%^#ScHn&AZ{&*biP$uZxaeWE5yXy8ueE$gqq6t -NOH*O~{A%|DrA=6u3@h=v6$G*U4a_zNRB=&(X>q}PP6x?0XC~{+$VhrhJbafo?+2~l3YIUdz5aBC6KV -Q)~Ja&h{v|aOj?ru!TAqV -WRYu~$dbMVTMU6s8_fm#yp33(W{?OUD!eqo#_^XM&(|mxS|OOvt&7l)pd;=IG+&eJ{VMeb{fg*9>CQK -!3o)&V3!pSE7LRsX!EQ{dA0bj*%$jmJOFSp(IFal{174#(fcG}1gLjXFkX`!&DpdS^gqrnbfLqiN62%2ZROWbZ -WXGHUv|_e6d2(q&HO_jR9I=Hx&DnMPCe8885U}9jla=)DGS%)GERYIRe7jDbC9ISx^!Jm0N%NC7kFbw -4CfX3^&){En66I}IcUiGFXcSHHO5&Y{e!c3fNI&t{|Apl~$m}Oe{lI5YoJL`aAP9;=VS1y(8!LvePo; -n9Xm6t5hs4P}T^EIWr&&Pm&GqzKUneB?ykY$A`3gR_;iH1R(QTJXf4BJu(Vmfu-a2a}zO%{TZSI%86M -9j)>%nbLQOUol=obWbKbzRa-XV1FTnp&Eb#50RiuWcH9N+or_8QyGM{kzBy?pw1qux%2-kf(^VegcC> -(XsRIHLAn@mpSqAb&?-`L~2yyL=E>MN6-hpUu5zpdRvhIqI$Fcud@CUgc5zxLeS%YWZl587k{iE`_{&-9 -iqMwLtZ2+37O~N;9jZ6oUY?pA -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 -q5&`}VTZACs9n$L1an9U -%TKm`z|X#ZUtLM%1#6a6koAKZABNHnROfNBF5GCi`?c2(rsR1#lo3F%`1j28U;X|%nqwNeVnVV;dhXN -|!4oUhbim1RI_sSkosiWBG*X>c#e90&py)#)5&@wYcgw-VtHAVf0PFB{GZMF*D(AW^Zk%811SPS34){u7) -L1z2e+6^At;4II0}E+aX>zG7ThHP#k=W6v1`({=vI(#ALn>aQ|;{ioo(NagT)q2KaIkB5e)VqL>Y2bj9ev -%Kh(ELUGiAdXb1lpWFh)wO^|m2`OP_$b9iPfu1ktM$i|ygSH5P92bdxecG|QH(xV*L;=XLCAEoQYm0D -&mu^u)gDP0Tc8x(O6*_HgO>_q8N(Y8g>ueFW?UkarN4|y -Z&!-snw@;eY8G65<>Cc&cOnDfJZTD0@hu?;qp_T*o9n%trbnRT?bUMKbrJ<8E*B6@U+RmEWKoUn8R~5E#JwdDc-YAAc -JQ<2#E_rycfHPKNGhK?Wcs_C!NZ&uYk$uvx^P6wZcPt$ERl80$9isw`msBR>jdfQq<945j(HO4!58XO -}@7s0$vfGBbOs586`6yPZ{&cIyI1rN3?fQrXv{EyK(vK9fD{-wyapp0&@ip*!tg#63L@T4S`oTsCn=z -~xjq^uRtDR#xvnLWVS)kO7lR0Eh-aWSH9NU-EI9?9jV`PhuZa%`QVku|8EcqnSU)#f2JBOKIHHdPm52oP@E{rCruHG4qi9`R1!0zl}w+rChye$BDC(KAv` -Y1%Ad6BM(5}fZ2+-GH=&Y6M*iT3j2$gJ}1o$8^EL>J8{y<0OFaAUDJh+r;dC!9Drp@!5dubf-xRziUL^g2D~SHR*J0|yJG;3rC}9(Ij6b2@|d6{iw;`y`HV*5-w -W&wYH>kC_$HDa}i6OZeuhf7!M&6~9B&LS5nSb`DD}B -EM;@OF0C?Otf_e?dco#}-q4rh(Tbmk!T);3rh+G{nQ)f4U>^t>8#?vU$GJ=&0HK>?Ym$?j?_Xk=kY0y -+-@)!iFIitxc0R@F^G>zw2jNF7+eln>{?z1EaE$SXf-xNnbV6r^@LmRXI^|PY$ -H_!Q|;rwj;4>%2iHku6Yo`yKN+wDLYvODk)(5E7%?48>w@ZBVbM0d~>6K_v?=-tucU0byg-t67>n)*B -iv~l0v9|w8Y7QgLwHpYySy9je{9`<%H+y=zdo}t-r)NVYO?r)p@Dg~6}dlm^yb~v@oOC|fijZ?$vp6b -cbwWR*J=}LADXcZsmjQ1W8S})VD14&F{aP~$Ix$jmJ(~g3GZ=$6=wehELX$NR~#sbm*6fXU6?Bg=NYm5EwT?VjU$A5AeKd3;z -Tm}>AjQ)cP^y4?MJ-8$qlxapu(o^^d2Wgyu`I;YWg`Ec|+yDp0DH0f8gxfQOugl!v63F#t$Z^m-#^w;u3Mww9?z1{aJ -VN=UCvA!9P;v(oi-!AktT!9{nPN(7VsB$>mbJIAk;(P$g9=qrJ*y)>P#8mCz_7QA<{~IPIQT&f%FuxK -17F*p -Raxmec}&Y8W9NDI4MfvC~s*vc0NZEd$^&tufK2@dwb4P -$Q~;5FUDZxzB!k?J-yJk;X_Wn1?bsZAOEF&;>HhurQ6)zcMFO|zNfIalab2;S;BQHOynEk+f7@C}uy@_&@@-d8;Vl{*YeF=xApBWwMdyDGd5v -YjoeN*4Db(5mjBNYp*K_QeF3;*=yPht0;C66 -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 -Bzv!1u%O!J&Jh;Tm^Fw{|(LtJ% -3cCR9Nhqr;M-fld5}Q5hl8|F5F*zdiN8j>`XH+8@dZ2@s_yf`V}jL`Vd}35Xy`62c+!(|#>^yvHj1-8 -Xo5(0nI7_7-yB&hj^^oxkmOU}DdfWS@6B&xt*|5hHI?>0m5YzBS}R90G-tE!d;q9H8`E;e^q#O`avFM=^XP4>7P>Aw%>=20u -KB#*_D>48$=(f+bzM_`j-vcX#}7nHLz{v(4F+d!>;jkL_kl`IsSq1U`;W1c}Z{fp`r%W!NAfx)_8J{g -y1oRNctF9Ko`@7eeinl#P1=dOjtx~$2Pn!bwOP#2`P^lKF#bwWQV*uhIi?1M+Ayq405zv)qeucKPjX! -XrH$~qUOJ>!ZyE-WjseVc*ptUq|vTm+WK2}{d1uIpPct2%K!G9-$!=}gbq~b7Phl=SC{XdQr~W>+dJ(Jq|ke-x7ZW#+pBQAZzSh$p* -Z*k;_16f>NY;k_B1+??5?I83D_PKyGz{PXV1HROBe0GxgODx^^$s7LyADqmwvWDdp;PEZCcgF)cq5yelJp^pgLq -Fd6ulTefcbGC2=5xc83j`t{hNuO4Q&O{|Oc&yD6sM|Z-FRDW$W&p;omV13_E+}d}&g|S>-@a&p$T!-O -+1W{gFXkK)V{@8`RyR~q(eYv#Zc?w%$_*~)LOv#mDfGn`Pcjq6e{w$@bQ0Y_^J(B}^$#v$5>G -UrwPJ$;=r7Q1Y|ht4yF+p55anduf;$`x^|=z7eoh0>)p6LCQj3cv03ov;ccT0p-VyY0k#8J-K2*3pqz -|P#sKt5}-0mgAx;{GeGl=yD;pRNSh>Ij$$%Y4{&T&P)2X6<@iNu?wbpDl25{16NhY}Krz<8jKEfBXw? -)aw^JRjO4er*|kwB0(2SD^ME+40V->S=h{k3ePVLP`ToiAEi4jRg}UI8N_}AY~tSPdWvH@k;epzF!e9hK1E*cR9F)6(T7=&~Ejg7{bt?iZ{l+=o3y|c~B+5>`pJ_^>F07^+ -9ExcW#54pA3chv+e3vR$@3m3uj!oTEHX#TdYaO4`z2n^4CX%JagirBdbz?w7(lkjGT)ox^x(Ue`PBVVc>My1= -$aoo{(m!NW>X#t*D43^CfiiF6$fucdxvX;kkk8UX*vT$7;ak0UD%$na&y$=0X?OS~;WE5KqWt$+bS%@ -=9P%4<^hjecs)YN6-s)f3OSbvyo=yKqC#Zl3wOj&IBI~+}qMz<<7917acZ8*0+kL6e2Sq@Ju;R`{n9t -{$vmYnNp9+)S8a3}l3vCJI>g1RUaSH!x?zqJZ(aKOc8I3x4s*eSyOudomyhr#2#r07u&Ou7KDt7rpdLa?TlQmYBr!T?vZmYOL{*iL3vBAo)h --Hpga-5As}Wh4I#E7e#O -}KN;*LqyKEwV(h@pStuM|8A=s`7s>;lJCYKfH_Bf`nde6dBUYW43T?3PJBQV*!kNll5lwyf`~VaTyvp -YF96A&2Ah*#J}!>oBQM}G*0?*5m}>VG)=_7LJsn|HQvMb9>!4W)g1)%5m0s`>B};`{e! -`M`I-F~g5#(h!Ei8(ZHH9fBdy?q5m3BtoDYe}_N}gSP*{&kOXZeKVCx-#d}Wo~X~>EjyBAcLhq*eY14 -uzwn+9Pd$cMeVdMChjhg^8z3v3!X+^S9) -&Nd2y5Am#k0o!OPMtKbCzECfyS3F*{(6eXCcP|!I`VJaQn4~DdQPU+WM<*moGqg>lf_{w>_;lj3;;tW -b0MA7}dtF_-@uXy8jSP%b>8=Av2EOl^>ID^mU_Cpzru~!*BR-n}Bg`FY~>ca=$tMy7CUVFnvu|9r|kg -`}G*`_24V#e(D~(JO8?0NaMSEEFYP!Z;f7U7`lbNIQpxT5l8zw%52h=4|OK|(1D+J9r!lM=(#egSK&+ -NY*lw&FSYzYtp>BV?EoyViRJHRmjy%cnndXsA^1-Qeu{A)I>}2v6-Iq`8a436IMuMm-x3l`$N{wPHew=$-6!{m*DTO97Rb9t3cEAhC7VSh7KQj4yA -^wMp38k7yzfJYIfq&4hQ@`&kA2AUQ6oZb0SS%B&A4^&fM`0StkHEa~$;^cQnZ_|Gb@lV0_w@>5X)BvE{$#2|(e6h@LbfkDXL2M{B%4Hlv>Mtr&@x^dWz%p%|(ZZ=Mdo)e^;Qu=)U -2&h27owxA@!`717tjXR9dnd8mDHQmt4uHkJ!@31=JbQOLiu -X>`Ta@2sA-}Z$EB4Jz;%y_bz1c2`hjx#OZRGaqUK$PVbU%;xSd1fYj|ytnO`#is{@a=w;k?IF@SEz%; -WL$ZW@#lV7#+O&Ov|`JIT!ZN#Eqjp8qoRhCr4dH(Czvqd~2Kc!Eomgps&Q --F}Uzk0t@_t!440}T5!#1u;ejXOPJle+OU)Ee$~`kI-fF-LX8Gt~z#MT_~u$#x>Sg4^~ye2>ZB3H5K#A*k+AA^dOlJk*Z$!wC$vI}~E(b;Eg0CE@9_r}i2AA}}jxzdkcwm7Lcoh3JDrWZ9 -@U6UX+R1jYGKU(WWKLr11W{R?Dru@zlFbgbyNE~O9B-Gc$!7srgA#3@BO8}8D9U=^-tF^2i`lU-&xnh -x(~T^%6(dB)cxZ;bq@WQLIfp`Ua}w5&2M*~D4l)6GhB_>5qwdQ!3GB>zpdytub+TcNhupqgZjfA$IVk -#BZDj?kdgoSNSp$%d-7QUT#as(u$0KlUC#l`dg?iTKbHMAccx1;)p$>;A^YG{(6bq3Bb4HvhSeVxVnB -gH>js`)pgLKcDA@^-KQNZ-xq9z{Hde0htN0EuniN)Y=kBIi6Mg1d-)(8*QB;5++)T*WUn##VzMXuA*Cha{IK!5Cp^0)QLKbI#1A9R<7mPd+;Qm-$s`*`-y -Dn2^}DYP4&_$7grW)vV!A**R)QrzXoM$OV`EVHhbSGTxLceW#JvCbbxBXl52-l?+o62+4g!<yq;vO3+5m(#2mD(pCSFg4}8i{yKy*>vxlJIgINgm`Iu|^7alL(Z4yaI5oBhEn+$NapMbB) -i`DRCoI&ZpCJU9sx{U%OCW?HL!+5)61dT~`4fyv4HE(GRALJwHG2Z -GUa_=M|zezV;Ere~`-+l+t*8h-$1TL1+6a#fg`{ePUCF>OEblekhq3B7G!WX$_UmnF#j`(z)Wp=snd^-W?REKlYt+&~!7y-u8VhaRD1?!{`zg$Nvlqs -v%$BoC8R~Q|p>M(_K5TH7oc$t{ufPv&GM!_|QL|oL}Ir4raI9@?RO1g3|ML;S~WlAoLV2N2-SRM@;AC -8YMAszL3;}Kx$p1}~5vrC+AW2w7q<7AJkAFUDz1+NORcyG?xJN|D&DE{ApP`^CsR}hN-970hbf{+xlp -%H|FFcL*z5=Jo!#}I;mP_Vgz}2H!JpDFUgx>lE3tfd5_ot)81j`O(pl1koj&5gMm9RLZDsWLlyg~P->^ -k8Szeq!27$V_`aZx3vcV+{!wq31-;$Gu${2}EhT058ofyT5kghQ6VldJ5i5ZUt*kXK++VWG8+ro%6Zo -{FC*W7`$#(x9KJ7mP{|S8B(G&0+@JZO=)35qF0$+@_oRoV=r}1#jy;}wur@pcKeAF-XGSc~7A$#xnS{ -?SnNpRP`n%Df|Uo}&WRS(#Q{xEd(V=Z1Cmpz9`bwuL;y;z!o@Vl<%3+CKdqmy3|M=0{-p>A>j)9o>SD -H<`$fFBamYOL^MrT%wp>; -sc7&iZSzN(|Iv20>)34{%<> -PhO{AaW**`q@TUh()ydPp3NI^JAQ7DYUAPG|tiDD>)Vleh;4Exf=9e?{BY@929Z>4PvZ7)84H)h$mA^ -6U0W_#lF(-^jwv=F-$6@=_DWe*(bUKqR`-{+3^jW&eX;|}!JLZWZ4Dq;&mzf#6*Q3s~>Gj~^n_cmXKz -BP*QzCz;NjEhSCn?U!R>=qTbm&62e}YoT+CV*n2K>@8%*UXGPxQ9q={aaeKg -Xd5X2X+4CCT_1V3ySblwpWAbiUHUAXHw(qwn`aKPFf6CeZ&Z>aVmi4!*`pzI4_)8DRpA4cOSM2<-hm< -(ReuXoLxJ+({9(dO~Upb6Q06QL|9OA)4zG&B5B_QYLG^K7)gI+#iJj&vzDLgYKJyyw+5>YSbv~nF+Q^ -b&KNku@oB*uMtnEvUBq60(PBvuyOVM&h~Ehe#AcPYe0bk{vCpAq%Lvp+ryfza>WXbn|`0M_}Wt`l{y; -L>c|q*=9AXVbfexT4hzl`b8jb5pq+3|)Io!v(E9WqsfJ;1Rqx_UQ#I%GHG(@N8qWUE(q>_j2ds+T%E| -x~j5mk*B>?J=sz;n{XyKEVV3AU$NT<8c7by0bm*_vO}xj2ekHDM_|D#?RNPrU*%W-d_Jcq&7wTl4ZKR -zkl2qB-rGjTg@t*I=U4%p*s1R9Q>6zRrNpcH%5B_u2$P6~Nd~qCq}O8F9Z%ljlnSU$waA=4d1+&clgNl9cIjV}c -(jhqOO^`N00qqVm8$Xw=SKS7c9XKlNqzx6kym<@-5wzsaPTUn2e59$NiMO6)mE9psLxp#E`#IMV~C!TanU} -@lkq~$-tSn+3GqFKm3JV0|+SyGh8J7G)wHb7Vz4qpqh$H)Yz$@ -a2kF27C=!f>Fh9XA?$yelp}bvbo?nr=?Ur*@7r1wNDc;Im~rrwqm3U*V;+%{IK)Q2`v}qe|PHsiY??W -S-?56&$!MvL36HZmt%n(+-*P7lz36$hZfczCy_rsHe0WY6VHJ#%*3nTS&AG%bX9}Z=YZv -hwZUn9MAme)WUgL(m5=p{g~u;h7Zs&Q%(f%dMpRea|Hq}8wKU6MddvHxkh(4%?xA#>+}&rns_dAstcs -ha$@Rmck}JDH#?JSGJq=|G2|RXf)1;SUxDaw=I&N&LE_5`37T{Z=I(Mbt;I>;qh>sG7%r8NA*0&KrBq%D{JOrf@5gcs@8{|o!k9At -5Azgu)~5b>8brA4x9*z)w9MTPeI=3QY8?kDf&TyNaBtvP-7?@O^AC+?i|Z6LEzU+|rK*y(e&OPljzuS -j`&V7({&`(1ltr<~Nc@)DWLPSqr?SWNJrLbWoqzO}R4T?Jj%OJRMLQDgNsMD^EHqO#J>2VjzF!L+Xol -YPNRH4vVyGTJ>nzf*jLa5E*Q -bFA>Ysk@JHduX_8Pel@WH-n~j*BjeWL+YQTWg?nD{*X@z{A!3f+lL|%?BL1>X=5CY0Q6PWU4E!I|Cez -0Bh?l921jjG$Yz1ssDxxDeWl^C>!4|*c>&oyL(UjA`=S*_HY}k;JjdFfHo(h^3wYP*cDd9mD7I&%#7E -v(AYr?oPa&%1+94=G?YYj@p@I6g#lM9vEJ;Oj*$uGqDq9b=XaERq`gwOG98YKYsvxR)Mm&YyQc^FSM& -%&nDO)_yiJb5BD>Wb2lr2*d4iGh8GRc}I+)xYHJ?qg{fJ51e~j>Q9j9plq#jm6>?q==% -7AXO~O{il`|IBRRQJcBN4&DGY1EcQ{`mxOF?QJLAe2%p*vxw>m+*8qHul0i4pxfUR -!+Ln#!lp^2+SK&Yr1Q;3g;&XaKj{zCZk~qj|t4IrKMc5=-!5pjXB&@?IO8ASN>r?yaRsN*=rc^=W -V%s{pU<~jiJ8W7*QoXdj6=vo=#WekP%WA$ke$*zvz1n`pddggY;#>h8ba0ou8L}#@UvO^-bRPN1%_a> -fW(aYksL9@K>FZd@d7}v{V{8Z%_y?JaPwSY0ouauH9J>y2*>co|B5!ou%!=2qnhHUvsY|{uJ4JnDCD8 -?ksJlHlPb1e816|yM22e7S<+&F`1PGN+Z8^ypbzT$g+|jgJXr{WXbA{hWCNhR}$j&%LS@KdfwoOyGm{ --8#h4~ytE8&yU&ano2M~OCaYkD~jl{=KVaZTcAcjcg3rY3P?#vM=|^F%MLy33NNz+D)7;VL>vV3fBD9 -U|4j;LkFAA&Ww89Pji1H}v9hJr_~>OBfs`2(AhnF?yhHOc4a8lVcI~>56v~SK<1~k^J3d3=_Wi=QJ>u -vf<#iGRW>Bc4B3Nk-wT?{(N``IxdZ>2h93bD;1ky_M-M`?dx3})n#YNQ0Sni#B@!Sn8v43-*hIrQrFW -%SrpKN2@g7>SRMh#wS!K*?>UVUIWU(H6Nd5ur6P=tgVCSlCD>y-#+BmV10TD1;A -^0)P(Es|F(7|+lpdKbk0}g-1VM^9)=(&p!A(GLpr6EUikV4WM;k0TY0NnHH3vg=yIPOCnEMH_{)RqQc -DG6pY#QHDcUy@!onk?yBz0xYbRF0l6r$!n)gq;VL_1HrdWh_+THW_qbWN#aXc!A2A_2qB`J1b-UcJvl -#|v}Q7N@?ni7&co|*+v^D?Ng1Sat}0?FxHVV@WE7+u4%dum8Wns2VmkC>pTeB8v?qjQZRE`ew_C8^aH -w(!CVZSY;KMO6iL9qGtU;wJe}_wO8LO4*;X#1}nPKU9SEIES=4$+xU3lGGpnI)PldTVH(P5C7R_&NhD -6woy=h;r!13Voq%r73~-Ezs!HXHNLHve=@!Bfkgh>_x?^KKi>DJeLn&q_`)0sgd!;t#%UC%F>+N@g)x -LiX&lf9Mo_=3V|=BJ=xTUJtdnDF1BMT;gbv%BJ@af2~4<@TyBMOQaBrY)xwv5ItRJw*I52(DA4y<{%I(`dHj7A%4ZdnZ%!pVbQX!`w5$U|foBS$Wpgw&>JZ -D-JyDaaygr^qTzS@nAuLviM~uzTiIghUK@MC=S_O2RD8Gfl!NsyvlSP9y>+;FUHwr%RFbTlr5J^tFU! -XjWUk|E!YpgwzIn}{xM^NV!`lwp>sRW4#PF|^JVcN{kIHKSN7GiTpLHv=veBuTCNd<-B91|({+Mt7%Xs`RKj8x^f4KY4wj>k@Pzu0 -Heve=&);Dh`srlK5`Q-&=vPb5-w6r&7}H+~ ->BoLS=uiAijLVH6yZK(ZHI>=dORoFIc;8|t(Fvqbv)Vq$=l9Xhi54DN91&ECv2hAVSu?*J#JokNX)h -u(ZegPE&}&E;dr_0?k(Sd#{#ta< -Xvt9a?US#<1ViNIH-_z#4;A$jQ|^_l4ibNw4kAt2IXt5$Y{ohC7Vq9_1KTq$ -doe|tZHKupFSjp-20O(wdZ0nIA4kcl^Xn -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 -a>e@p!;TKqh#q0E -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 -FxBED=P$GfheM)PAd<1b$3=ZfZ{E -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 -tLA -d~_J(~L(lP40kUDucW}aSk3DEP_yu5=zoq0FsC&RC2-{8-hR!-nyzfo=4)S{k7X0%1hO<<4)G)ISx8> -F8@$rnWd+eKfZc1wAanigdl%i>7!pH3rQv`g!e0+36nt~v7(Vggyws?yHTz1E)ZA{%svX#Ptd&I~#a6 -rEuMYd1!EDHKU0i!nHjIyi{h*X4KAz2aFmqSfG(XP2#kvF#+2<#cNGKro6Rs^LlK;eA0~Y3vOQ#IQ5Z -Swg#$XUSY>Se!09C3M>(^96e1EfQI8fxO{fVZAQFRyl4}@J0QN#4ero;bnJ -t56}HdE|c_nN^hjG1LVs!57coyci^qyMbqIxs%KLa#6ozq3>rF1OG9?m)8gKI*wkWrG+~MLGj5`L9kM{kg(?YVttLBoEbnx(gxv+9c!0b;MqZE(;x -0H9%%h21smtu@UZFJlQgmsmX=QYI^R^d5`d1#qu?270qEFB_c=O`_<9C)?_8sdBYBL9hkNXh#-)33!2OH9ui_&`O$^w74k9E -c2c*}(Y;n=NZZU?S$qR=NcIK(ReF`9Ko$OO!Ea -fc`qO_hqB;M-whx`Gs|n7h9cGgrs^u!x^Ygp2<2oCyV@RgL|(tmw$4k%>E??{zhDxLKm)ER&?Zmp;c_tqqfrFC#yGP6RQVhhqk$M*gT^hy9=3PoM^N`_HQA)MSZD{k(7%;A!yEkWEI)2U&-wbl -9A%zHWIAHm@MFDJeC@gpMo)RHYR*%EM=l11cE(;rUr)0g5b9*=4?;BWI0>pm2$nx-lK?no_ga~&kOLp -t%EOGPRnM=}Hy}fp1y_>TR(O>c*vqz1{d#OhL=3+BG(>Wq2HeGkO3D8ig#Pc=eGU%Y?@-GvLq_Ghs>1PzauKy03h%r^3xt&4wShCa@T9ue(~a)2IG7WO -QQYhh;CP)L-lKcr&*^2Lc@Wj~%(#MVm$dJ?{|xH&rg<@=Cz?o4`tmM0a7nku?>(G=6PE|cc=J0v4o?c -4_KCWPx5Op?K$^t5*%|ML0gHJoaQf8@b`VG+k>ZUX=B(_ClH+n2)q(+ME`NsZwQy2tc;fO)9{7q#z+QTVl>DL5Q|3E`|WRmEvr~&4Y^4dVs);u%YFT -8{9wZdezrpAd=rc+1b=Ex`D^@kzb75C;Kt49tKq-NuIJ8z|--Glq^bVnpbd1XE197Uqu&28EIkSrTIs -&;z8H*x4xd<*%?1|NP40h|c!qI+!~h4h9qws-e_@NT&An;ME2$3kIxzMNxCQq|t|p7G5EqM&f;Y&yEv -0o-mquZHEpOS~LUS3q!@UQ1yvw+TM6dEYDbRdQQFm-2$^{+*8QwPc(Hu6TW=m+&@k=%Wq^)M?0Adhtd -@YQLWqoDR=Rn)4Y5UG$H|Ih1_y|_sdIQrt<4JGQO#MojH2;fCV -9EYpEpnN+=J;xn#`w@~`YmQlTk9PDX+yxB70~xK|A&!kD6rt_lz$!|?0_55JNCXy*5NP_y^jbEXbS?5 -YE2;@7(7f$zsb!!TyPAJtc09*V23x)L^c^OOpv=(%yfuVZ;oQu`%Gb2nb`0^Ms(!K7G|OpYRbJB^3b1 -C~VrZ#nczl;#8RKoqAfgnK^0D^_RThu6`)HfcQ%3wQa{Q~2zd{X!FM1c2jwLDZ{V@Jhmw+@|-vcn|&6 -E_}%@dhwE4kNdei764>yt>_p*QpXvfbox?N@)fS52f(qA< -Lfu6o}}erFDDe!7p?>XOUYs>CI(Y2-y%hoAL_{$Z$lU1{4VFJk~yNH`$ZygrIf -NDUTKb}bjG#+}5r^7`anCOi--hDt(y#Mv7B^MvlJNn^4!@?qg4OO?%|C*df&0Wh|D=u7^&UjFNHl ->MwO1Tv>q}>z+CB{I3!s#Te)p%k>pO`hTLT1*~ps1ZqiW9ap>h8@OahK8~C>h*1wHPiGLzcd^w_cmSedhZi-;RN@ApKE`tPH6uft2Q3muab;cfSK~zGv -%F^?@4sAZ&mG`iOp?8^+*{v0o_IPqPYrpV*wRVzsoX>sxQ}1}Yt^yKYWgR+Yhva7h|0`l?XaoF`OyJl -?%tjyvztof(;t|V36Qk3%c%UQ`&2w2A^S{!#+^8kZw{Z%ly$#wjU^` -AT`snJn{(JDpAels6o;yb&?{K$^K^aI1GorCG -hTng`C~IKM}JbrvL#4qE)X`%NGAoPm>PkywqT>s1FrOvuC7uxl&tUF2qgh?`8_q3lT*+W_9Ow1*d!qVBo$4;)y(>4&d?24Hvl{_tnh -GQQ)jhd;!XgPU)DV0{>g(<9ReqSDeWN2}`87fMSL4E2<9)?4VH{0oVV0i#WUcq}Jb?#mY?dd}Y?l;7A -zgWv@ffQ>_hBw#ygzRtgoG=wc&aZbbN -d=}Jhfyha0nHN-$bzUtu}zie=7*m^*>q$Fvc-{cU@O|4#msdA09QW=)YBN&<9bCz6j%Yue%=hyz)Rfe -QbzTz9Y*WcgV{BKZ80|XQR000O8`*~JV%jaiiJOcm#-39;vApigXaA|NaUv_0~WN&gWV`yP=WMyAWpXZXd6iUMZ`(K!eD|+d#4irux^asD*9!_%R_xTMzhK!Y@?_A`$|g#Y21zAQfBg;RuUu*ac -Y2bRJHy%8)i#@#AL{j=h7%eMjnqO>Y%(V4Xl#BX~;=T*W6u3d$ -zww_w?Ep@+q`3n>m(>oL?Me~sBXwHu93upUEVxzs4>k?(Q-0k0p5RHZXMKh^3Ru=SupwN>yG^_m9=tK -RmO3AeqwhFH0mYJN%{VRk$P-RL=g(l0HbbERj;YsN1qp`pjCX;y;LZ!}7PEhUH7VhZb(_~2_c2G)Btl -6TPS-Dm+1$ZP=){aRy+J%_go}C&5A<01q4GidOcOQr)&cod=Y#k!>snb2)c3^B1dfgH}=tnnq0eB116 -)AMX9+91k7Mv^1Na~t)3-9p)LKOOnv7$9o={PS{8w|*$pTouX>2g8QqnV_vi6)v)b#>#HuQmHPTKf3y;_Oc!SwynU9g<{+s4 -qRoi^QfktFmlg%%`$4`dGNfilLnsb`!IspAOPyHDNj-G}byyDrf(LFC#){mJ8hTq?~*Be$lYPO(n6!a -DXlYu2bA{R=6#b;h~!=Eed{LrDb1QO*i4 -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 -=is@KtWcCdjqy+!~1`YrKDF6TfaA|NaUv_ -0~WN&gWV`yP=WMyZfA3JVRU6}VPj}%Ze=cTd6if1Z`(Ey{qDcw(0quqx?0>~z?vYyoYZNJB~ -D;FD1ss|Xz65gr9_XUoO*x#9x2JclXXUc*2lZYyLWew?$OcV4_$UY_xg0xyXy9bUAnxvyZVz}@I7iA( -P+kWXXI4oLeZ5@n53ml|09(Wkv3C`VT&5IYcH2h!t)a^Sm`+%(kZzE81V12v$2>nOj%asG8Rti+~TX5 -YZJZznC`VH?Xh9uIHAchAnso!jK`WUan;OG^Xi1!A3y#Mh=cACb(Erk_q8-&%VxnzS;>{oospBmY16b -PXRr|63iF-rrJ5R<(K|whj-00ZrJL!zsvp!SIWfX4Jxi%!CaY8TjKt1qsSAolPKFa{OL!3BNaV?{+8{ -pDiAm&elv_`$UFSB6O_*AJ{!7mN -@r!FW6XE=zY|#Z>tbnqzyHN^ZBEjb02tuq?e@74O2%(2Ps%rnvl!`>j(x$NEaMvp%G>)xo}9S-UG -Za^Kn?+ix0tB0G;fbJg#_jki?L^NcsDfVO@B6&q|3dl@@$%LJd%9f=+qP-Rgs)S}@!Dw^L5)(xND>7g -HuO2F$PeZ*3Srd9rGNoL*7Cjq@*}?Sfbr&e>{RGKwm4ZO^YtIb>8*1gV@ -ve?e^)r_J`9p(Zdjymp;_(fBH!gIchY}DZegCPoV+RMk6&kqlohYvt6Ctig+e-9y$zFqtS?!uwDrg_y -=2c-qe%7ICj`ctR%g8^CtY=A?tl8E1f(u7)!rQ0y}^M%YPS~$7SyX%>^VSE|f3?Yq%r=Zzg~-K*}i3E -_#gn5LNZlwpMeSGP)Zkx%Ae4G%@!gc5n<>+i8gf#zjn&-zj_zQHU1gWX4g@&+UMCCh{qRBWE|@Cc@%k -EGx`3H@uL#u+0r;@SvVJSYE_=Qt?}(9=Id+_FkUD6WfF!=FDLHEdpy3D$RYIJ -V0)RhmRSN>~597i-2Qd+(c#65->P&#z`ZQ$(QUoo8l1X@GAiBusBs3A!7!Zv$nnCg=3{8*;=T4^5t9X -FB?bc~kdF6?JWZ0G^x;?(v2)l!`n_ekkV13Rp0^)iZeOi`-wt4wuE2lZo!r0~IwCqFq#=tw*r2Q9^X| -2SVtcH`GXA(o{Uym41lL%6Z2%q9ET%B=L)*L1k$te~|)kK+Pc!GT0xJde0h1EXe;|^D)XQ#H3v^C_Xdba~C|A=AstnXv*#;*qY;UE;1gEPqrf<1?-w9ja6}_!Jq9_wRQQXyhlMX))+beaHV5KQM`e56dJ -eID6U^tt0ssK81ONaX0001RX>c!Jc4cm -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 -ol5<=Aye>cg$!IR->zh -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 -rX!Va$X+CKj1kWJ>z;_Reuon4mDj^ -$gc@&RuidNf~8sX7ub|P=Ld-c0`rSwVFa(2Us5__6Wfl(kd$Tk(!v1&*YX_3I){4=cOWh7F`h4!b*!e -@?8>~)aU|zEo6c8;@r-bS4>wu=H}BxGzTo1N4W15Q(9Dil0p5svE*y*%vhkhV%T*fn9?1$mkmo`%W1? -(u(L=*^ErA_Xqd=6LKU-*0fD2ur4F#ZNH7`cnBZFFhIxHpGy{?5gc~eyv-IGDKaoXeMu&wC=?=lBhjq -tvTyJFG9lm5PN7vL|{)NzBZq1$4AthB;e0fAt>)<2pFrOoTHXg9KeVKVIJXW;T`Kv?P?WJhi;&KeBA_ -Rue@st&(Uu%*9Z3?U8yp?yD7JJl%6i6G;7W}W(X&_0!LX|Y&u(~{6zWN0m8n<_;t*L`RxX_xoQy`Q37 -K2gJPSwrF_>cmRT;N)_htwH5GLM-~|wGD07p5&INKW$rLLE|;5C%8G(J(w#N|u_zeRv;-ibRSZwz)~5pKFpzkO{3D}-G~cy_n3>6HUSPZD`9sNFbYhcBo7!tU@SPqif0y -VZUtq>R7z;cx@~(EyCb##-tqw+ytHbkIwI5rQS!~)!FX@K22$tIwpELQjc`X)^R2@fk1ZOQhw3S7k{#{Xo*9w*gzsBCIVGuxVjwl-Pj6Z*HD>kWkurp5LUDzzJdpg -U<}|>=H>?s56R6F?ueaZoB087CgVbu)`Va>)=`}lJSxJ*D5aib7c{=46M#j)uB7aLQawU#%0lwYHb1OjvxsTlWT77j#+N|PAykg?x? -ln0RwT_RD_UOiAq)7via(AWz?6IA^yF}TyD8nwA^u$2BrbMsWU<{#@<1V4z|W3vOpC#)}{&cDI~HXe% -94=0oOBc>Zra$s{(+-Y?vd)qt;(hr1sSL6V8o0+bRe^vA9ezN2Om42*e4AZRjVHI!{(-3WHNJb2BOD`I$>u3;f5J4NB9 -|av9n`(j~pczTfA~a_+cVx{@IA)4FOUK8=osFDR1t98(7Cq@2I$f119Vb -+X&4Z3?h6<}eT=D=1Z&68gJS$PK1h)L^;UvPgzy4&4$?`XZjgVI_$OE4vbyym7;wSnova|GdKsHqc=AduC&=E3lj$lU@(G=fu|HATmeFJ%V0kFNQ5ZMyHy -+ohO`b=3Nbz-M{Gbz2v|+NgKBgFHbycz3XD2nKy@|rsVKS8@JQ)Zb&?4^3jZzy&%jCYkx?(7jX#Z>Bu -ggtrsX+0Kaf&vf8o5ya1oF~GKhxEr(tbWpJ|66_MEcG&zPwLD?C(f4qAwz7)u-DGS1`!>?V>&z)=;F$ -!#i;aSI6s)KLo5@#xWJL`+s?Y^wTIsw^BVoaho{>OtC?TXum&$`}lN5)2>AlVMvNxT6`PUhk8yF1%FfvPlVqS|GO@zto#mFvbvC=3R7FK1=eQpGq7f@|%fG+{!RU9?C@9-XN2P2-1jym~ -b&~UCGh}qblsZ^{WWl>#NZQ2uJw?YLg8ZKJxnOIxcC%`P{g*}=2!(p+K8q8j>&}ync7_HrER}>d*oQJ -Fxn(A+&zkl%+Psu0ZoKO;QTv)!_ZK}Q_D;)g`MCDPpdO(3V*P@)VWyNfoK7?U3jcoEk3N|CY?P0-$#f -$8eG68&5SDJlLZ&bjcHOyxAVIxQ&V`pHK^=x1wB+LmSrVlKK4NR-ckpgRX`8Bt^DU($eK5vf1=SAHju -LV6~f`u(lcBwY08!xmc32b51DYk!zKvp%2pAq(g1k|J45Io79f~qX2bb>-8C>hGSDP@J=%48i{?ARcO -5B5D~kr9G2B0+4RQZ#R|Ed-Vsf&i>#Ow$O!6Ct{8IVlMoekRXH@HDkhmf -_prjJn^8+HC+$d_b4#FZUSW)e@IL)kl3fp~n3TmYxnGZ5k*Mlz;1gtMVfI!{979K7KX|S%QCq~tmagB -UHt1>!~E2=r8^noh7B4L7t>(M>QMJ$FiMrHtHl)*_5B%@9Vs*_^Vb+INPrWos*cxWN*BG??MLJ&NoDj -LNw5~K?Hq1X;Re;gofoLQMCPNSYF%nJPq)@hcDQ3%&#E~Qw?aFz#Q!_n@?_4E{C@aS?r;AZ5jY16i4@ -X-30q&;Zh2I)#ZAc*G|z;0YDj@TnXx1yea^+H11W~YhqG?i>iAj2GE#~F#(6vd{^_?yLwDcGz%QR+`F -44chH8Nq)v6=XTu&OydpolNgDCnGuQE$KuoG~Njh3cym^#m+1j+13AIHHjhzbm=3KIC -LZf|4QcMt@hs;kc|Eac=ClOQ1nhzsICRMCbscHZ8xM_F2+us~r^hW>?+?2#F6$-eR>AsG%AZOP;nM-> -ey3Zt5lkN$-8YM%=}v@BAvQQKVQaLZPqwc4d3#L(C>85mUnIUTr}a)l~!nU+nm$!KnWkW%6cWft2&7r0Q3oyNBV??jg%9CA-9V+FWmE9Ql`Q}Wj{ta83|%8e0UF*cbGf@9B -O}i{$tRwIW@!8ta@61iAY#kNz!;-8nLGXw^oD~TR#*N!~XBuQ^76nbQpyL&^ -gn~v*Q2E!keT6irL_wJZ0UqkJpqvWw;xI!a+J_Mbkk83pp1q=ff?NfK2oR+G@7S@<0NiJVGDdMr~w0PEml5WRO~91WVg`4&<#(^^oPbQSf6VDg~$kln2>Fgbd@c97 -5y+*9(F}AtfgMmCYthKrAQ;0itD0)=$=rHVD}ccxXR?^Q;_V7}eGF@vE=@hZ0FAYd96QBbgl(p0sLYmIgz!hipE2P+^l{^QM#V -(WIc?0isCUS4?>ldu)x54DVoR+GK1A=$ZgSRLaiMs?`&ba@M@=h6-PU}#YFJ15ER(NINgFz4!U;JswfY -})9SS23|kCg}T#lDk8c;EvFHYl0(yKFXm4!BLJxf^JFxT7l1pX#mHrT%5q-!9! -0XX~*RQM=~d}F$c03sZ8R=?Nke+HRYy+BiBs6fNcWTI8p1Fy^LmK=F`OGQQ44(d1_`RN@qIFix0D9q^ -Kl@nWF$A3x&_{YwKKZ5;0+P26+O|Ajs+~lC`hZu?a9Z~-% -Uc#9oN(Y5qm`x^Eb+F`>`dciXCULKdy?Rci@}XlM04)8Ig=&YOLfpEfk#igM-Lb9l-N8)X(9|yz&8`r -Ayz5Cl6EfZ$gnF5Y-bWlR(+Yi#|R#d9&fu9H3I@+2{9^*tq!u!>-I1~<#nZk0t8cs+vXQL_~s#~6UU?*m$RrTAL@f?g2lxA -9`MO3}QXA5$VC6yO~#TW^{esQ|&+?4Fe;NlU?>>`Z|uYXYNPXuGk?U?a# -~5VWW*!`z1AG^j0$&4@227?u^3VWwcStJ^;!JJlaJ{wQW4)L7Nl9?3>f+SExo&;BGOHg4K*X-?K?;e+ -5jtJOs13G%0CwBL -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 -`$GAt|}M?3nNA-xt;Fs&` -iV*64|8#?^*HY`*2J@NNeB-SlGglAdCxJ7RwfIhk|94X=?-x8Ml5EM4h;^p?N(PASGNS8=nZ&O1)G%6 -R<=O5Q2h&5mH0|I~YAS^uW$V=z&zR_{IQ;P%kRTNsQ=HsQ)J+PFVv-z!I0aJwz>GFhV0@J%}-@L3WSb -iu>6&9Bd!hkA!~;s}n&T62-KSZaA_uFSIFCTD6Fvu@qo*^@Z$(9joRCQO -rV>y;oij4Efg`H_QPMq8r*m(WR@JGRZ5HaB(_kq*S1~wS)<(guPB48n;*iNlIAxX`oB*- -i#KVlUEiNUSJgFHK4B?dOmVdJaXT(~4eat#~-OIE>8W>U~=G(XJ5j;(1tf{>LhW1+)zp2Xp}z*usU7! -sqw-wdVJ<`n{#d=LZ;=Gia~n0$VutWi#8J?Kl=&Ukl(KD&6@lmiPv0GE}XYv2f2D!LWBrEx$$3@LO9L -?g8th!C-bEX5QFoOfmJ#||1qMJgA=p#oT^j55?>jMp$x -%{vsqYv4hu(3~f>iadZ&FocY7h$*&;QEuuh7MI0mDH8^#5F|%%wG%Vrq{=e%-tL|&y*yykoL}#63pksguV=I9Oi7TPC0WtOqlTa&*Pb!%Rz>w -1Tlod!m;>hEhZPii|lF{q1f`9P$70ZrWijuJI -j*KSU91GI9QDf%VQc%#I+eioV%~rN%7c_^BS0drO$+36(Gc#@d8Re_Yy05vRg!@MHIGh?c#Utce)l;9*0Li2KJ!WJ3Lr6 -c%4a!i*DuCGnGXEIB?RU@6n~sq+Vae=KQp+3pXf=?)Df46^dXQ%tBp3;X%NHD+rMR%Bt9klbERZFLo8_%PjtB0A{orKk}&`ElSUc2y -3_3QiRc9^N6X)?u2Acy)+@~-q2=3PQ$}uV5S>FNKAt#NJrMw4s7`Xo5Fg;=G7_qC@4A10mL=ui3jSZW -3rSdL9R*R{1q>KvQQ^`NwRlbMks>Jh&HQsl6a7{ZzK&!N~GbsHd9&95ToZ}DCp~v@p+xu2te5})*6Qk -y~#eh#)`#{#1CHp(1U1V+x(!gTI`#t6M!#n75o76>cGVNzbr)rEL~p|;d=~sDj%50go1zsnUje*Zl_` -FfhkoZG9L(KdkOLWmE43$iaZZXNOG0LO9s#1Sy{rW{nZ$YQTEASSgwFa(&*p3Go%VcJs>5?m$nfgc4Gn*&G081XUIg!nvx-)u?f2aihHSp`km{OvH&u>@-nI29AIwnctri( -IyW23vV25@rd3u!?pT_#%X)xat#f(7$Tp11JhFl74;qjLqK$p(OORM3N%F_N;oMyM}tgaA -E~7Nl?J&GB@*5G3Ku6<3@FEJzFPwt^l+4B^W%FoquM7?~J}az?&T8dZf7NIIOmuqLjl)FsC!0ZS5W(n -*5hl8a|%w^`pfhPuuwm=ZO$8wlG*x@O#{D$K}Kz=wT9j1H8$fy;uB#8N1I2eb2EtmnbHi -DBTWtc{FSRS3IQZ!#V*9wlIn7P+3fGhAO!JJSQe8mo2+ga;Zh*sGI}4!KEkh -<@II(kldp1G9rXX|FuWFfhI9XPv^fhnb0ipmc*p$y5*sj7WpK{+dcuPQ -NTgXY)eKsvC_t&G_6hR}4HpW2nJ|k~N2ZSvDh$ZpI{v)ivXR&#OL_FiAXZ_$y29dp7-`nFA)Fs -T(3eq?~Bz+)xXGY1)dIXC&!WOgbB)A%;kA9+vkUI5c8LJ)%3Ha)1FjsV~T89~M}-V|nRtZxOJR`9Spg -G(pf0Q)v*0wT^PI0HO!Yy)ejDn-h=4GB%UXwZ;*QhuLf_r642r9QY%#&@mHlAUMHGlc&`(opC3ucRoH -F9Sm`a6}SebB$O?T5$sipBxyGsLsg0Gt8o@uAMS=NJ6xl^}V%i^I$>K|aqLanVn3)KYN)id_8r -Xp45Zkmub?^f%Gn516WbMLJ5{9BQ19Y1xu9mrt7MwcQVWQ6IOv4>ivL+K09h!L}=@>5f1oW)ni-0Q|( -z|KNs%87G=^&+G#zlqgYx8mhEakLLXN(GJ)EKj=+$6uuvrykCq_9gb@{xo|7;$wcv{vW@P~?!$4j{bb$Y6lD%v>o6R!~P9~a5jxz`gf}e;+Dpi(Vr

A(A8wg4i+N54F+|^%q|YJy0-J&rCe$w!8Pa)K -w?e>zTaPJ9YQVJ-4|Zufr$M%RbTWwMT}#FwdelRF5MM%q4^3qbOi8FuIY#iwVW!VDJ*`#?$!7&PtioA -+z$;~073}JvWj+a)FY6X*+Ra9lsDRpJ;eB%X4OQ7rmlwchw|vabFi5s)bckXT+JZA>^W**U(lu)~c|| -ne+yW{?6^AS)4Wu-!p?O5#4OpP+X%4Z4!n%`BnugZ36qSIbA8rcaw&sv^0+~xxc?w>3!<=+#VwqxWCO -C*(g;^9#%n8P>Xyj|g1T5J+k67oxzfsSt$w0&xFc&YbqB{sSnS~G#h@q4NOR^NlW!*w8LqQ~I00ZvCP -ubkyU@JNs1;$169tK!sjU=d=3hsR1B~){8Vfnb00)*vktb(w)x=D0bte#fU*vkDrhhGn)9{*PO!D{1p -&Lrt$1uTt#MVvJc7xy3kQL9LRXWPQ6f -cX&7VFmKj1}1^2rh76cFFU#6qJN4i0VN|UkdqTH?l;QI>x`6@inPk^8fJCg!OT;218LtOU`RxI4mQZY -M>U`F%^8~j>*s2b#HtC@~MF9T8NV@M}ZynS-4oFf(ZB`+SJF0_Mgd3K|V>oS?Da_i*+lahfTza_!h}8 -GVE~kHO<9aa(qw{Qt;E{1caAn8#_wh4c7@cFt4nll^4?38CU@}O`r@{dwR!G>5$Z$oF`F=-Bx%k8Z$* -I$EjN>C3Heu0o3@xJL*eNB=|&K!*pD0e1Nc^pz&ykcv+U5Q{?S@cZ5{CpDhERh;VHxQc+DVKtnnPy_} -1z7;$Ep&0Qb_@0p42k{nNBb^_Xtc~92fPD&_P>u?@KkkZO@d_^gTV|m$%0}czJ({{{yF*dR2Oqu5_G+ -G_AK#b4Co@(te#Tm9iHl|6E1@oG5hr`lMg?Wi&v2NmS2oIU19K3@tl%!*W#!!k2VBOF%lN8osrQjMk0 -+vG5m9dT~8LlG}xw)VrRo!xSCmMwjVr3~|6qpHxszj_TduhB*qzhtQC_<1VW -&EG6)V*r?*FMr2<=)TbKDA{te)4-+t+JKb}{`dqFF(rt$-7k2>%Sqv5}qj=c_pr|Y3RR`lKfeCC)qs~SP1KV#ik-rrPWh0PtfTQVdOC58tOH01T1M1D&%4j1)9`0p@ -yOjaLeUws%oqUWDvlOlBU@ti{s*SWu}mBGi0;^H?8pQ9W#^h2QdkfQcVIX-39q}L`eVARda%+-E;_6= -#`E^x=l!WEfaYqGhkwwbag<7qA5MZ29~%%x~-ID5= -Fg4P*`B@X5s8UIV?tzpjfaSk{+4Fd!|X*UvVge`t`il7}71=OOggIBkTh|24P9WI;F{`6{zu1^v!f<LCW-t%T(egF$q2_7WunSU*u-H;F=lF*#QLL-05?ujU@oQ -p@_Al)X&PUSS{pz3rQ#-f|VB}J+~;B2gx&*6QBn+Ii_>_2P!5UZ;$-5}j|86ibG3$M?L9bC|Az%nM90 -syv4&s2J?8=BBsNOmcRFjkV@B;6*Vc-@OJz#wgG!bud80NT0$CyLlbf)2TMSV7uYfX55TXQ$LM2hRAs -Q|rjGbD0d%OztV}Czo9SO1DTjl)K&wgOgAD0?

Xcbr*q}v1sCuLnQ|7wa1hCl)nuv4XiS*mR?YBO+ -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 -rh%W`bzvG`N*lb9SN6P8k3EZ8^3 -b~PgO6loN9~s-)48m94`XGnOGd40<~0tU*+$0SGMVgQ-?Flrl^sV`+m0>MTr`GPCGNX&gSl#O{U!&|BQUN^1RF@Tij~451WV$3@j$IS&lqzEZK`ahnJ -IK)L6*MIlPJz?wd3L58K8H`TRdr?O7M{Kz_SNw5}Y1K2Q5KF5Bsr+qJQA509o3mBfq>NfQ^o@-?jak@X+=DrsSQ?XK-HjmFd2-i{D{<^ -#ubFVwjmL^b;*NN1oUn6@8L)%-F&n|*#BW*30&?wmB2OMmc3|zAb4KNO28I_;1m;-D$>NU=IAl&S8;y -Vk=%d>q0LH!+4rO%QYyrU8nvLky*_wm%*mHto!OOM-U#^_bwq!?o4l^j#iJRC2_FH*j?_hFVr~~Rh6XoP@Rp4h#S`&l^Wfs{=-jk -l%Lw7NGQvOO7n&l)c`7xz3aFN9<%H?(;q(W`&CM9SGf44uCTwy2Oi#`ydiK9KT}89477`NCQdO*9EF` -LSBHYpa#AS?I_@5+2c9Y^M^n+!bxv9#Nl}ZtCfe~e~mSbGgexOF4YjQx#VX2z)1%40#*ay#O~Am${Sr --1F;E$i4rTA55f^S&KP!;^gv@W!xi>b3AKqfmGjOiFalHJ2(wT^2gX>+g6m0Hh_4j@4>ZMy0I8VVMXb -?iAV?A%@Zb)5ZXC~f$l5ps76ppO^A0dSPvYd`A3zScMlile=o1o%!FcOQT-)?&O#BcO57-eKi9=-P06 -ZWwW{S_i05*U)uU-U+i5eI>FE?;{K$}9-Fw2vu2SUVYvd~yV)v^*}*p>W^*btZymzz=%YYq7nfG8?O}xrdPQz~|Qo*n|ozxcd5+SNb2JMT<#l$$b -%>SaW+KPt-Q)DUzGJuTP)YCo0PQ*3waFM@3#{Ae2H8uic28wzAg;d?&BQ4zZ;*mXPDvNjO -*l3nlb*ogVCKU3A7ulYe06d@O&?cc#Ut_;Le68wo+-jp0g! -?W%hMwJwTN(7HDQz2W58fF?rNNv5_CJ}{HSWeg7r6Gz%H>>TjXF=s-#NPo)Bc9TcuN|0Jt)(z7&elI3 -Ni)cf3+SypycI5!hz*piQfpP&MaugwZq=$`+Vl@@O>+*@jWfV9q -%iGT?k-Y^6}>{5v0#V$?$AE2lK8Ogzle#02|2FfGXju6^KZu=ECDj+rA}uq)V3vd@NbFd(^QIRS?)XB)d -9FCx1_9mqnlWFUGcLPLit`_srED*<)o!HpBl5-W$2z3Jj;H3^q5t4G*uCp;$g6153$cVTQUmPIl*h}z-UnNlGKO>!+CCBc^qn&c4D78vFZ^MUll81cl0E2wUek8h+mmy({K33&L71p_fVLPsu@JQMXNkNYZ5;yW9mQ1u -iWK|WI(Biw+KuF%$+|xva`QrtUs*7?O(wcinG_KNS%oJ9ol`cjEBtUe)?lzO3jY>>J<|>DoCaxDSQXy -oTe1vspQ1v~4tecOMT4D`d)(Nhy#EbJZb0#Y$$K4OYC~UQ(A{BX9173EK%bg?FgPx89C_g)Hj4Z{bC0 -+nNdWQ7DKo2A6xN&bX_eJ+d+ysuh7D@bARdB$ox|mH9p$AH$Ff?gKJY}2;XIdZpaIo5qSR|1@5xW*_d -IzVs;B*XVgJ`3xY-&s}6CiD@gn+Z^o{}IuKw%5CX=XJuN8ndSU~6#lqQx9tRt_!z0SI^y&V`f?Yu9Ss -w0Yj(A)=@$FYeNmd?)0v^*j68rpAi_5mVYO-C`*n4zUY`C~=)Q0GrIoD`J2)Jn2|vr`gvNcb5|3Z-vY -whE1B+Ne^+2g59?%%%T1*hhmgFumf5I1H5AG_!f)T7^~zPaA=6sh4|5TIiyk*6a%bC>OG)=MjSSzn*f --3^gIq?x0$IS*zN;WWXIJJo(j4|K1a@eN`gp%hv_+VP8Dph(5bDN>vaqPdmXwS89lZ%NU<9kSV4`iP~ -AoXzV>Efjk2rXo)UrsJmg8p9#0 -N}t_O;<$vuJE@y)ic>Jk|{b@$c1Stzs_I)AA#i{M#S;){wi}&gV^NxX6mF#_?l8H=~S{s`BV%6%2L}T -7ZNYxHxlVgZ+CPXm9mO>BX+1+qqoN9c3cXmQZxo0PbK(Op^PAlZ;{7kBV|#WW<%r!e^>JqOqcO%lU+2 -WH%0|uIg}1n%TZ&=Vi;4{^Xe+$Y-sQWC~2_JrkFZq7N`Q{i3CcsMG6k5PsfUY0K#Dog@=GUBo7xkYtJ -Jti=N-G*L@Q?5k9(LHa30C -26aQ?*Y`BZ%EVypFD&r$E5~J0;-L5O8wdyLt~>m92?I0F)s6iO3Gk&nWorK)NsNGi0`*0XB9#(LLTeO ->luqL${W0wyS|cw^X4iTODd$hPr`aTjEhg0(Y`_!!$@w@Y#4HrrJ*8c85qT4W& -*4)mmIJ!`6SDox~KSst#3=8lqBFq^Uo(WD0}!DcrNo(>15RC908Mu82fs-Pbx4&YLPFv&{S(m)m>XsN -0?n*uSv5+F{LF@)Fxmn~Kz_*>FjF#nY05=JT21ni7JiU7bPp!*(oI;o5r5V2=5A%oN5&=eHo%hpCoXp -ClukOM$*P;JvN#0upZ;>D>$!);EuEJd5pCJ0TJ!K$q?L(H@uhF|7-;@<~2Js@KQ$_}R+5ay1~(Iu}9! -|s{&S_KEP(ME)5iZZ>)!vRh_e`UZO;e=$qFZ!eklrU$hhH@GX&_IZbhVe95NhSG8T%%aBDN?gqOld_s -C%X<>lE*b5grjH^7`8V9f+7FSW)>*(L+%{zi3nAG!!b0+8h4CG -SgPyY4wW&{T=CcT~Q|*Pk;t-@QtESXvf)6fW_Cr-ewxy6~uhx2%`x;Jf)p -}OfcJRX@nqfix5RO$T?$o(tXQ-4fI-~wV&NkKFiPQK8p>Kn6R9V7eLS{WyJr5W*HotW_jdiv1vFe^cNRRRG@tUEpiq@4l5lD -xL~ky;B!hs@O*VBla;196C>rHO8hWgYr=>;9k=QzF|qxb1*}Yj;Cve6HB(b@YlK8X0yfo(A>%+t=@zk -@!G^-YYJsVfr?7JgjCmBk5HknBZNi|IMfIqHf}PzAG(j#m3MQP#AV5HRD!Olne_k}nz~XwUESEYAphf -YxH6c{_ILW;OMJsjCfDcrJWPVY=RfXPkZG4 -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 -BipRyJIJFr{vnVF`A|W4Uf9 -k?pW4wy(VXyGMNaheh#DghYGCo^_bh`OniLSQb1u&;c9L)a2tn=e(%vNP-Vh*2aa*c@3$5B+Yw21w# -gx%z8b}GFc1)t}^XjsO4K#W8&kg~y(Q2U^j@LNqf|B2=T?(tps5fJHrVdwC)ND*-hF&*@h0Gn$Q~b(- -!yDG~YYqrQR?att8mR4RI}z!i(uhj<>8&8Im=Jxfo^QL26BQv74FZ6&0su`O{Mrz*s<=btt{h&gS73v -TmCYL`st8!A9pf~uR4%L+TN^2cc5_LuXJfj>TbZW>s^A3#Jimy+z5~nUyUl)TUy$Cen7Y})vDx=0{!) -UWlL2kGZu9;x=j~PdMU+LNo4S^F9e^(zi?|h05rXP0w)zbRQ|}=XdiU1ip2JiH3%@HsahXo()mum2Lg -JyYi#D}k$k-$xyS=L{uX>RTC_Dr}T06RV5H%2Zq~aFM=kaAB`t-J!rFdlod-|x+n;Gt2E>m~3QPf5jx -4}MfSh*6p3g>Aq!+yc!i)`E5$CfR<9eg;s%?lh(6HVV-Rc(y`g#R_#bP@tA5=l*Fr-4-6uel)pS-zu) -W<_+&W<2vJyWB8UDrxr9p((|e>-T}|pCA0=?JNIBT?E0Y`x>yUfd_(fsXKpOMdwYp -8uh&&XmicuRYE!T%FUFW;F2*rC5~%P{YTCRSwtAZ3$HF>o*|w -*0vpvRkPFjtT_X~x|YDwDa%kZvemr6-!;Xq=fH^k>Gud@>nc^4Vyd);8SWttKGa2;$Hi87+zoSTRTpji}KACGi~Vy!y%fxz<#~`j7 -Zy^%x|;%l~mS99ToVX7H14Lr?vn=V+d!LQga8?TDG<1nShAK;c~487W==QmCeolCRHU`BSEh}c8QyjK -2q`=1m~tD==RhrBS()wj_ZS9%y9n@bc06>65UZQ9fwW?N$%8dE7NG1^?N;-6Xl(YqVhrJKdj<{o;#o7-0Qv{ze)6v1cpmetajGrYCst<1x3 -@;Kxp-T*UI!oDBaEuGp`2Ci>PXalp{} -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 -F7<)k8dgY#CGxE@mA32R(y7@ -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 -&n#p;{yjN~Qxfu2CJ0?7&_E0C-}l8Cku*fk -M7W;Gjn0L2OvD^RRJv7Y{7BrnMi^aPR>NLC<8sFw{r2_7m?tU$2>#R?Q*)7j7iC|00Y+xGJ#c}aetCy -=Z_vI5BpBrA}tn5_yFD^RRJu>!>k6f01yK#}0I)swFecu5746-Yv%vY{uCtU$72wklApn5~N0s&MTJM -k*MoV5EYPI?r{RtQ%OVU-4L!j~1tS%VR4`J(NChKM`qscm10xNLG%(UmzCM$eo(yf4XiY<(!ff?Y&9^_z(@ll4U9A}(!fXqBgh=t&;yJ#Fw)NWV!}%rSZQE|AS -xSrf{_MBVDs6~6O1%4(!fXqBMpp*I>_1g<6Jj*Ne3$(taPx_u_qmjpo=x=a6{*6NI*@34r)qMd`@`Q6 -px8zL0uSkp_jl*8uX*dI?6D1Fw((D2O}Mf5cy@pwZKRRBOQ!%Fw((DkG*aSUeduz2P++{bnHn7BOQ!% -Fw((D2P0VhZ0G?-IvD9-q=ON(ER72<>0qUUl@3-q_N0T64o0B4+0YY=bTHDvNCzVbeA&0qRTk$%P(3trN}N(Uh1pjnKRiJvS -oXCQ{kKNCzVwjPx@eTJVw%RytVeU4ZWps6&Crqc|XNkb1XGQh~#abZRbxo+rf+0YZL4D87OBL -j>KFfzc%03!p83@|dl$N(b)j0`X`wvLE+p5ua*0agZB8Q7BnMg|xeU}S)i0Y(NGK|RdPNDnYFz{mh2q -t-nxykvlt0agZB8Q2r_lH9@c1S12C3@|dl$N(b)j0`X`z{mh2qaS-*c*y`O1FQ_NGO#CvnhlvF8+w9~ -0Y(NG8DM09kpV^q7#Uz>jK0TBUXmZ^304MJ8Q7BnMg|xeU}S(1)a-_k(qKakBGe#4X@%$kM#ehV4PJr -(l?^??%D|osFfzc%03#EOOfWLR$OI#3MAlLbZy8M2`#7+GLsfsqA978q -GzWPym4!V)Qq -G2+U}S-j1x6MaSzu&=kp)H;7+GLs&2!!0B@3)9u(H6)!k#QJvcSj!BMXcyFtWhN0wd&|8Y!oS>e~>&l -9rt7mb@fC7~eQe&{Ud6(`g3Hq@ibGq!YDeLl2{e(ZlGmd5L8>p5q!ljh;qNr>E1?>FM-zdOAIwYdJlf -9!`&-N6=&Q63cKr#|?T0J%gS>&!A_}Gw50AS?O81R;5R!N2N!lN2SN+C6?iMj$7&3=-KGm=-KGm=-KG -m=-KGmxK^V_r$?tpr$?v9<|S#)jvTksv(vNFv(vNFbI^0pbI^0pb8xLek3o+?k3o+?kIhRg!|@z<(sR -;t(sR;t(sR;t(sR;t(sOaGMUO>~MUO>~MUQo^+hW}oJr_N}3NmCz=jv!$9XYEbXLYh@9gVY-MeAhII+ -}Dxr0itTx}Cfva8F>n4SJDz+ -xN*YIqP(dxB#O47YeuM|ag&!6c&Zaj8l=b2%8W=Zv@1!e8397U)0q?}A~R!_3L$JEk~E&dQ4ut5@)E= -3I4Wwv@fgx;L0~iXQNbz|xVFQnP@3{CbKF32b7ja2sd=n0I8vCM`EG0Ead2KnGO|FvjF_gAz$(Pd)^r~v2r)EH7+m0DPAu$LQAb#F979F))4 -0h?0t4dg0YhOn9E77(cJg1rR;%cPjznx|p@uyTrSynP3u(zhBv&xG8aGs@02FczCjvLLoWM>M0>;997 -!J*_n1=N*T$>RpblXTKcPFWQ>)?4g&w0Q(H|6{o7e&#L{T+xIxmP24c8p-a;|ei{V+LK0U0$s#m%8=e -#M`_G1fb54jt*|@af6OJxHRH^}Rr4o#4fwE`<=l)`_#^T;%oi1l)Ax-`?ZIOGC>m5v?l;7kZiwZce -+Cn7VK>#EvyJb<4;`n`YIOa66QP(e72Aj=joGBIxwG^JIBPdO -Wz-pnR77Yq6Q4VF&oR;FvTU*UTc^GRg|+BbiQNm-%#j0}SuIL+Adom2%n(W1{W^l6d@Sy&RJ(CWC}zqhcyIT%#o^V-!eXi7TZ1RISPsM1u8Opfr#o<2`ScZ98tFLG&SPBaOQ^i@rkhbs81{i!*f9f -9$^(9RO1PgPY`^}M{ZL=-RH;%fHxG_?zPU6$i!7E9ZLQ2H&96> -%Hj$Nma-ko)0R&gzbTw+l@46eePHWOA1!J{BO6eR63Wsu1HAUHd9C`Q(=AWCUmcnP7jAuJhnLDt2JcD -=@AezEmU8y9+4<<3=bA#0aBL;h-`aBP2(adO&qCL0l=hU|$-_DBTdp1F1+l(naZ!nJNFm4@VMawKT12 -HW+v>-%y&Ds_X{2O>~3B_|(iNF~0x%At+Rf*w$TT=ocgaEfi@lnI_3`MRK_Y24%`Mu6#L|DtcMsaaj@ -25pJ_2e^&(&D7XGLu4bbHMKcaBo*3I=eX3DsD=SO%0N;w)n5}{g61jZ0yL3%aE7YF9T(5h-Xn>zHz4D*KphJNuZdP+$uR>7q`NouP0PG=|ccA%o~s@4q#hG#^GGg2*;?6MQ@mXMuhMdq$kNPa>bK$WNBA*$3Ww%HH)C -Xp{S2bPV;6#%RF?0|eKHWI94;6jg+Z&>#^M_E3_<9pMy3RzWi94=27UNJa(k=Urjn -&UaHOec{@(@(rqXQqi0ouvjNrvyqw){HCe03$Pn0(D2P^xalgR8hc#oUWr8mqwNR-#x;$ckrR13N{?0 -5?pB-z1%0IJoxEOGMHK|LAjx_3bAFST`1yMbjUdJ|6G9p>b3x}fWT)T?4j~J)&>|{JIi({}6#SL@an5 -h@l9&oqh$s=mO~DQX!>QLW(NV=zNR3Xy#Nc?w4lOe7$bXIi=U|U24KPzSvRM`qae9bkN^k6H{wlH6~?&9OFUF -w#p5czp!&mI5s#*k;4HQhus|)|2RPx~jjBQ?6T*Qb5^)saQ=^nsJktP;3L7m{3ig$Tj8U6jBu1PXi}D6bw){mS_pn2E$@f{6*@KIp6rD?lm_5R2 -+#F=?Yn>d*ZJm|5mrv#fQlFXyQQuO0+>!ihL=RM;bSINkv35JgHGB;qPI}FK^GOx}e#Vkj`3vqG -gQv>CwW)(Zj1-38FgMFGm^h+I69Xj_!|IRI?ojvYI8jr%F@hk)7VKGe6kCFnv=Lo71go-Pociw0T87H -prgSSz1KC843oj7~gti6AA7dK5D-2e#?!u%q84#yv3iMS-r-C=hA&0oDIMBu!b*}|D`I7u4NiL+C>12 -b>UnmTg#e#?sMo_U(kmV~Dz9<+GJlp9vcik&bPckx4a9jq#MLMu-hoW3IDf)UD3Rxf#n0`MLfFbWvpK -89(i{Lk?)5Q`IUBRK9TxL)iQ>a)dT?8E#30dq?`Q%cldNU@}p*m%5uD|y&b+698a2e9rmAFVuY*$RG4 -efhwMk?4)cu#F7d?&VYLv$q{0-PC_x>uJ|L3P(MYgSMFs~}AWk`H);95Jvi)&j;lO+*d0iaVwR-pbGH+e}Y{3DbOmlJ1yk`^=)H%P= -hjnp6gM6!$uq>1yEs`ur=C0?EPo4mxRMJlkeIP|1Tq_Wa9*d7)A6wK>n0a?EwtBG-8_Ms_?{i@`#z2B -&Nb;^(mKQ*YYlS22QeXlrcX!a2SWIgb7@M=c~awgspYmJO=(JeBr^b$oCOU_6x3o3~}rbxGYQKer0D@3(Ee1d4cw% -2mc7oYK}r#6oztEtX34fR1jBQ_yw612TnYK6V`7)V-Rkew_)W&c;nFStpbTz|d)3rQjGJN=H~SwEQZ) -WHFj9BTBUS4t1|it$LwO*HmUeR-Q@@o{uS;Qq4re6c9`5V}cBI))nJx(%7}@9Jdn(XiHScEe5aZV3PggdKAKA=0hjDIA&o>ht-0UAToKdV+GSoO3%*oI(4rl6s*Jhs6-&Sj`QTFxC -O!SLKI}!tb&&ch1HCkyrfX5;&UfmbP__ug=mFGtNXwOwqOZjsKbZEIl#VT&9x;2;*B%vUVRB --A(cW0`ry!0Sr+Iew?##%)7VfRNp^Z^$p&mhXDK0RC$FaNwYcQiJ*zy6=#q&sa!1&QsiQ4K)=fGs244 -p#NQ5}$?1i}7{IrwQy*eU5Af5-Q{GzB>r=pf(*3g6!K{TDuqL@F#JMr^Nr?<4K?Anu8Q}-G=btbFObS -Q^aaLFw?8L0&&$7Nezhmz#f2D`?FI4-da*3PI1(YVP=yeVK(Ac)bPim@eXBI}s|0Hdl(#B}6M!C67kA -%z39aD`Lv^C4b>4kHmlj*=j`P$^n)v4({&h?-va(h{6ju@T$>en>Lx*eC4HbxU5-IUZP`o@M6`Ae`t5 -K`CNTifBqNQJfR1zVQ}JJ;Tc2A(-cpKz6mFvDGk&A)HM&BaO}+>L&{C!jW2@f2O7Mj#VUYGhD -asVr=2j`LGqg85PTh#Q*HGO*hSXe9=91hZ4usv&89L^i6j_y9Kmj%FhN+SRg!HZXoN+95uOuU}Q(75* -x@9MhYk@dXK4Iig%%MYKL^?w$MLBQiI7F=YK9BCyy_&K}%4KqX$1tw0kOiuYW`*NHj%7$$ShCS&DikA -#Q}$+)jkfJCb+0Ct0j)4sPU_Gtz$BPj=|*@}$FsTkvIR% -0cNToI@fi`3jr*6Bd9M^R_%2zf;jpELG-Kcvl%F4(@Y3=b*rhNC_CR%o!9HqQP5ilmi&O|>v8TAqctF -)aTa_U|Sb!8@h%wJB0S(li&^I0Rrd#O`(u@3_m~$no5LFe -uRSWRI_nnj*XxIlQ4!%iqxO6k0l;^T=Ww1rlteRp|hyqhw3mVTCu*zg+LAn){eIE)Xo*05PBe{R%I-Q5>iqk@Div -$T?zTAd>Z6aaTX7(XB~Eq3ojvs66iL_wm3>wM(KS{_jFa)Xu5TbF;u~#tR|S2@DAC~(V;faanVbbvar -mh?-Co}3^AmRzi1Dli&fj{MrtCIkjJtvCBL+zF-?kXKOvw~9DsjQ1|DjpjJotmI7p$Y#Z^y5K?{|8Xm -xsbgX%!o!t6X{>M0P>xSSJqF-!tB%$Ds#rBhLn}ipu1*pHU3iqPjBTDro@xZo_KjoKQVNpsw&Yg@y` -%q788%{fk%nfA@Ac^e9Gx3mdqN{dcfaoJiiQH=0bHdWk4LE)~Ek@IHa_jd$7I}RYY&$xRk$1hT;-2>E -J?wkX}Qk*2-Hzgibga&((3j$|MhA9+N#SP<9U&6@^52g1T(Umf}@KoN>;Sm2S-%DLb4T->*29$nZ0-) --hwBL6BHm)*2 -1R0^ASGg$BDxz0Xn}%K;zIjwo_iFSs7+5IcQ5CCRYM?qe=7Dpb`J`+s;>RLA400W98y`~sk_6k_<5Ks -kH1QA#t`dOw3;UqRuPMQet`XAhu@qCs>Gc98B4tSg68*->OQ?H|xfuljSLC@QP(ylHBSI%@3a4DcA@T -+O_0ak%AAo6~wA;tIkEwet%D-{$)|N<*B6hF}-b1e-qLiUfb!JsZ@|BmtU+GRbm0+f{!)e#4dvzrbDN -Jw`%PLF9>ARvTVa*%wLNJvUa#A3ai#im&VihDbD9>AWoE55j-74z9NT{m1Ym@5j9@6_n^i-t8wx-dZ) -vG|33cW(|weEM%19h)8q*xA>Q7@p2)s^l^{upK65uBYYL`S8rJY0q5cf?o6u7^D0-0x!oCKqmc8`aQ? -LtGfQizEqC8^3@PlTK -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 -@T%Ke>Nq4sC -=mh$vZtC!;j*T^K!c0+7>%8cmr_aa4WVhSAgy8#}WBJ@EN7N?E%kLS)W75kWxmbkZeJ6&NG&?Z7QWm> -#2Co1{^WNy$7OsRpzG0tbr0sv(b3%1qmmq1R(8Vr@cz@ERrE;f>6RyII0M$G#;{1Z`#h8~SsP4pR-pd -dbeoh~g4LD2w&dBZ6}ou$g&XkrvSlx*Vq0-0LY&gd08L2-qMGK7rV>A&-(@@|`Az2VS5>GCA^5JE3$( -r4tY~;Q9safc9xe!LtD5qbU!eJk}@Pd->Er6{XEdgsles&K$z6T@aIqhNQ=L7?~)0Lbee5qbVgNB&hT -g^N2$%kVX`5$geYpamw*2Y=Ebw$G@e0EHN;5CwXbeK4UcoQd_^uKW|+(0BQeMb6%Nr+F8VZ}^;w9y;V -6P5{dr$dYa#?TPZL%U5xemD@)6yi_5tDUzzGGV4J5_-0par+e -jaEq9|)RxEpsBG;z90^PW;LdN?VtZF_U0aIIjr;A>rpy>0oI3+nAfVKQr+G4(;? -bP7K$@2id6X!EYurG(}hQK=iz>!ZRkKB)+TV -*(MCjxfa_NxM9MbDcoEQ3%SI5_{qzXwEreh@M;S8a{ji0@)#%C%;fiO^lg_IDKi}(AAYB4NQmtz@lz& -`nT7}FIFIroV`zE~0V>?eK!t!T8FnD4&)Nd -H8l3*EJP|g?3>%;tgyoyAzWK1W}CgoIQNMuChy*43<*-`R$zCdJExS~f6O|D3uGjJxtDpo}W2SQRBw8n7=gEBZ4K_ZUz -jVK2jdP93QL4PJFrwBlPh}^NC4oMnXpQ5GDhX=-DqKA7X$_vA56#POM_B4q%G{20)Zz$lqejd}~h@3C -1;bSL{%jwuOPE_-Vo{+nT*FAZvLAxHwmNMK#Qy`J-v-JRM4GB<*@5z^#4Fx5d=WIxuj3h!q7D(oi3Hp -%iGlFTDijZE(EkRYGEqtgQUt$cW7^ZtvHI1FAQ-3arXj_s;RG*$IjpMe=@r6t(bkGhf<#*|XdnTT2P| -V?XJ=$sLINi&G+c$6-9AP26ON2HgQ(Fsg{C -t@4ynXny!JA^fR(k#}V_=P}sw50=75P*qmNw2htBQ@7# -07bYX{!ge!ZtueFpto{uXfAbjFdUV;RVqI$xwiX;(TaO4>P(GU_wXq)bF#&tqc4Vc35?O3CBU{v2hNB -pivu4U2~O1C*XirfDA-XGVd5H9)c?P4~ad_tuuOERkDd_;)^129ad83MP?(2FK>*%ouLYPk{oc;bd|h;%__4Sz>ar`{b8d%qv3~5TZnwGlrJ -VPB-|h(Y@9fR%+4;%r=%pvV`eHq6%eCrs*3{+T -v^p{Cio5%DIeoq^%Y_4+yoeYa=+Ax1H8Hp;AR7uiQ2EA{1832P2_G(=UKry%{ak56drUHo4Br#&%11)ING3auY;^Vplg=bN -_G4|m^emP`F4W~ZM!HP2>Jvu3FuZB+^_=1Ss-jrLFqZ5aK4>W8{{5G(b*(@zZRew{w7?WO0b^^=-*lF -zhk{7JL!OD%(b7~LLGa^AP)$Ew+MMqDQ7>C!%6yIdEydXdVcTDt3YQ%3sfUgc2wp=kXevj#>{FO((f` -FdlXm0lGc8|$VlYqiS77D$~G>#ijDEY -f1{Emq_041^v~r?|tv^%u=2F$F_ONleSSpCHATFmcCOo5i~88dyXeoqV3uaf6+bmjJ|iLhqNumV~`LO -7?djHWJB-n)b=0t$S4p`&qT91;?ZQbwdGA)79XCFm?Fu$@bUV0lR9|+&i;Boy})gzFu#c#6>rVTy4l> -WW{aXPhC$ZS-ZnK&r88`3g;vGj%W9z=a5yskm4aV3j@up-J8CK_*7J@2dl!OwD>m%KL(_ifEOnSa3&A -~ayD2BrrYUj<)@X?Rv1Q`-d*ZLdx%^;bU~>ifgfP^R_2@7@jZLsGBV}cmmtOsNdNs^Rx8$mBq$jh(jb -gLii7_K1_uhf;NGxMDppm8G_^dAOH2lB1dU<+sJyQdHWtaF*MV4yp{?W_8dprp?-tdd`W&du6KQn0#t -4xC8vQku||K165F+KZ#xNxp0^tNC5*2}7OKBZRdc{qO01=x*${aT8@f6p7MZ}G3+oa~uD@e==f98aA; -IU0}}3jxWH$alFYy7$-rjnPjIKWp^umOQ%nCQZJ20@zG2$WoVf6zRjwD$N3yz@3LhSU)Og=mjbic6lw;CK>z~JOj()uOatHmhpnHV7W8m&^67? -s?pM&aht;QY=eX_M@`e#L_Qd8#PX*D*6XZrc@Jd~de&z*k*P)h>@6aWAK2mt$eR#V|nhIlfW^40?6Dw;s4A<^$Y@8e2y}q_^bz|@M_cl(hFR!ef+SfQUx@&J -wAqxw4ZLXhgY^-ctzjW_#ZFzn3`jdALHkXH^>$gs*|F<<9ZEUWe7;ZLJPH(JljvB|~4pxUZo*NAh#y> -tj935U?9h#>}%YCk~Aw=5k#dehB^8v7gP7aH-uZrtVhM&s(np5Y!101U26qwUVVMspjz^Xy>R{Gb)k52BwjI5hB63 -^v_FPqW@z27ZRMUNrEttaaPK&l$MG`u;g<%{qCW!A%3dz&hz0_!sO-*ag1GI_VntCDyuU;FlT9TK|%D -!bb2F)|y@5uNcfO@Kpnc>?2=et=Uk$&Z=Y|`3AcZ*2$X;W*_+$>x7NTuNlm)`!@_`SMpm1v)Or@-GuA -{-(fK8`#shP8`$>^%=-9%p*_04qYn+-H|R%fSepj^nDr4D_!BnT2K|)v!QjtW9~}dK&RUy`{(_-h)A& -o)M`+-$7|gEvYX-Ls{0$pagMP~<-JsvGE?6zUH}E2%;|m5g13i9#O_o6)WF4D(Z2S -Q;d-uMv)o7oya%3yZYk1^O(KK?PgKy#yxA7|CDk9>lS9)q7`otUv6|CDuNp1U#QxvuHt^2LPF?!Qj|k -D}xAIc(fD&tysR#w@4E8*nl5Mr$ABjlPSKH(D1XZ?rB(-e{d7Z@?+?2Am>qz&^+uea{DZ1D+&rpegbO -oFZ?)ljIFFMc#l@_+7H{cX`15S}Q;7RfZI!WF@Q{)YJlDt7aljIF_lDvTyBX7Vd@&;UtyaA` -k8*qxe0sA0tz@^ArvF`Z{>Ds9~uu(Uj>tfU$=2JIXr>GmPQ`8MOMcwEmMcrs!jJg3Aqi*zFjJnaf7Kq;51WMBV6OlDflO>IOPV-9S^+4Mm!wZnQ3@y8)Nd-9D+?iSAbPdOpLhJ5b%2% -oKG4E~dI=)Q#3Ys9UDG0hgj~z@=1o<*D1Ey3smC-MEqzb)$75>INryP~G{K2|lol>PF$ER5v;)p}J8^ -A=RBv-T9V@MRlW#VyatanK)71t$ganm87T}aEiK7WjX35%Y@UYJL>jnm{?S|%rHSGQ>1Rdr3@39>P9D -}sN3RpTd3QKVS>JkQ8%u;5OwFf-5#l1q`J{ZG3vIs-4^Q3ce~MelDdHwa=ZPhZYR`@k1R#q=%f^NyK% -cIr{sL;ow{)gPLaCN$rM#LIw?in=){NDZJ}CCn -d-I}Cg{3^>ULn5>{9A>qPo%Ai|TF@bu$Ls0d@O~DJ`m7=5@KPscvJKFix -9QOzA{*SI6s?scuc%H}*iTty!^E<}1ngDHF -accI{gh6qThY|{3`@D&O=&Sq0MFhq0X+Md(urXrQ{5Koc4C+~i77pL-7X9hz-6dAUv*okyDElBzUl_` -%=2zandCbF1PM%}WQvhvg|Th~2}hB}Ycxc#LPi!X%%%fqFPO5M9su#BSM^oP4hv=#)+lgfH%6OAg>t -XPBUMbx#g7ivt-q)G=js3=>OCDWh(SVS?6E+z9VebvuYDWrj)qy6)+F-58HzhrZ^k?tJR@7E@YOx5Y4 -#QMW~PqxBSHO2A&I+bj-bBrq%Lw#1Y&!(;~3jn=b2!^BBU>C-ToeoQ%auX}gyrgT8v`G!e8bz8h{uzz -K}Zc9vQQQbbNJ2WvRd)jxf!US_%%|-N*b++U -Hp8al0T=(n^6BpDi+LAL@UbkpncfRTtZOM@sCKlB#qweZ>-7>?(4RxECl0A{LI`mbhx}8wBOmzdE{R_ -@4)QtlW@s=DJb& -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 -@Y{%F7-ue -%`*afmFev`>nS|`CP?$ucRid{8p{56~PSp9vh#)CJ$607lSjepFpB -JN`RU@XToKK=qwFJDhzwC~Jb2@uF_}ekZA*UO*2(`U -wr-@++K9(Gg<;oX*kU-6i^4Eui{Z(27=&SbK`s!6odv~k3m9e))pf&8CWfmP!xqDIfkk21wHP+j4TIU -%4c`EU6)!ig$E9e8tC<|GS`J$dqZ4a~cP59;u)_<=VFkvG>$qq+Y&mQ>Jn2K`F#KPN!|>~dau}C4D-I -(~kmRsyIh^Eho%}EdpyjaTu;uWik9Lf%6o(-j>W6W8({Q+!&0+MBhr=N56dVT1lN?4D`8bT$DGt|6;j -lv8#&x8#9JU;`9G>*C-SQFwhao3940Q@WoXugtC>Mv(MUuln=v00fUC*7vwKy-qkhZWK#==Q)7;=ik_ -{(xC4x{fBhasmp3^~PN$SDp(PH`A=io*yeABWLNio=jo9EP0YFys`6A*VPDd3$hp_I?->JdGcQ#m&Xx -0{k#=ndC6k6o-MuJRAnkQXEFl`TQ_CDTBlK$b5b{<}hQI`5eYlD8mn9xu-Y`IiDX!>wJC~t;_Jk7^-d -JhlP3x$SDp(&c$IgF2N6@>k|Ahx}JA_7(M6Nr?dItbbh!-9A=O*pThuGJ`M|hSl}>PPsL%u52KSAau| -K*1=*DH -;2(WABTaRsr)cHnTEsYBUjxT=B9-Fu#lI)epslN5I8)C{4nsG&kxh#5}YsQb6C$0V*!^@ -FCpyHp>;kE3w~IS!-5~41BWr*wq9auq+-?7dWmd5ynv52>*XcD%DLwy6iGI&1Bv+@E_0u*ki3MzVdL` ->xb>#WOU!}8!akiAhhhH~D?hBq;qvklW^lO7yo4Tywdy4lGB&Ow#=>$K0LbTug}g+W94<32Q3!_%@x$ -nQs=Bo@_vx}ZT*y9Mc3#35Kdi-JEkCT2AJ*bEiq_L&L&ySkDg&^~2`)VO-!0{j -grW1TJu@{II}b^j$`Ncsu!FTen7C%*PMoBd6J?^WwTS#(*8d;WG0Qdi4^5AD%;A0&+e-EaW8$;jrL`_ -3DQO4sWNrHGE&qJ1`Qai -s?8O{rx3Bqr7_za?HA9|1hxMMPGu{su!ePkUAunMHhoNp?4hw!5@>J*i#2jY#>B4eYsF%=tjzjRn0*8 -hA;W<3VA#fP-{Ga0x>LrBdIP^FSdG7VYGq0Br@)EcXBhOnaV4qI!ISyFa8P*T$<%j9*bo_=ipTl#=OQ -7`(_vse3UP8|g>zywl*Xa7dMZC0@ -6)k6Z@Y0=s2?t~ZmrDx@Eq!g^*F5e9EZSRp>C}ZKb+0sLi}(xhfDOsrsO5ect4$R?qwl)34z0f_~H0D -4t9%eHx3Jacn%yE@)81v%dD3WI1Jg?^Ck2+TtvMD4dz*&gHP4A2x%-1=LFj94^xj>*a^_{BXQa$JV>~9ELpq^M?fv -LoVa}bcN(4^f+8*UcwkZ40Su_CCcM4)G6}Akf+EG>v7na^M{?7!)##}!(rDL4(s{hIdE9d4?`}4!;p7 -kUP9n7HlXsoj{|b9dI`w+IE>aMypIENuJ>_3F8qDXkdqvSnyX$y;4oUJI1D+(VaT)d!%kd3%vSJr^TU -?ImcuzXEYwQ~b!%GhMG`oS#?x?E@WV6baCW_f0zNO7|t%2m^Frl$LaLa5_)!6?=cP`EfF8L#+I42!v_0yguQ!y;qVYHqmJuEy;S7Lg&h^jSH?C|za4+|I;(h>rOh11sJsx`J2tR -1!-HkHGAr>zxn>ZRd!*p$;HbnGzH;_WcXB4%iZ1rDQ=TpV^{Kg@Ay{jlY*p&W*6YV~mTHXZtyd-ZS;e -psh^IQ#TrTRogk569KRY<8_5wj4H-$@V4e~iPaKX9M-eLp!Uq -R=?bvJT4@O#JFK@&CvaHrrAQ^(VJAKWp3RKqu;sAj@T3oRdUL*~IM90D97f}O9L|0zQUM&!_QL?(yg6 -)4TEdB+;^0%Tqd9Ez5|+c%hn$xX{4iug)5DOb$x9f)VW|0@;t -pmb5=Eqk3)!VZ>ly9RX<@jC>S6S3svWMyc9@Oo&bGsr!zOdsR!i9G;XlCPGT%)na9Hmtj_h~S)d|CF_ -^cha7&aBdkf%E1C)*B}_%K}|7}m1GC00vh7Y}d$hv|efegM{K9;TzC*7!hKH*7I%DuzwZO3d61>t!Vb -49_?#Az(QB&2(CBST8G4X0e3U5kK@oBt9e-!xqC9!;?PBeIe59-LTdp99nKzr)EufL1=bTBKrlQWxC- -)FsxTAL0JhNyv<5j4x7qhAu9pdzNon}r{ply`5r9+xs2p6-Y$}Jhl^+(;5 -(0;{o@hqb`TQ_iCpnBQJrfUF)APf^YnsdS!+P(fql1U}wO9^Y4lfvoOH2>%1P*7PEFt*e1>`XLF!h9= -dYqSFA8I*lIczG2_3{#MboNCYm9ncPvY+9|<}lPLI1Gf&@I@R#UIMMBIsiVdT4TLi4qFbJ%3+W-U)7r -MLL{`#Rkc<`ei&Vs@FEU94r}exY58HOi0W8TrHY%II8oR9We*RQiKa#lO^BA60t&%4pc_^?UuZm63mGH2MAT0uOYD!(R8Szwd-Ux-jfd -x2f -1{41TVv~t~3bDn+KZDqg#hZ5D6x};j*79cDP!1Dsfyg8JHbfrLpF`vk-3pOMvAm5Er!p2I|}Vb5hEQKQC0tX{9qL@eFO({>QCZoLK*v1q*}6R~1FK1cf@vN_uQ7a$g&qrZ -g6=V%!spQFEm$mi%g5cwSaHN-HYR&<%SK~q(g)#y9zc8F^Gs2VZq0K_n$cBCDI7RJ+VZE1%2O{Ue41{PRkw#yp)i9t|v{+V13Vi;00x~4S}C-+a(^j}F;Yc3MWb0 -~Ash(3Ou#4si=>Xe+1vK7s!OOL!1@R+N`O4cMl -`p-2BbBec2T0|M@83z~-$th{HEC%i9K<&dzDqiD5XT06AFhrYh;ObSJ>Vn^#Q#1?vYTlB66v*s{wC@5 -g#HTYUP6DB^hWkoHWST%P0d@`&D)9Q_yg6cS2LT}oow07mTTE^JzGu&`Yc^aBcVS>dNR@%NKZ`bl^01 -*hWcORzmxO&6zNVvf132{V7s4!EoaZFm%V}$HssW$F8xU{u_8635*{KsjI@ -DF!o#HU`*ia)lKG9g`8vt`PThQiWPYn|(oN9d_v+?bBy*>=`8LViYi%M4PyANC;pt| -$+rqpT>nY33J@3l1gT+w9#mqsvNUycOdwF6rjE9R0cC1xh=k@_*_t-m{({snzIr-2?J8%3zbPm;?J8? -SllmN~VQ_VP+ViA_v0>OKG3_dMQ^TmeK&px@G98qbc9p;JpuR+^iVf<^q^hi7L@dflyUO9z@oD&Wa!gOk)$ws}kg6sQEAvfK!{EGhlG;^9r-ea#o76Bk57+PxsV!#og8wF2c^ -wxM3TU{>?BMSbN^`ptw}YX0k5qNzrHk*}Noo_(eL$-EE6Bwq|C(ek&ggzfqCyKM=_69reV5LAyA!LE- -hOm>B?&m;PM&&B7lF9SUNc(iDry4-LA4T_)Cov-L7&o(Ueo3+pV*`zpEswsM0SKaVI_opY<@QYQEA -nAFlE+_4HPwJ8>|$F7Ve?{-pu+F;c_8n$awb|ASN&83y5TQq?zlG_;fppJc0D1bmHDFZJd6z*1Hc`RM -V3hmKU1D<^yXv#b8?mq)(JLZ9?UM+YnZ>dh)az3z`X{qA{xdH4Oj{>tF~^*!qMcW-Ta>ixIc?Vr8SJs -Wm)cz(PYcGd>#y;1L?- -x;dygXQYpcFS#r-*Gq4<$b=t*6FUF>Ge9R{z&a3EPuDVy6)ddU)|NUbKTSaXgRcy!6U(=!DGSW!L{J$ -!4tug!BfH0!85@xf?o#D2G0f02fqql2wn_c3SJIg30@6e3tkW22;L0d3f>Oh3EmCf3*HYt2tEux3Vsv -(HuyOBB=}wMY4BO_dGJN>W$;z-b@2P~mGO_q4~~B_zB+zr{P6gv<44AijvpWYEL_01EG^w}@_47!^4g -uI-RrP>9d@t7?seF`4!hT3_d4uehu!P2dmTGwujACE7CEexg=mq*N_mJDd90L)Xw~QkN!Qe>(+`risp -ZiRlD?_cpdTcSQ>#foNIIuhi++%_POUcmAQgF+evpcstZqFOIo+Q1ROEDv)>DzwZCX!7PV-SuM&73Rs -3#+D(|pvEk+*3+>dDC4G#~Y34cupN -=Y_d|-?4KZmiYG89pBv< -9N*~njz_)QmF;`OoBiI(k;>7pAFW*LZ+3e(PE>Y>*N?WOva)i0(7#pL?rtAjza8~9`-5ZGZgvKn(eT* -LhWx*sXt+J-Z$yJi_ttiQFsxioI@pR%-Wx`zlE1zj4bS&Cqpg*d5UR?NusADyyBg%jAtNRVsn8|OlQOeY}SYy`p+iYI43g2NLn4LHA_gE -k7pxzfVr`>ZvauMb%3>{N|EWHPJfBi2bXE9c`(W_^6Z8fPGjf55J=tnsJpOvp-qc% -Pji^^9;2@3SeZHCcN$4YdYq&*q`tV(r=3)&kaERr>ss>3vmsB#Y|>}UYyoC7;+mNq6y&q>nJsde9Zt>cz%zSUnC*S`qca=olKAZ6m`&^~ -htHO9-8;ZmOP?K1&1{=&B;RM}XCvX%K3jNZA5LaN^~^4w*|W_xCBwr95LWFdSuF -@8d3Q+tislxH)770K-UXOOeqQ)A<^{aQcIZ0f@^+cO)j=jXE<q9p+*i%RV%0L)Nejd6K0gP&I5r*02p(!#3n -`Y@?LPu#G;(u?=-1Y~ur`+P)51!#3n1w$-5ZM6VoyI?*dfpcb@!-4V7WefAE($NZkCd}s$2&<-r19aw -m68)ygKYcC75ahc6(y`KTvF1)rKwDY~T6|~V|>JMhr<&>nrDn} -DQ1XhWTu(>6fc;Itw8fMnEqe$cMcBJDApcxaC%%tO2E(1v>GQf&v&hPr6bwu(oV5ZV$Pdk47Tp{*v&L -)$~Uq|k=!A4R5}94e|KIhO9vw@g)3$Qji?QUdO1o{LCw4Ff -PMW$UGw878`9nI_;UqBL%J=DGRjioVJIy18B=jYVW -`m<)N)6%tPA_+Ov;Gpf1!~{&3JUTc@4Bxu(b$M)|k=6&R1u3#0g@i4bb+hN6G^2DnXlHBOcmn!sZ2Sz -;7z2jZUV5Hsr}NZ9_a_2W;(vj~Jkh#29`&%+bSM0K-&kXz>U?>pip8e0|k-M2dW -7LoOoH1}QaWL!Q6Uc9M=*F*`pU!NL!X*^o76L!M;u2vm*PkTqsQ)|d@>q7{FTHD*H|XEs!g*^tMX4OL -?{WR2O7HD*JeEFD2>joFYVOGnUJV>aY*W<%AO4SAf|Pzz$Vuw*v1A7^$JjojtJVs&QDnnZ8c%bm}r;BX}f@S -Nu0LbCUz@mTdf^&0d2!7?INLVaN34M+ptRe(4-^uia&mC`gBCi*H;;B1GAkZ+J-&skY|`^8`k?dSvs; -r%q}n;F&OQVC)x&P8=gXzJkd6#BSjnSFrV49v4)?8%kHy<3uYJj6mkwLN1TKsApB(Eh)=X9VfJD^g)} -hR?kObXvNF49pKW4xjiAlWw`GU6lW?Rwk0A{kYYd5YdDdtbXS59)YYd5Y{>BmkR{t#V+cn?25oj?d1$K%E7{tSIiw>?wcc-;9z&KT(Ka}3!y0Y7$B<=NJ2J1wkfyaGS -vJQ1|>x3Cxd7}Ax~ -CT6qG^~_fDwTzLr!Du^)v=8ml5g%#$NLx+93?l7p2Rl0{Jhat>E%bUn$VI&1&j4+-p7+;7qU|)V9m(I -pjLPv-yUA#0&}Osmp{*utp`ks8NZT$Qnfc<8nMc|#(vih<+C^ -?*cae@biL{G`HtE{CqKMh7f6r_+U-Nnl37Rb$vwb=;5wpvkjyQP?=_JuE%VWrrZDAK#q|H0>&>l_Lyd -FaW!=+6}AkY2l5AxFysP5nI2eqVMe_%*Q7WDmoF1D~6-tU*6jzBHRZb(CtEIJg479J1s_+px37V6>g=tbsf; -qg}GKBO+U*&HjOhwwf^i7;-VULwcuOQm4I`k@mctHt8C)v#&p3qwb-tCah%JA)P?mu(QSv+IBl@(8;{ -t%32(>4Uu+PpzUH0y8+sEJ8SILju@PFaeLSchPJEckXby!hSx(|P1qwiZ71;v;OF&42n(Q -`@S5tGx-zG;jNn1{BSuw`65Vi%8~^>U^x4{bGJ^ -MbZ_+GTaxP$zTRkjuG=9qJ67HsslDVmCN#G%oemu$$tMY;O%40uOC9Vaw>Wi+o*FS)8^Z(=KikyWN9G -7q9rsk4I+zAkqNsB3F;Nco3Nn?FCsqk~wWwtB1Cluw`65a){sYR~)npj7Lfi?eZ-7oBf+d$O+H(L1e~ -kR+MM9ny;^7c9_p>CyPhym<=Qz#>FGczf2qQAzY>n`LIUXroA-*L7N}GhxTZ~=Jn -JfA@)iQ0go@Vig)vHHbIc-~Nldd^!Ms4lO#)Jj6MJ}~PF11B2wZ+t4TjWwZ%%ygiOKst)y?E4yTF%Wi -kY~2!532v(n)Ju~y)}>*k=nIpMs4j&5b~)lW=idRuPrR7Epn+XJhhjF+WGs~XYREPYyJ#g8}dSVZL}_ -}*LJWaazUuw$f$jYz6Bwl+CCv!Kx(^4NNl_|)B?PA$*El)uk9is$@kh&%T4WiE2Fl5JFBPm;!zu_-xO -(|kU(8DYD0FPkj#nN^we=euN`F6)^(Wq?MFLlzZ=n3Uv}27-02TC -(|;%jqjI#;PXCZh(W!oKI7qt54ox2SDg3E&zO%h859pcB##Z$B&J!nAu6*;t>5Eq`uZyI(^|NPAUOID -S{nXi~F0DUvasA?@(`PPK)+&3W(UBuluY=10xGctHq2zE}4wYOTm#a#yjmtG9*T?0$k{jc4L&?o?xvA -vVxZF~5o6EQ-iL}U?QgH_qT8$gfwIXqUPT#pnz2M#`v?6hb7Fw0pp^uj-Xu!wo6c}+aUZ}v3i}tvVF% -QQlRAA7<@oEJ|JshuAVA#X)ITaZ9aJ*iDfe**)6>aERz3@PxRxd_B)au3Phg!YxM^&v}jDJ -s9O9DiEto{2Ig>6CZHCCvCm -SM4Yj8K1U}YvN>A)JrIk}(eFd#bF>DL&(R-1CzQI@3s`7JUMNl7pUnY)lBsq3jvVp38=^|?)=`~^v3{SW4`28X(j~anb$X%Si{RxXx^b;NypP`pM -QR%Y)y@CDM6$fLaN0b){|@O%nKeBRN@O{AEy(YZEDH@9(BC6nmI`eA=3hycwFcD9XQay;RM0+9lB`tF -!5}sesgqD3X3bMCAr1^o>Lx_>fGp=z0gKCWLKU63oD|xk!F`R6B&oAbzlXg}a#Cll3HdFOlQL^GSZ|X -oPgm*$zY+`SE~+HIOtQR#;jFm-3dw1es`KB_N2ZyYz=8iu)_GcHQ!g`pHH?~wr1$@!=5jto>p*ikpW? -v0rG6s}4#;vk!voVMJx3M4gk(9INTWkkv`?p2YCJ-`3xNts7cr?LT-|@4WO>gHa0d6W826o&C~<2sK)(`yZ2>-t*LG?SDdYde=iIh!BLdBMBKs@~>YdS)R3!_y3*bv`kb_7r*o+ttJf?- -EO2eY2!%#lhm|I9QS{boZd3fH(iC&p$Hn;&q+=>grWL^ctqOigq3~0*(jswuKPFigSw+hE -6O!dz2H02kQS&;)i)pDtdqCqXZ4Anw3|B}WQ&opX5w4I#7H?Jd5~&h1 -YHBG|55v~MeVHNKk4lkxm>L~*ol&A5rf(sD`YNd@MV0y*sVu*Loy641DfJCfHWQOm7l -OeVf#zE)@F?iLx$zGiI3X<;KCjOR6jk{JR=+IZ?4ndFe9G!_54b1lz -jf;=`-glYn990-i@v3c8wiFtaIKA(%C?e)&iwQhHPD;mnj3Txl!Z0$sk)LCb1`(|e&8m=Xji(iaiieHXjiN6z -nH-0sKEq*Ngpv<-8vp=ekO2TG0001RX>c!Jc4cm4Z*nhVXkl_>WppoWVQyz=b#7;2a%o|1ZEs{{Y%Xwl?V -W9x9LH71-|wfGSb*$c$yL=mPXYn5q)1Anm65C%g2^({j-`oLGt2BqmLklNgL%k-KztlR;y46{K=26(@ -o{j#IK0@|d8KcmPeNBu-P^a@w?kjobk9@In?$$EHGR9PfAjDEt*$-tsb}u&51+Z->p#=&->vQ4AKe=C -H;&Yfefn7K#$cz{zj>l|XY|ssmW^y|yfhr#uI=^qj&I-X_IC!u<2P<~hCAKS@%`)e|Lu22d&9x??y%O -oy*C(+YFEP!cDv8sA9YWMhp%==mj*lC-Hna#8-f$H=LY+|QSWXy{FgaVyV!fVyL&(U|N2DjT7MV*rB2 -kgx;HyfyN&bLw$7cpa^ck0_Qg|M=e93hK6CbB?PTr2MlJl9)`{AKTJ1>f=&7So0D|i2baXT9RZpYO#? -ksFMo*Kr^%FHSjh>04fx)(CNe^+qpCLW8v4?Y{hZeRyPoi<#3#8uw+kTd`ZD8Bak#Ex&y-3=|xm_aBI -JYg*LlfIxCeirYo+Hr!qgP13ar7#QHZl4<$u0i2Yoy=!>U^HGtz-1}Nz`D!Umzoo1K){*I_~y5Y1+W( -4bnC~-5nCerd`rOoWo0`X*@@6lD6?2xfMs-80?XzaR*^GQ3zf8_6{IIQ%v_ZE_~>kZ?T?ze}zt>HEjznv&Mw`30Er^Xbh&2mh!-80`AXY4h6$@f50I_C4tXU8%7Q~tbv0*{1SP*L## -F_=M_6D(HL993s8y3Wh1+ij5tT+${EQl2gV#R`3aUeGNL993sYZk_EPJtLYNr4!`DG)D#1atuD`E+Vc}1)QidYLk%qwCEh?PJQ>k>iCD`E+V1&UbB8N`YOvF1P=*UuE> -aQmAhv9LkVa$qirC*HmRJ)KkJzvvmUzUxB3=ZKm<6$BL9AI2YZk=b9&zBSh*@hwb099pBNkW_h6Aw{f -VgCjI0M8W3cWy_p@=g;T%<>w0pbD`aUDHk0>Nw$D}f?b-XNA(6A}>nTNAt@=0Pk`#L8O{ORR}9Jz^sO -@zQ(5#VBHbYk~*yEIi^lfLNx8WgrHlxjc{9^6o(zcjXuB{H=+_1u?IP%e5v0i(^gT@-+gs*??H0h>Kb^p;!C}RIr6TBi;EQrfoN32*7^VWo6LCku@DnE#QJz -~v)xEPPvupm|}hKLj86bvEygcFz5X-EI27s7gX;D0435d&GN6dqG*_N*@SFw&&#DOP>1=a+si0AJSOBAuh -nkdpE2F@+)3mq~LLu(%pTLL{u187k|T$DAzgIHoslndfgm#;0?I^tzoH8FohJOht-CLo@JB9?(T!z0e -HCNex?ku?$En{@;=vw5L|FV?NHBKG%)%T>fIh$Y23-Xks(#J-A{wGod{Xy)nhy}~n3R*Q$sv<7LBlfl?d=)WoO-Mkz4$Iey1hK4GC$lD+0AhlVMR~KXC= -gd~*8LI?`>&eN9EddwV$FhBvmn+Sh&2mh)|xOJi1}3$`4zEdL9AI2vmUYLK+Lb2kbqdTAYPPJ6VSTae -Jutt7V8KoRvp9=MXU-^#5{L9xzP5$h5aaefdNrHB{Yn#cfgh9V9`iZ}zrGf~7D)`UzE -=I&BQCXA$Ag%E#hG0-p;!>>A{4RaK& -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 -mN4qNqv#O;?%;uzyBTIL{b>^# -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 -}p7BSNfVa}N?AljTWTR`^obXGW?k8_4Ve(N -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 -SKTk27j**EcHoJ+0M4jaY -g)`)x!}#XY~k)pHuw4vwEuW_@!14cYw>c)x!|KE5+~V0GF#GZPM?spr6&WdW7ZpJ$w8iA$~#`Cpfvwl -x5~>^)$pEoTxgdl=yL5{FG&uDt>8MW`_7V#m`thoZ?3}1&N=tdeDEtvdo@KHhBI37f1Y@)r0mW6~DAr -kMEPa9PzuU)#F?7lasrI8r)^|R8^#HRfwNc{J@)9J>?a@)Z+)f5m`N)$ImH#S6^NPUQqF){YGT`0bUd0_bvEcCG~K`&j^0b>E -X)pyCQzh;ujLXutuA!yfxLoX;KgH<$i$65%F_h+?DR_au&bm4{)KIV#JT;CHHn+IXxIp$uxeB_@x%V -l=z|H>s*z&O|m)3v%C7=VObT$Uy;QxCH|UO{2cK!Ru6h9N&K!Xe&OD()Z*ueA9y_~HgJ2pXkRd?XGJW -2#_9pVFIVyFhtv{3c}AOx4W^&0$;DaT}Q+ZykIsbm(;^4evbG##lIrN&saSb5r3O}fQv -qh@@!7V>S?k$4~ZY~mb7{};^z+Tq8~qBUi5u_%Z=jatR6I9^1)qB@e8dU&mKQV{I1S#ahtoG)#K{?me -lHzrtvc#KbjXKe&03PNE$zR=IW>Txy{{GA%3ai_k4Z}a7`3H9_m^sevbHEDgJt$-;yeR45wr{e$MKVd -i?yjlT<4Eyy#E9 -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 -N!=c!Jc4cm4Z*nhVXkl_> -WppoWVQy!1b#iNIb7*aEWMynFaCz;W`YbMoB>{l}BO#)P6FZJKBkdp!TJ3C -RXJzCKIf?Udp3ce3d6)w+c8p&k;0HK{B@@&0H!x-(4wW -zMwDk8TZj?xz1=pJ`p)-JyS}Gp*-`HwM}JHm+Q~^xU(TpV>P9`14P1UAXwXF50(&gwAMRc=>o^#&Y=jfBpaf|3vMYL@=`YGoK9gaTjd|I2M&p1aY&iC0O+EE<+bP?6 -Xai4RJTb$W>=X{GJTyWl79N~Eb$r3w9h!>P@LIsIOk -A|*=L>O7P)=S8M8C3`7p)jb;}t9KERIi*5a_Yol)fAuJhJnBFD}d6hpgLM2oy8MX<-g{UX@s;MDo3;sg$y5h!N-j -+0h#r+(3i7UTW0bKGLe?>Z?I$NiFXeHB;I(R~b#)9I0fr64u=2&Qf*4i^!%Bc*g)pod#IQmbRtUoiV%P-2u!0!YHDH(s>V; -tnSHm!UWOEqS7{e5v$M7a&*nw#CYFJ00hP430+8e_0dDUl!yD_;!zs^L(kL=9?~;J0d~1o5_#8 -m2c_Q^Od;hB54bF(igH#IR}D})u_=MQVGS{?A%-QUgsF~U1u?8 -4hBd^nMi^EQ!`fF38($17h+*grhcP80SfbPZYFK7Un1C3jV|%G#Y)Tjrh7H89rQP8o>`ZaVRZ_zk!~U -j(38aRN0K?dnFaiuqykW@_9mcR(z8c0DhNeWL-Y~{6^oB8pp&G^*#%frHP{aP-u!b1Mri92FE-@vDiP -h9F9b^H+bdUy32@!^gx7C*D+Js>TYf?2VF(ojDu^Pr0*4}CuW7vO*PJ&@L`P(;gkiEOd@$VRx?u-k>%K(iuZ9)Gu=2*R5@1+C46860zR#BE1eFpP -!&nVNQv#}C0fw6`SgQlW1V1kf%hYh>1#6a7hl{8>Y2V1t_hgCgc+5Ch$s1M>!vb&E8^d^sPJm&oh84n -an7UzTN?;7DMlq};7$(ZC^%7kPh84yzh37G>*&BAC6cWS9iecD9fMK(I3}ZE%2r!%=3~Pwt1Y$UW7)} -s|6Nq74H=KB4SW+ngO^F0yI6)Xr1Q<@dF^nrE>R6(S2g4+(mt8l!$pvdJegb0H2r#T6hOsvsgBms~ri -KOW8(0lXFboUU0;%CZ-Y~?lq*9`~H!N793#En|FIWp%DN$ldP}rw#SXL>4F>KhBa6luil#pOpHL8X+# -IU4pI0iM03)UJ{!+43#_^RPRdrDvoV>Jx-l)&DwcinJxHLMVZ4a9J$CA!G#hLbwfa2=*ZEozue2?r_7 -S4!X|x=?D^e~AuPN=PsqL&2Ir4M$KZA!y%F0t`bntgB-Wx2>xR8e30$xy=%R~24MPlj*A1_M8m_^VAj(DCz7cSVPNaqt?xJ&U{a7#@N)2NSV^hMvZ -a5T%{Y{D0@`jtlF!qKahQrhiLk#0eiHP?bE-@u)QNyw&x`6E)B};T7HEi$l!^N9?ecCs0rG&(kh@pKW -g1TYLb=DN%yq5;ZLHhGlibC2E -+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 -rLBiF&na -_T1aP9i`@WmT9M=#ylxxG8yo9s^y?%e&-?8~#S%)UDN+U)DIS7&d`-kiNPdw2H!?1Ou+-FyAs+xOo2@ -U`@!e0XEy{{T=+0|XQR000O8`*~JVcR(3*&&lF&W~DH+N -;o6ik?6o)Qu~RIO^Mg>JwVN3{p`#(c4462uhL9@D7bXDamEB}36-u?o27DlUQ$ALUZW=m8DGK*(6`N# --gxftPh;ahJ-KWNW!bs=0!)aRK#OO-B=Z)vM6*eHfB%2tyV!`Nk%3B!&o-#2{ -h8zoxCHc2ff6uStGNgw4ST-Euj>n17g@YfhLQwfN`3w!UAd<0P9&+lO^R^Yh-NH*bN%#WE; -;0BYgLLz=4CVaXk(JGi=ycCvVauKFU{@x!$kA)Ztd}jfZNIzJ;_}oKqk9#Z#8Mq-VNfyrYRGQ?7oj3x -fK=V=h?}2e#CvGY~N}yiVQ?|rn1!IM>tH^&W_mY4Y==2(VESF4?)x9amBb@*bBC~TW4&IOaA+)^OPBkDl&P7fXSwSm4+aUo3r&hA}GX25OCtqk+fH(D;FfR=eBlj- -JSSvpec{Y#U5#2rU@2hNG@A?zM(67!L=Q-Kk=68B0@4$=sLZEX7kaxe%C1+$1jd!#MOOEN_So`V*MDZ -w$-qG2B%XaIr&{>$7U;i(nSTi;z5__rRwR2vvc1$+35^J$I^$R`U8*a94QM3S8@`SGJBwYsj?F7-xD2N>HZIrM=}|KY@!Pf63J5;sjXdABU3c){qgdbGz36xvJ;B4MJ)f)zG ->qChJ=nAr-uP1DR_=O4#tjhz1Y5&ncLO&dR$cvAx?McZb|12)mD(GL{rBotAHw`>CibrQIt$2soAC0C -eypD7is+qmAy*TaSD=c~p~B8yf%tdYv#Z{nT#xNhP4^`7;Vo$xn>_1JuZwh7(bS&gwq4}D9*y(Jwb$g -tU$!4yPPdYx~X? -2a~1fRb$e~q#_$^s!5kwPO?NS5QD*626&5fAx(^ut=BT|s`*%Z^?MlxoT^xkP?MF4ro@zmb(pvvK5GB -CZoP(WAYZdh#u5+6Oi3g})_=?o@We+R)Uu*!2SY&!d_}KP@Vy79DpWE=YVV*&;W!IA -PB~=$G?CLMT;Wt$E}E!7q*Bcg7r8PnR2?K$4jo$^FF9ECMZmU{aLoJH@$k@1)6hN*iNmKt+nI;4VB5U -|jA```56jckfu`hq&Q51H|GPRNceCpcP%?dDu$;j&G!1GV&7J8|ZkRqBY2H0XKY -PD7)G6(mV*XNiF7L10XUI@u*p0TO!#DZbhr+{Wbg0?b?v-3Y`yWtC0|XQR000 -O8`*~JVHgyz`sJcjB?J;w0>~JhqEloUdkm{HyJ)@6mHX@a)_@Jzq!+1HqTt<`H}l^72v=9t5 -8d@2hGQBHd;RgOPj{n9?{B(bdsVw4cf~Z(Vkx8_Q%{9VBB3YsL&UL9b17a~C%6e#zE0Rs$c6)>GzBni -)eZ3SHmpfSWGbLxKW38z{)Tb1UZ+w7z7a|$c-0%Fvljr45nJCXrqKQO{5bXRD;g3SZH5wt>lqrrbI>NJLufdR;3j9k~D`R*(C+}4EZwrO= -sY6%LaAqknmu23<203e8dM^Dm%DNQ&^<0cSb -OJ9cW<7Dnqcl=CWx{lKwyU#bUMSXCKs_&4*dJ_u(gO&EB*&=ZOJoTO4BRsly!_m-vw%HGc?zlgj(O}| -Gm!@6E9roslMZ~MB_X@ -Bbc-tG|1XE?d^P=9>?lN;%(R*N(yskL7?8?6>#kLuaM>)~+5W||%D4OymW`n`d@5pp>*=r&~O2P+{td -#lxILF^~kZE0-8-PZ64SpDP;vyZu@4<9~cf2&<`lk7vt5qX}#w(-1r!g16@%gdz~i5QK4J64kSsP;)x -Y@&KKZj-Egqm`o_;~af%6lkz-<uZhGeF4 -lCHuab91+uOYbGUE01@!m8^$5T&AYu0@h8plO%X)6rIMwPPxSbcUKP!Udf6VSK2ly&p8Yc)S%qht2$q -=^sjx%vigXYS|YPm#JLSCfH`o~`q!q0yESJ?h+NfZ7P*xq(E?_uYQa9gZJ9pATmJ4Tei)JeDe5t~`7j@ay(FTUg0$hm!Szl932m1`P8*cR9@-Cyv -{jyPo^#^zkyfKi@G0*|%G~Sb%I&EsrP{i|;Ip^UZnWc!Bj9i`u_XO9KQH000080Q-4XQ?1hxsB!}U0N -e)v044wc0B~t=FJE?LZe(wAFJow7a%5$6FKuFDb7yjIb#QQUZ(?O~E^v93Rb7wTI23*7S6pdcK%x}rR -%&|vCHJ0teD687xx2glP=7EQPiZm^2Ge*z{mCr& -LBGM?)$VA%;WRU1EfkX!=#-PN^q_wUDH$!4c;Ut(lWdr=+)>IMPnuGuFdWP#1v|?k0*vR$5UFBJI3p!nDJbBiKRA<>Oq6L?REzgH{mVnNlB -5%T8g;k=7p#GNdm2$5vv08)otT%M2H@kd8SavwGmr-N9fRz(-f9&qty-(Z%X`_)IP7@=G`f@1B6EFEZ -0K0CfZ4kszGNdR)(33<7ob(;~M&d&wG}NHqd2&x&An(`qdjb*awmQev4vb9*Ky~JP -|JVb$FKnRck|aDd$I9ChoF+|4s2%MFj^PD_81xl;l2)7w%dnZ!1i0>qfgFy%@#4uXe46nsNPuRorQ=TE--;dnkB#4!zL5%nnSMe}j6nDinF7g0Ei2Q4_x`B7 -po<}H!mB+fv&)ld>AhY4x-qd&o3iw0#%8}`CsbHZ`dQexOPdHKIQlv=HK^P=P==sk&yl*X2OH3EAFd- -lp*u$=te@69HC>;S~nvxEgP%7-EY^-yYUn$*|UdFVqAwe4=F(|*_a(C!c|VnlxT&|v!Y4{xNqS}ij;N -v*?$vo^ZObE8*~02cbw7a$XJH}aWqmpPOaF8fr4F6P5e6^Q*QY!woJAIB94FOr`ZD-IUH_pkz5ueu*P --70F-_pwW^XLH5%BdUOs{PSO*FGdUzs(@Bv{jrMaBXYTzd%aq0{&)b@YKdePcY8mG&$O#w%ZZy0w-z> -RM$4tZ{kdG$EtgpnhghwbnUFZjU#42|1JXaXz};HEAa9bdYqZ{I&I!)m8z&mzTl*m_eW8Ee68<^aS#L -BenUjl`nKSP$Q|E@=C_CD8nL0D)GIjFg2H(+%`f1vKt8*Lt8#$!^15ir?1QY-O00;p4c~(c!Jc4cm4Z*nhVXkl_>WppoXVq$t8}_k(L@Lt4F(?t0=mkpp{v=J(w7?|8uc8HCaA?fyZ?m4y@f+agN19S%JvwZUKk7iP-z256tNGOvr-Vz0M -9?mkq_fAsh0L2lsp`YjWg!QQ%DGksrJdKl}kHjGbD+U+lthuLI|nX$(pjvB+l^i|EGplKMl_xXvW7f` -w?MS-?39{aN$?1^dVr{+!vC#{xceLYX}@M8qs_`Y%5K{;fZCW($!t_oh%CQR;{Xw_)0bU(PPsfQL|0_ -Jw;qfZANlC$3`ydEnUG3pukwb|=!pn^j94i-L@jg0N#BvX5K$BjXO>;@I~*2qA?v>PC9BB*z-_1NPpE -MET)t-U;FdDD;B4a5jRJStpxdRnsYRyg1t%XnXhrGK7jgKsRC&&gLPX&D|R00Q<7<9a -PFdQD7jemTEeW9fRh4UwKa>j2Qr3Q1(p{rpx}_fX@7Y1Com2UPX;IBAENZfC*!mJXvB`shwOk|91O -=NN0)1pgUzZ);)*QY;y;C6tmzGxm -#>?4qe~7j_V7QRfB$H&Ww3}RAUc;1APa2U*YURF01ofgHCVPYpiX+PL}tV3#VWN^kZiRSs%N;AJo{PA -4X!=(*xk@4a3kiqp2H=U5+3{^~3sSpGyuR)k9*U+B#mOJMKwho_|Z3^~GnY$r!H@XPdv$)_ -JYJNlT8(lRn_zg9`Dfmq_zbW`l$#2xMAAz?7ztvUag5Of}+k)R#^V@>oR`WZ8-%;~Bg5Oc|yMo`P__F -?8!S70btCsx;+!TDXtHuT2RP%d+-&6B@g5OK|&CEyOmf%~G-=uKCw`NLwBKQ+Ee_W9bpv6ag -hHg-a3&ICTW6RFa_AI++jaUo3`9^7Tb6FOnqm -XP-S%`D(OZyc*BasXD8{US$Y{8$`}fer>EYLTwP2Hj6N41&e6tjNF2pVC>+O1vS>ROE0N;?3YN&9X2x7LNsswR?|>p9&*wv*z=NSk$Zo!}Tsu9sH -Dxqp*VcUG(UV?6mdn(Vfu5=NdT|$6ZRlE~ySf01W}%U~gJ$d$^wqVZHV8{`En^TmCHz5pb(Zr7R8`jc -18E9Y2I1Q%Qk5=Rb!!m&djapVvvg~1Mi#F0awaO@C -B961ENv_qhuraZqxAi`l!=n&MwFf|`L1PaFvA&ZKjSl%MQh_LnZlz6Cl+J`4wJWqL^ry{$G5=%_PvNh -M;oUWRDtTPvT#0EZGk7c1#sbQDt; -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 -ZE2{&(+i($A~KgI+HpgV -b4M_ZxP>RPx?;U_XG~YWpCg2xaKn%aF-kI}miq>rHv-1(hsVC{jC=pG5$Rt(CxN$G0oZ^)sZIuy0r+c|s|@B7A%K{dXni0*GXzg$zwW&;_&i&l=8U^8c7A>(rXY#wuF^T3o??=YDs<}QW_q@S5 -nLe~rj=%VO*i(~F@CT{)*G6nwBo2}FLk^9j1D1t)DUF85wF&(L(NM-Gd-EuG#^b!8JmhznMD4oIi|54 -#glPPi8G)-& -%}GIem&9CvQTg5;7uYdtZgxR{O*EvWxef6Pjscdt2z*+2eZw&)si1^IiLZEq}S?G3XiRJls~UX;$imc -TEY`%xS$iX<5_3t=;Vn?A!C4urvDh?b}td5WHb289zPT4Es)}J#mJkTYH459~z;J#bd5ZudaxgfuxJe -X5DN72h!qC@%2QkSv7DT0RT1-@CooIO`F16L7m}XG8*>F#G!$$Ouk_e@q{!Zc{);Ww>uO6KwI(dD+36 -}hBjx)AY7S1W9yQcmCCFgCMTdojFDhma4e!&_HcmpQ;L23f-B18?PK3M!rB}%Y#aN8NWCO%e=l)PYI; -7(=2HOHzhcrkP#~$tE{1mbMvLn6!dyQ#1G`;f8tj>K-}od>GJ~$@0i^*fvE3@AGyB+5>vee5Xu -xsHXl8kG)wp^3O>)1HsnfvOitEPZkS%oct&iR7Cbuo=D{uUOM7{{n6n$k^8;qijpZL{#nT(Xx0)-z_ZY2 -v(UvjOxM@BEz!EQCw6Dj8{AzTW>6t)zozjn-y4cP+$)h(O*0kkPHX37X|~6eK6qr=IFfKeE*9wIKAT0 -0TJ@(eug*_l-N01*M++Bl90W|Ax{l={P~VeVv}YI5)cbkocZX;sPH$4+!x)+KL|SS`_No8L6EkM;E?^ -7S?6p)q(GS&dDV6pjjsAh5d%2XthrDV%@1aw@y4C7bj{oM9565FLoH!5muGETS%yzOnNacgkQzh$dt5 -AgbdTJg_P2j54T2>P~MG0nLd0aev%6`90<)es}w0WrURkCRXcA5(Z -*OO+b?-gCcveGiwHhZi-SXSYiox8-zwhnk&U>xGWUUS3?%V9Ked#6%=4rk0F22~Dr^mxyCV!$d5&(l` -mHOlfW?;)aJtDheT5yI3Q*i7*0Uk9@&nIp>Pxn9Koa$alHXA)9k-&1PvLLS}?aG(Oc_s_X;6)R!vX4; -5#IM>JYxi2J6PvRLyKOic|{T32*?b5jSzLGcnaU)S@TV*Mu~^!3`5AAPp&~&@X)H#Hsg=#{SaD8Tj?EIDKhR316taXAkA%+cBU6T0 -68$L^^k*^>xvh?N-+P&d`8r^q#Yj|FxJ9vM9EJ}e>6gWo`vw184jHnb#%nFVJ;MMV3ZD=%_ZpR(GTkoITLD -%tpdg^)9pnk&}xUFHQ;Zc9+^?SZkLvcA@r`RU*P?A4WJO<=UB9mwmCSsk3{*Ll$I2cDXWebinha6oM5 -yK6cug_1vp2P&W0gqAS8$mG0^gV#!C068KPm5@SF=#Zsy&HKjII -^tA!qo7;N*=DPmvrcN~Uv2$#Z(|tVg@wvRX7%MbQt$14|N~ZIIHqZ-rVkE@68YDw|Dhj^{X -x06O`biWcSnTU%|8II4_0CZNzWeCtF~7%q6rft-GM@G(G37P`jip#WeYC25y7)uv^Z+<(Dbgwtce;u$ -=>yYQ1l}`FanRy`k@KtdiaR{(Wcnt7>c&atH3E=iUe2nQ*U-R_on!JGjGVYL+fVNp(9Nc;Q4T7X#N1z -Te&igLcE+tGP+l2cGQ1SLjDD77elR?@|kIiN$gPmJkQK;zwet9BqpeTKmX)VzC{ -0B@Z?ZV!-ttdq1;jfwwr-aLm@}+Scj%zKDzRGA^b1&=_vUPDxBzz(PLcTWI+b<_}FQG%1VYd|)qx#s| -q{Bb;yjkEt1`^mt-Q+cjLuAj?6qp(_U~d%7!!g5R%7&wF${H1=TNcs{iq4};ycj5e8I4BNa)WqH*?)6 -(*fCAbnM;*4ojXs;@uWw_9bY(g8ZK_bT(_8Aa#BN5qAjPimF@EYdt%n}Hhf^P0X6|?|6XAb@%l45XY- -k^Tf%+`{xylJhO^yTD++|0^iQc8 -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 -6N=v5b~@6aWAK -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 -WW=( -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 -}rY5QEmP=zdSQ{gIC9#CR-ZbEmBNAa9Jk*kqve -?N|MXM3lR#1y0hiDr5`au6elS}GWX)3OTDEOltAt6i&Ej1OWCI)oRUpZ5ww#bCp@K#V1c|a3gDNArU| -cc2!AP136GD8;I89~OS_0gVM5+Ad`Wkq5&%Uu7Vn(}}*j($?wbl(mMl^>|i<%NFACnD;{g>14f3>ao( -tlq4ZCj&YYE~5YSuw0lEuqRnU7c*{Rr|&wcnMHD!HE|8gGeO`41e2OyP!%?p<>vmmiesnbXwfoduQX9 -!SNfjmswMwB9xH;;4N$y40=szx6f%m*r(i-arjl{M}4zVN+q5Im(17gZ)H#aK%`2p)(u0(nF_;}gmhi -T^>sy50z~efi~=594ERd`DHN$vf<5i@G4B%=B}7Afw|xSEXIP0msbaX=gHDV$QpO`VQR| -$@nF8n*PUNUG(Tf`Jlk5e}(J5rlRAP1+{}n#LQ~P%ZnChst&II8ojzy4|iSygn7h4maDYL7Kg$gCLbj -uFyvyJ-tkET07lWjohx{O}OaN8Gq<;9-PnInx_yInuPosgNhJi&)g&q4$Dq_7KPaN~-heI$dxoobnb(##}I!%snohD1TPTdnrbx42kfLLf^sbwPL$ -^Egh#XZdCi~HfUjP$BA2!~Vscf_$Q{p>OD@j=wKveNYC=mqXk7A5cpJ1QY-O -00;p4c~(=)#1YeR3jhEWDF6T?0001RX>c!Jc4cm4Z*nhVZ)|UJVQpbAVQzD2bZ>WQZZk42aCxm-ZFAe -W5&rI9fu28zN+tQN(`lV*Qd!o;P8{dPcE(L@mjhFf#4|@F71F2L --PDNi;<{Wgqm;C6r)Qy%eDT -`8?X;potc!_7R32KY;p(3eceot+(vIVP7p2GbWtj)a25T&kzXI|Tj;Ni(q52mK(TLV&3$qwCw0CJ9SD -{`fNsWM8ZGdOh`vF9o4QC{f~xELagL;C{|C*&h%Z$S_{wBT8fnaY|^)Vo8#ezih~Yr-Vk4f}&EU;bSH -l=mZRgrywec@_EhViqA>!ISnQA`aSS>6~aj1&YVSTNhO;T$qCO=g*D~zHHU_Y -$7RM6Q?}*oIJtd6cW5btB)mQe!J$6)C+IWX2@Pb=>W@BCe+kR_9CP3k*w)542;Q;iZI`}ZZK; -@n1yd4b?vSOu^hZn5v*#MN&$Uwvavm$twU(__7U{j(eUV{gIP_Xk(O0b1wbgk+l@kSb#>Z~D;DMNfqQ -yCeVv|7u0Hu^pRaFbr#)Ddz?~x&e+yF{7f?prAUael@Y0A?wkQwbq8i`^!1~-jjrC~i2`@cih0RfwFaVCO@3pW3~G -f{K0H0Vf!eur-=lD1}GrMuy`SfZLM#`!vl*cuiDR)n0;EjcJ!A0RaX#GF41tZY!Xz{W5klgsOK60B$n -RfTD$7YtTD@Vn530tpq-$)q$a4K+w^45Z0+RDr~aCAakuit8ea17s5tKy?!3+JQ#+Q}}l -^gIK2w=_i3+!Ry`c?;pIo-#l36Ls8KTI5xF)VkaN -7BXMyodjq~|>+BwdHclWP*`T%3xQwP2H+a;eNcc==$^+rhWb;{Rkh>puWot1EmOK_m4z+BS|Sb_;A$1 -1T)bbnK?X3BYpbMKGasuc9o$FhY}I(#rT(M$VUExqZr`s#a|=5|907o$8V8tp{kpl$p)^r0ZBonGLBiR2z_ZR^%l|)s{9=Mhzs=+Kqb3i; -cT8_wcvjo%0jnVIqAca95NI0NiO(=VUK&6L?(aTbcxmA~!hSAHL&zUINADx2&)}n7jRa!Vv!-ej2zh1 -vNR~!bZa7Hno@7$`T6~LLIS0KEVBcPsJNXt;BJZ4lHlFSU~kvy*s3&dgG)6N`yjWijjDZ<3<5V+B9LgMJeQ0n4(QLXtF|GvKT51zI`l -O990*PfnpvH4GWPZq7t@zYoM$wj&P616P!@s9y_-*3ZdbO!lweNpz0MxyCDK-2CQ|6za}x=>|&;1C36 -za4Yu1k+Z@CqR4g}pS1AZMq168?b`6NO -ZWZNY!XO`_tt8?DA&P4qybjFs5H89U;KCUD~&2=QkY=Km@;b -PaZ6QntGSU1tzsh3V-&GSJGynSoc*lNRm!0c)ul*N|;V8C9x%%&Yn&6f{wqTS9-O1H{Q740r=Qu;M+ssMCylkvX9O*Q9Rxk+ -i)xv2us%}olU!c7%`4sKG~Rc@+Bu#KC{9!?SBR4fC-ZELyxHBD?~&#QMm0Odn-YtM?!8L<_rA -8fjtrP8r-tI#6K#L8*mt&Mq8-Y`Jk9;>fc*74$LcemaEh)urk_r2b_Hz3~rpzpc~2<{#jg4ez+el;MK -w%>K39`MF_bhkOTsG|0W%Mj0lP}F=ItDMW3Gd;E6qwz+aAUNCJj)PUZZG%0Zu}dAW+LKhh(JanzibK? -@u|bv!^~q|#IHpE>$E6AKxdGj_BDhc(x3L~abv?DOHhxKn@yZKRh -{atRGx%eaT4hCHm-|WmQW>=godtu)h7sep)`+TUx|HH?1uP#)w9SOcBp?KZ}nw(5eUq!-bRco?->n?{ -P_Zl;*IhpXgK-Co$Xw)f!-58Kurg}=+><=Eu`RY_{T+_HySP!jwppvWK?KjIA=tAz`xLMGg?TvL`lOM -#6%jdH7$+O1PKZ)D7wdws(39W6#SV{%+MUb*QE5R-NO!=BIwIM!s4iSYh`yU5Du2FAxZFNbw2-U~TA) -$|gN10@eCSbFqReY;rZ22xj^jc{#BxMnm(4LeGiF4AaCe1FHfKrwW$O-(;`ExuVQ@DcMhdG5sa|$=uF -qgDm1_Zy||Js{?9k9U3FzydX@5J1xn;N7bn7v$}b8y?yRGHi1s*~A8I~AFd5bz8N#t(0QP&|L|3c1#c32HX -#>_Q10;{O(im(aR~GSdHzB!nq|O%~JNE_jSBtRvHgbx>gC2y@5}Y-w|4E^v8`R85c5Fc7`xSB%udrmGOa773|RB%lH8Z~#=f&7?Irb~Sb=`S;k)m -y_+%ja1d18IRwadE*(Z)UhVMKN$F|Br^vl;S(bxu!ftLuNEo1zyAP>7c0RkjUf0ArN#s_B7C`Bt?%yF -P;h~1#LJ^11I*xGZo1u0NxL_KZ##>wbrcn(N=TX1+^7?miyLUY@2u%TeRHNLij)Pm(I!`wfeh}H#NLE -NohdpIJ03BgL*EB{d=LdOu)E73 -5;74Mg|2*An0|IZmb8Zq_o+LoK3S^XMfaIVu*LG^lj|vo|lmGpd~!S1v#?Ehko>NT~6yGWMw?!HLiLB -K||b``2?^d^gqCXx}OBx#5_858L*9e2H>!T!GMJt49o+%#%Ptl=h`5}{Rqq_A>F(>akaydqaTl9Rzr1 -!vwFUHLG(01Tkt0nG_@P4GkQC`bZNURnPht;4|b=7B381EbHvwnPILm-*S}A0)lqPCrY`R>R1Y9(jw1 -KFf=gSfz}1zcf6yisnh5rCk;Vtkq#qJL0J0|XQR000O8`*~JV2pbF$Pz3-092Ecn9RL6TaA|NaU -v_0~WN&gWV{dG4a$#*@FL!BfGcqo4dCgZ_Z=*&Oe)q2!CE6Oc1m*G;H4lm7bX|Gv$as~ksujX;2(*~7 -W`;Q4U*9tfhQVC2Nn2@AWQ8;5_8rb`j>(8_b}W911o?xeo`}4ZeDat`U`NhR&n|3o}cRDZ|7f@-J_eg`DDm7}1o|1eWk3N)5=8ceb4WfAqb4gKsnS+qv%g -xk*T)8Pd0r2rCU|hrsX_~I7t21b_L6Q6NmB^LA?=0=LDz@Wg={k2KpNjd!_qvf!!cEsyT5~QMDWSOS_ -M@KShV)vz*0yq&Taj;DPVi-Nfn-+&1E9p%9K-S@vTLeVPzwqUVxvjXSdUFqpBC`DyX7JF4$fXRZ$w?|A~k$n`hOH$z36(;)mi%$y%AsD -!a8srDDDS#7bi&}H_yZ-6C<2Eo-cr1w_xkxepTHY5f&4$w@MEpp{IMDy`JKIPQEc(eQDm!{F<_VpX~p -4#NJ*v%NQ5VhyHGmv$PAo5|Zit=)qYoItpEB==C(8{Mrg-& -MSXQ^G#kjBhPU$JFtWCiFDIX#g#Sd7Is!mBO0o;ec<;7pP2y41N%;AI28kdnrWwKHpScx&dw*jX= -TC8q)lf!m0s2LFVdSvq!nH}yy7DGC4^5vQ7@2(`u%BA<(Nv-N+M2pG1WB7U41RzCMO}kW -Vc*hWxPsZ{7hITEkIh^jVdUWkb;`zBF6ATNVoeX_NU>FV`8xEk#70z#q(X{6ovZi8LJK$&}}^@yt<60 -4DZ9oECOlNA204+FNQ%}^{-l=oCTq?84! -rY!P{FN47!iceyqjd!GoS6?fu?aq)VKXI1q%dsb{tr=1R{-HE!o4|6%GlcwYu0V}luwWeg15ir?1QY- -O00;p4c~(=RDAwlg1pojh82|tu0001RX>c!Jc4cm4Z*nhVZ)|UJVQpbAcWG{PWpZsUaCz+*+in_1^qs -Fb5>gBa-h!Q`Q6(bTm^e-ysK8O;C$yKrY_iOt+1X$!tKZ%;mtAI-1w#^7eJC&9Gjs0uaUFVWvEuRFOu -&0aeKGSXyz{w8$O=x{ol_6a#}{yV+)Ml{C6L4+p($xWN(fwE_4dk6l;-@KmifNA}j5k>Dg@mH{dkye+etp*a1OIlRZQzf6P;f`Vm!p1vpx_e)_ioPy`u)t}LeI15l<+G$WetrmzPT8FylHyNGa+~!WP+qF%an+Tk9TRqk< -hFbJee|1P`W;pz!p4da)Bp-EqJL6X@RJq0<#WR6c~>P2f={Mv&0uN)ureApCP8w2@zYSEm#r=80TyWV -n*`NDsG;|Zp`r77i$)w%={F58aXr&F`!I9n6Mx~>BJ9bz>qU;j;L=RIn4Xkg1s1%chTIA536H_aRI1w4AR`lS76(V>JomY6$uz~> -(wwUHxhMPAn_~s`W*~nLuF6t7VVR5_HCQBOn{>>YB*{s0Ib0p`|$XkJD{Ph5(6RR*Sl{ryLCy7y+QDl -wT#k&JWH+0{xFeFx}E3l!kyF1IMB3RNzMj`pBbZLP??7sCA+2PlUssl^YSkudlk#y?|`EzR3F5v1QIX -89am+NgAi&>tbW2aD23#G3s-nRImTMkiyO3-Ob(2MDrgf5^=)FY)BU;&RQlf?BF<>p(n>1s4p861)JEva-@^!nSZ##9 -GDfUB__~AOJ{T$G?a6L4FC@E+H|hGLQGsC&i#_577nfpJXlPnvVAGv23zo(rX4LpVqgv+X_xmg>hS-a -iy^N=f|7mEgGC3!q+Iv6gnUBK|SA!V$WP$@68(8}|@KX}m6IzbcvVAUpAomMgz*6XFYHve(?=!r}w_f -M`);%|)HNP%pixOJ)SPt%!IE6DSa|bu2qq$yLGR_GV$x81gF}+h8QBIEaCU+pBM+RkJip49jvtn1|W1 -)T1LDq)<%USA1u}ITgMJrQ{>;33UmI~we`xdNr`TJ>X*y&SK00pwvmqZhbs$u7zw7||$xNjcntqmFZk -#We{iG*WuOj1>~#)osQkm>q -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 -$;fv|nU&Mb1`1Cz|8dJV+f_Yq9{~x -g2nEl|2t*ji%#i79-AV2ZVuj$yWSC`%^Hq{cLSfq~&0%5QrBd1qpfT=YL9}adg+jUTQ5{#=vX#herY8 -(-IWSoyqmI{Sb3!k5k6oASfF?dJaoKAWGQ|C1p1>-*qw>OJj{w3!@9@gwugZ=V -}ztS`Tl|yX^QNN5TQ~o<32snaPnhMCwHkAA{eFwfP#*D-g7&QSW^pF^#F$V_?Zi|q5qaDXmB}!n;i+Lm -$S|>W7W?gQ=3~`~TBqWeB;j|XaQ2jU}=K-lLxeXtE(L$DUWC!rBtfyW7%xy0|A^6>KT0q_;Q6W>1O7d -&5E>}P@bSaq@7spqxp*(@x7Ts{J`$_m2XNHhc^!?L4 -u3GzBw&eM;E2;p2qwp0_TYjZ#vUyZTO;ZKN?+{B(rIJ&koxE!t1m!P#M#Rw -m8rY2tGJ8CGtY+KAtH*ZwM?Gy~5NvGF4GsJ*cfGYNU*u;4W%fMCXRy_tRSJHBVV+srq%9s -pbLR$)?sWBjoZeTtRu1=q?)i`~3^yVf8e|v -Nohn!53n*-EAoa%xK{%(&4ds(5=B;*IX&|A$;g7Jk_w;VPf!@XzO>PKdO;1gQ5}v8%K)EJm>v3h^Ve( -T_3C9Y1vRCU?#<&lB9xB5em=8y1$*jsJR@J&7b-vAMrMbdc@zX878w$%?ULS>0qR{Bv^f5KzYym1+WIHG|D+Iq#>1J26Gq4KXja^@HG@KDAN -VYLrM#pRdBoFT84WZBfc%NcR0d!td3+Nf`~7(&9xZdk65brfBy5oC_I7q6T=e;&&oEpi464Ei+q|##S -QA!;P~wM$@$5}$W5$BD88n&!;>Z8mEsiwVNLv^{mIm+1HEQ*R6~4HRYh#7uW0V1O6^E#S;)c80- -mcvYlzWNFoOcX|txPKs3=ladqivTts|H}{*AP$m)r4wfCxX3Dba~=tNUDC?pvt(N-1dYnmdUG~Fp9?* -eWW{9N|I0apdN?^y)GpFA)YOXz@uE*nyZkuq2fD{ZA#NlM*j)(<1wuc3zXTfgz^ -~^A9#BIQ3ZP*uOgaX8QKx#pj&LS}jeJRVkMao@D8?N}av*0b_7^G`aerei&RH{BR#$eKHytvM`0kKE* -&O%4vQjVS62?WFB2l13@Y;x!c;!HHSAgZ-LJ(Albc~&=EL9`YK9sCz8#i8>|r0y_QnNz -Vyl)yA>?3kvtJ?2dLAY2cHSxbN~kIG%cGd#mq==K^O>jE5EZbxGwRUww?^5F54PoE;iXV9OL@?Yz=U8(Ih}^K -sr0(deZZ^)IuK(@lz;~>bOAO2ZCyI6hnnETa^>U`!gN|z)4C~_fun~yGsHi+TGR}kU|!Db9H5;xxGp< -!qXFZ3IJD#USym)dmY=}UUA_41i|10?)B>y~I`b!)EaOaRX|gD`^a1UZM~qtc?7_aSAJs>cSkIwAELX -5vo}C|`o|5m46`ZoFgGTlM!>VJ&0EIxe`UA0?%6NT3-;_VpIhbb<`7NL8%u}LL8{&2S_innu -?}<}>X%{2RdWG68f!@16e?>2Ll0tH_lzHQn -&q_j=jNHb=!K%0|%KLWJa9MfB_UIXScSJV8(;{*t-v2UCVxnV)jc&T1i1@dQ9A&dsiF!-vh-I9^`UkB -v`>Mo#6;tGZyD_FE=1>!HS(Vp$x{&AThu(hutl0q3U=M?KIpp64>7joIB{kY7Ff^HPpZ}|cO+VyFV&W -Jr(}$-PrWdY)DK`OZn9+-B<&+l6Vd($Mr$_eYpKDKT{H4O7uKq(4+Mp3s8`SN*=%F47KOx{ZUokr~!m;dzu5-$cug -NI-O8eZw2X0Bxz)pk?8xBUV`&cOq12yaGbpp>bZ)ncb=V6XybZNGw;E+Xr<{PC3-cFbqR?4`4X7wFmk -`6cF+eliQT?z<`YoZrj`wgC%7bwa8oGFox`m=PCq>cq_^iPt`C1CVKTZZ_UoKnpUf#ka<|zL+`YrQa? -Te?c0`9e^favvV_}=DQiPpKSA$?HFlxX94#7Kx2iSG{OaETMf>qEgnxLiuhfF6S;1Nqnz^?FN$t}RFmcuc)g&zNoIU~W_tkK4y -vl7i+aHS#xTfZ5r3f8xHrrQa+BcX1qjIlJJ76E7suGCpDfO-V#1}0~g8Dh~F*jv08Mqtc^RTW%kK)Zr ->S$c1zD=;HR*B0fpSF7Msh6b#4#aSKBJcZaiYB&o8UgL6goo6v+c48yzq6&+r71#7eqUuxS}QWuoS;k6JZ -0NN^YO=4hRJan@RZqYZAp=(o4p-o070s(U^4cBYb)8tx6^4Yb|a{hLR&xt6Dq2OlUf -qG8HS~P?_{+W@;V$Gn;!G?Ps^J9Lex(Y;vG6!`N&~%XFm+ybMYTNCLD8G-^Fg%D&8r4*7he)}^kqH7# -A5@dREtkKfYj*KTk2*kLCE%gZ4M5Q5N|JD4mz+jdiTBzHeU}MAxr8yUtmpf<7^I>eN^2(@54n0+dXh- -=Zji!WikhY$1-e$igOe;SYH5iCuyLzbORgMPU6X2}GlCK3hG&?xGCJGD#iCG6)Uld^TjuZJ!o+EfKK@ -3Lq3Z845Td+|bwD{)%QWP}qH%tx88{lr30CO}Cna^)JgVpFhz3?5moPb9z|fAHZ7V>+2E8M@&}211y- -Ap5y-cwVl!&lwT2k483}!vwh$jN{b^nv=-Hsf{i%9( -9gt^23UYaD-#Pt^!tHQbr9!xXc)7+R7m>JQ)e|iR`yS8P+7Gvl^(77R! -;Z|HO*FJg>`A<=CQx5sG6_!CSSM}Tp3v5yt(uYO<%Lohl>n-!!FheW3(VZYknj-s76X@j>FWS0m@8l^{Um2tn16IltZs$S-r) -P@=_1Z<~)*Zu_Z0@&fj(H!#C(db6zAQ7mE@e%R>|{Tp>QD$6g0u0i!Z+!nyi3ik*of5p0>mJz3|_&)5 -pOKy&__}WlWu_k7s_GhrcEqsO)3<WHp6ha9(F -e3Y+(JH9ZtImDMS2ex*4M9TnHU{!AyN@^Vh#8(l#%>CV@^$eIlFfkd8g8ATUmo=NMh=n33Jz7w_M{SG -R@XPE-ie3BDr7;c>Zm1ny(DOzTH~`0Ue9zj*pc1l1R#y?ybODo6%j#TYmk$;1c67S)P^XrYT-e?NwQ{ -(%2{I;NUe)F#XIa*cN+zviff8^c2|uyj_dUS$pI>#T^&YKi$4JXr*u0qwa*)Jlf%VUHX)bFPeGQt5Oh -<&5>Wt?6O{m8=$v^>jCYJ=jd6YhXhU9Vl;h*Ck&OKd;i8RUTDsasr_+*hb)8cU7hX_8$u$QsopPk%J52@eG!k)8`|0Qg&s-GXilupKU)v?`M{%Bms|~9k>05KQ3*ZNIb -#9FZHLzxhHVn`F1Q`oqT^W7JoVUx2te- -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 -?~_x-&tjb1-oIm4a)`Ecg;D1k(U(96Ecd1xpH* ->JFQPHUID{2iFzsZYU=%9~Dw*CwKLOYb0ZO>xPLYH$2yL}(&c@<45v9JZn!6V^mVY*fm%Nn2 -Cyi~{4SLUxV0^&_~YLjZmpwee0e1;nOfeyqw@qZAwxGZtC-iulCDw=Hx4JS39jY4;fA48xj=cn3BS)C -P21xg&YGyZ5jmqN8HSfOCEX1k^VRS;h+jz&7I@!dh%bA`_TFY2F9uUssY|5fmNC6z-rH%%Vq5jM%WOX -i7j=6Ywy_>=Iz*`3j74)T_(dJ+YeX_8xShwmrGt#6wXPz1g8y;jw3V^+P}gIl3xfd!?oR8$Q5fyW9#17GX{~f -fh^<8>yY6pb&img}NW1JQCZgM~f4veiX~H0UHBAP>y3YpZUfu2jXz}MBv|QAbhp(ege$sE0lmzIe0gvY`0r6b^rGx=Uv8tPLu_^8 -a9innGsZrh_@1ek3b`S8P$sB8RXA!*}z2m&bj4MXM7#|J;xAt8wApMHT*duGPVxscIV;cr)@Y)$D9Qx -S0FOKO|>F9g&?tQy$Z=Z?e_p{2Kmf#XA-=v3ntBxCCP^zjQ?2?1q(N#~1Fb@2$e#L7maTYxXQ|VoIx- -Ohr_7{fZUo8cT2OgUcyrSaEYe)5z6wziA`?ksQtXa2LpvyPhsKNoZqgWw8X0#(AUgfhzhc|s%&l3Fq3 -s6e~1QY-O00;p4c~(<~Kx6#)F8}~@#{d8y0001RX>c!Jc4cm4Z*nhWX>)XJX<{#AVRT_)VRL0JaCz;0 -YjYdPk>Gd!inbJr251nPw;jBrZ)u4$t605G(s*WrqX3&gf$U+U8`Iq&3H$cHUq0&nXn>lr&yHJ!JtEL -mSyfqC`Krw9$Jx>0Q8sTDWqoy=ZTjWWAMuxi#|MwIXU%%smRHw(HhMPBzWD6(FOJ|pf5<*ui|p;Tzi# -U6eY5QEidJObHk*1;^kq{|q33%cvX?KOoxOg4Htp~Gtf&{+vp26lynFHI??1eG_a1+Qb_b6hJ$ifgHv -94q-(=-#U5S;bdjKnG1cjjB&i -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 -%)uD$wcXA9RW&3pT3#iymG`6sa?)B@QAUj -u({pfM>2u51BejGhVUOmv^g2}oVlh-o-Zg{8HYx+R>dvgfOeOF?%b48Q6HUpMj+yI*(sL3Nl5=lpAED -7%=?k+SP>fS&IEl;S`awi=`wQXS5?imDZp!f0ypZb(XSz+^cmbX#igpmrqVB47@nAfXCR8Z_T#;#UF+ -^Ll&s_UOy6z6QRrm)<$3NZIrO1I(|gwU#U#x&!hZFmyMou<2-wkja*y -FwI8Jp`WM_x3T%m-vH~yT{Ro>;Ue-VYr988ZwQWz@3H%?1-qVc|Owbr#J~s_DK#!HuZNfF2jsg!0@aG -ju!w+q7TcRf)-TVUd^e?~4?h5;^7MahY5nY}49=z+~RxmWFPWJL1G@DMR0ncnG&i98u`x0`gi -MNinsb-znBCBCc7DNs4xtcjV7^12Bf3Jc&4ED?}Bx$vcoCK|@6sM#|@TqL+?<1j6;n4x306rIE#iixW -RFBNd0ArUUIM}|F%F$FS{Ao0{-C`H?s^G#K>8h9Yb^b-z*P~UJzI#IOqYklE`5fFZ7mNg(tO-KZbY&8 -$^4v-{d3MGe=QmtcadpIL)6(^D5Z$78fOTzoQ@F*Z7YR^@p@Ip!Jmno?iGz6kZpE!BiSm%3ygB0;{OR -#s?ZDx?$Xv9z|h7N;5GHxC?9407`$c1FZ47Kde!^^CDMH*)%wYn>|r1-5iUC&~PF17WA34A4@$U7op` -6phjOFtye0Ms9t!e-8~!)mhh$K|F3OQkP?#V-U#NQrvB)v;sty50*4U_;L>`M7bMo4UE9V6RKT#9A=G -BLtXP07vLMB`~=2JiV+~_683WlRWljhZ>yd>}WRm7?9jFl^|cLqZ=yh;!(c1&@q)@it5WU -W!Tf6J*}H4ng|$n}SLZjymGHEO%Pnf*tvyfNNmSBZEdyhq3@XM`y}C&Jmzw0m0wsN3qM$G%(s59hTS; -9PUce-}IZ!7%mrHZxZK@rJJh84I)t_zyX<(~|Q^%`FEy(({LinT^j?W)6`NDZRf>nsRD^U_;6xZFBU1 -K@zsK3&yt?_(Af!bBgoE&YQW=_u*<(23HKLI@w_kCM*@)7_37~>pEjqRyD<9wrQ&_h%~V#-*HSUJ^D8lu%t;&3ReI!&0b4WkXYgNLM5-SWe)`4}NW*Gknz;tAnvDepCvF*&A3o3= -$a;s4%fIYhg29k(!Qa*3nF4JT_aVU12T8h%?hnEluO0|Nvt&u)<05{eI%{k#Jb -m&rCaI^*Vc(9Sd38L3pc6@VWU -RLhmSwxVsWj#;~P|9h2mD@ByF~vS -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 -J#asq~SwE==XY2a!n`atvN{zk41|=Q2 -7p-&CXfB6?b*T#$GXUKJlDBx^f@QaMsS5r60~Ji^NJ?s7q^C(NPw=q7tfjVD@6jDU$n4ZUot$`^ncNz -c7i2~>h$Lske~7JdMMS7fk4bw5CS9HyD=)2(eYE#dSx3lHX1XPv=0Tb1<_N2-1hgT*_))1nRuKw}TiM -uUKQ1mt#^#dxo3xv#qy!qgp|I`TY7!xc=wx}PNrseeydYbgp1#YuF>yH(LlNt(z^Js7y4}v&O+8rNmV -`{_V-A6;OFnbW+hN54TsC?wwf}dToz}*LBrgSa^Pth46xRL0YZlxrTt_cz3%BwfKwb1;>1%aU#9nFm< -&I^d%`8$;(&|>hb+<^=*mW5xuwXCKjLd`$`GYlh{A~)G0V0Bmp7DbubZu_9bC4FADpng8YoT>4kqwUu_0rE_r3U4brmczAWH5#vpqdT6Om=i?$E~+>EOP_ -@Hp${|kF!NX*5i89v&o5voe!dRQXGBxhi{_2DVTfn~0cI^ -Mte!F#=U%RtaBtrN4%eD0CMQeA`q%0Ce@*|XwLV_N+aE^a`;FA7RMLS*N8^nOVRGH9G2^YYGHsybsyO -G6g85`Q_cDhpL5HT-4HIfVUh&ZPq2~SPi0iI%JfPGf|rC3j6D+*TQw9j*53lU>NH?j&Iqu1df!txopD -cF;!ujD)JXzY|yr)%+AJlrni%TZ*~+n}0b(IpuB7JZEGw6Gn?LZ;hHtk(T@tZW7YtL$V@+B7>ufvYRl -(v_ur%H7m;yOiN{)ydLEgOe+e*kqSE;uP1HBgyBu=sYXth)`Lu0;9>nHd@rV3}n~fMg&3BtbY>1$;sP -d8G;^V5(1WgBINNzWjRyz4h@Kpt*Sm(%qen~evC^mgBI6+y1%UYM2VtUczD3H&JxwpymRsppI*u#7xT -lZQtzbY>ILo@hwV*DU8uxoWf7+9>@!rw#qe3X@(W5ha*lAF7>8+LCM5}24x^|Ti86LA@N6KzV^NlL^C -nhBCuAb&*@71tnO0}WBWaW5Cpi+$u9o<50#CTH%qIXKlkD>#tH8`!v2|uPd&Fz7-NBh2;x -dT9pX{FFQM``{ITqC;DYT!ffiEYIu?^jC4XmLCQ@H3)9>)%Ca|^`Wa>TbD90y5<_OAKQY=hmR(>s$6OHWN#&M9;{=Q~O;xTcg<*WTVDC-=1tlokYyRGNfx=o4cS -WS`l9-SdTZo0~iMMKj;%KxT&<9b0r}Cs96g-v+O>oK*?Gv#0aJ-M`AoSU(UJ25zHkBbb}i{g10 -7~$9p_0l^38Y`ylyH-sXNOC-@)nb_T+R+HkC*DPqi#O->`^?Y2ry^YbApgs0H6hqb7opE+x+^wyuxsNYi?KYS8ODK2Un!47ez3TnF -oW@=0MIP>R!OF}6`GaKSO)C>z(A2CH3!xmdf24QQzJoAp4ngw;v8(Y67ABCSI;9KJ_U;fmQC+jpEm8)Q_zt`i{A97s0U*rQ1zILJWf?+I{{H&5O3aNm2efQS-(%Z)$U -9ylZougWjY@!;flbi7xz9e;Yyo9d>1uQ3F_t23fm$4UX!A&;$8|fpNR{e-U48`MhF}YadGkXKfXJ{w| ->BKl$J!79ZD~X)`FyJ4YZl`MpjN32ZIm`2>_h5K2b4Ty5T@qgt@mPAOVGOy{5)*yct1FyOTPqTkEAZ$HkH`LGmN3tDRUr& -xL@PZs$mp(W~})bwyS=$6@T@4oV^x00T?~Mmtu)E@o^!3r1-Cyvd>@l%?YAqx|lV&L3(18trTHJ56D& -0c_$iMQ7cGQ5DV@ysEVOo<*J(z6tB!Fel-8H-x{qdr=lVub9@alf9*@KMISdAa -owu*$_|E*3Fp-lRO|%P*dv1wYFD5@sUVt@dzq(qrY~qzvGwb5lOoVmLpK)tV=OUa76`YjyccK#nt5eN -eS9>*fVj_22KkhzKZtj}$M-9JJsDGPg$<$YqBNWRKJ#-)TF;@FBFSCNFCo&t)QZOAZ%f(uue -&S=;T!n|*4zCHJncloA4eN#X3jDTME; -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 -!IaWuj+>;rH&{BSJf -YKh4JLTVisiHy$L1{aWOydy9d;DZ$JUcV6Kn#p1$BJq4JiV|vH$s}86GBq?)yEWz?aY1DDq|!8)*3l$ -dgnNWB!3z2xaC)-tweDi$dzWOy-c8-qQads-6p|xZ2M#v*L{zigh@2!*^ -h^we3T=a4)*ye)ww!VK!ui2w4wW*B6}4Fr*rij{Q++qJ(C59&ZX5My-sNo6PHVr -%9sC?HAq8`X8bz`P>KOgGq6n?uhR+Y(d_Ub1e}><-I1O65(_4sDgoB^AQ0TkHO9XJJ(M{4D0?<*F)sf`E#j3huXhXSChwZy|qB_Oyn;SnU8 -TrZM1&%^gn7|cs>vxdNe4`kfZveB1B^Z~PY_(ET)6!)D?_Jzt3GMD1fG^Pxsli>6eqicl;ay@!$z041 -9JP$Lxh|8X3fe~Ji=~M}(+inM~_)t{&b*>SXIWdR*YS-!UFvVp`nc31@z=5uyQZdi5{+w23ty}x -Jx|`9gnd!2B<2h>A7+Rqj3#Z_NkkRL{HHZtN~VZc;N)x1El$X}22bQr(7mTt#x1MH103PhGipp0e@uR -#Z06ER%7R!m=l8~C#+^%$&TW#Fgxj6w0^arY5L_c)#xA?3boRw*j=BX$sI?$i!Yh`qr-2pe=^%pk#IW -cBlSTXQcqCHoqBzB@v+%-hMs%=xRL}dg-W^0tsiJPxPqr9#)KxuNWtQRmeI6E#7q{Cb(%zne< -nt5YNOPWocv9e!a6#T<4DxKxe!mq1qbC9%kS;8WgEc~nuvn|7W$UU7_x~4apSO?CTUvVE{bNi0>b2|o -C|BwMjpC40><&i4{clbMG-Wy7p>|F9!}yWk=YF><{8OWq`?~sL3UZq$e3_@G@>#I+9w)-mcbl!!s&SL -$OyOZHS+dE`)}U|7v#7p1fpb$qsx8ZIa9`qLJ~qk7RdbN!afbtC__37Oy=VKhnd&O^27E*ij}r~?BBy -EFthY$qrgGHk>R^Yh(d!_{p}>vc!)WOfN<3uGJ+;e7pf@=t6naThh2xAhp1T(VlM=xP^rWdKwV2Uo)9 -#=(JUm(8C(QEaY>p%ON5kb@qjj?#_Y@YvuFs+G&kUER}9Wb6Jj<{xy|xiAf>0#@;zn3S#@wqz#A6Gsk -;geI8-&-fd`&GwIhrSKP`cnSp+4nkbjtHi%~Po)8+JBxZP&R$=|AGVBc%f4@QDhNCFb}L-EX}w&Un{9 -8)tiBt)cY@DWty3TBZa!%+>u8j>PIMOgP=n$iaVt(r|3Jg8j~7+N)(c?R4dpX6mN{lJw1-a6Y9y(k@7 -F4$m!;3XGlsFCvkM>3dex#u|GSS8`LnLZ@TOvh9O@xy%@iLEhXF;-%Sn~^gm>8!+7W9Y51y9V`ithY> -&>7B8;w5)Jg)OEF0F|3kv1O`xWIM*^)rLr5CRN|XxC!S@&Xk^FSj&xRWI34%~ic? -8}lPy;@WB4~lggPBYg2zM}Y!d&(#-Wpin!`YxD-CMIXRaFHhouxX*^%Jz;zRzFj}9W{te!D|sl>ofiVPm~Jh*XOL@T*RlPh`o -U;2yQn?$EFe}n2F^;lY?%GkzMJqyr8MyoA#7G+I$6p#a>HWr*}q%mN};6U@u0|+GE&SXQq4PO<04cLN -^sV1}^e(6k-i@JFf%ec3=m2(Ydn|<$8>aartdvGJ>*Pn&KQ)O4#arCMc=Y@_@CuzKzv}-2eaeUob{-BYz$%#x4{!@9;?XQn7Qe(4zzjoGjd#Hmrsm3PYN#Ipv!Nul>t+&NGrHSRv!u(q}V-7t6h^sH&}auV}qS2CPT4i`WCAY^~yP(3_t5v0vbd -xl3P;@?K=Ooz&!1h&iVVz4JQHd)d_k_b-0G9X-RVylvI4aN$kvnq%iXsyAsE3QB_wlHi+24XVLCQ+mA -v_0W6sHx6JhI?=8fOu3k3QB&Om3qfWV)`jbCCAS8bgSjRh;pW95)EG`jN$>K>d(PDAp8rPRi_eWP`c| -8zLF$~mT*%O%j%=)^mrB2SY||2X_HX=Ln|W$elBF>p)|Y@jS?(aR1mOSuIR@C=1}H-d};}4HQ7+&^Z~ -@!ZBzycQ%Ey9i@&`k$nmAiNPLUIc@7VEy-*%wVB0+7wrukB2sEe=NaATmv3f}x18&LkAPeBW)PgvIW? -w~5CW!nMJHcPZT7fNty6hhbTCM9Y_dm^T5QH`DaB>CBwtw6G)7KjenP%2V`0K>NbBMoxIV64of}{rTn7C(T;{bLLm$Q*UC8`11{>o*quaUD4HKx9*|@Xrqe(4HCUPmv -4l94{DE-|c%{FF*#cq;}@Gxw9&7!URXqxy^+Qv_Q_w~4|d45;#G-@hVib0y~R)Q`>zz~dG9u$%Sh)tJ -C$(x~S+x+pck+b`F>28N$_Fez>e$b|44;pnphH1ag49fof-(K$Tt=R)5Dl@zf12Scpr@^t3+`JRw?89 -J2u(tIj-f4$>e^@(Mm}qrKL0W9x8VB#kTC}116y9#s*%2joW6t_RnfB@H@6TelS|Jte6frk9qRL5tomb4btkZizxbFDfl`Fs -24XZmxlFQy`$|`lT7$ma^@MLNfnM2kq0{B%DwOk4qcH$cR)IWRSXvqY!g_Pj*G#afkyEOAqHcFo+kwt -#uqyM_5uy_qU&l}wEcMVy0L+FAA8G5DkNZ}#vcZOW1djFi?e6 -oC%uHB!=dZ7K5hvO!k=RXshnaqhzu4|T3@i}f)T>4`*|$Z`TPZGgVeDl=`n^!3XI*l$?-+i?rOo0O`* -?*pKDALo(i+4YB1oheFeIc^>1s?C$B*d+S*-SCrsR*Z7sI5BG!VmL;nmm(BG8 -7myaMG1GH>d{<0J~0L~RDJsT#2qUW_C!u+n$q&N7%ojTDhKZn2$?L}r1W-Gf{q_3y)Db@W0uENl=&>{ -jVv-rX`9*c&o;;n+XXOo<7h!A?>en~S*jiF(R9UF+PKM^KDsV#I!SHHW`k+lD22E&-**9WX*T;4PCMj -pHt)f&FpFg5;3XJ-C0pcda??VIi#@N~%{<#l2E@_@Xg|`XlsvmO`Ps#~DUu61*aGgWOx{Q&TzF(Q{ue -!4KodWW73~FGshAG+*#k~oSEmg&d0yVuRv28iZYERKlnw}_4ey3=gmC)H3^2AcwkMl9I%%eKsRr%Bk} -221<;Tiwe&uA~w1bu2>WVi+i0A`3W_l#W?rg?Q7LzV;%K4|Ow=T{fZ}@d;I&d4^`l>HX!i&~9j_3Gvf -@UJYXU5;L6)B{WAeB9tThr&_aPc;xxkb`+vFk4o^QKgo?K@-V!((eH%O%z7g5`2%PoeOr@D~==P<8`~ -x~Xh8I`Wy2n&-xp>JBt^igN=cKImPn7EgwU=H2+TCtdx3>}BZywG#2%#kh7&TU2W2V?$f16d}dYb0d@ -4#8FEp(zfhtZceC^TghH>Ni{e`Bzzh9_s~|8=eFByP><}B{>^}HwuT~Chy9{JNXMD_vY9-My -X9?Fysds+MTB%XHVCV{Os?zf@01Mv{_%f4nJ829yYt5rWwe;$p-0m&i)J_Z5_905t*e|v{2RT!M-$s3 -JAI7S;*b?OwC&#CduCz0FfcxhNuv;`voMo@yZ50(dfu^Q&I6ADA36`wqu;Wf3;Qj^g9nvKkGR$0qsr5 -#kPQ_O#Fv)>>@-774+CYkyWWm8YHj0i$*%{=wr~qdN8q)(0Q|-(WJkP9OoqsxDk?j*CRSd+h|$Kr`!+ -yxW@S^F80B_b=F;vf0IJ5|_^yPN^S`0$=Au72-rUao##2=)u%-V-Eyvk22lH;y9FvJZOW>T -~;%092wAwo;I*JDV<}sKB(u+M0*&hb*j$jp9*MrIVKwFW+(Vemj>H1j`NspM%1(UCLzMFLmN9K@68?t -K5enh`iv#{9CO5)m6UU_On3DJx2!S_uram?&s?b+kO;+-7HPV*nQ#J0YWvzYolVDT_Ec@%-%affCJzt -a_La-^Z4Zh~b(xKT`d|Dw0ifT!17<$oLtmotzUC8t;UJ9iV$&evK!l{-sl#AN!EFlm7Z#>flT?fRSgTVYo<7?f -9h)!bm$cuRU8GaWRv6%p -E2@UmvSvYmGW0Df*@se1M8|sfP8(ktJ$C4sEv$;Dx*j$z+7y!U+Qo(gP|){1Z@10|XQR000O8`*~JV-AQjCA~OI0{mK9U -9{>OVaA|NaUv_0~WN&gWWNCABY-wUIY;R*>bZ>HVE^vA6eQS5yMwZ}r{R%`HFTjL?E#)!Y>?j$x<8(Y -Hop@}gCwmk(1ri{I76~u_D4A*Jzwdojp{h^-DLcKBJ!f$eiv+7~y>8ui-8u -gVM<+)o!IN^ctMcV_6GTr&!TF=J^Hcce)8KD;k?ew($v^T<48^|9=UGu_Y0#8GbDagxcg=NK1TV`)bC -*j -)oyYWz$34k>ncldo{q9N^K8=u&*=Trsw%5V5S#>?Dp{_RNl=t9z}u`cE2rD_dN9pKrMOO7x{pQuH=P$A}ud;db4J_XZ7rYMsoYa}~dK@&_n`YYV0J0y}iwrtaH) -8@y^E?iIc=0WL8p%umW+gKKZcZ4X7EIRI`t}^gJr35%O*T#8wbWl{O@c5~{lCihvHtX|xJ_1hO07yIf -Va|)ap&(Ev!&CUy|D62K}{}-;SrcJ7=L13q97zTFm2k8C=-)8CGxRg2cyX!2|WB+gbG7j> -3$_SayR@u=J?lM^Q3$@*1fc`DLi>5q)>FDU_o5wG|dHQ0Cf1qfU5l+J_MOFCs=%>4nMi$C-%gQ=JpJy~^ws}<{?vvMA{ -sutZq}=}$p#i{PPFRbo9Mb3eYsvi(_cOP`tc9nzM4M!?yILiLgnzfX*Tub;ls^tlgDMXqz~cIQJO7iD -wEZUa7>{djk?BddhuOZWFw|4$H&J} -F`0h+=sc>k)dKhE&*NaR8KV#a)Ms5U$1=!z)Gj!P1~_juVOAYPc`+|5T-4QWq!4JmT{Zb;mAR8u8&IO -U1U_BPk{JxM2AmI;C2TSr$`3D|!E6UVz=#)l1)GIy$GUcr-7)IpcF=FJ>P@!8^^U@ov<$Q-+*WyLj6i -mj6qTM$Iuu(sA|)=N!s||{8Hf%y`YnK!YHF|ciX--(Gq*I*lct7tl?V|r$dTEKDi#YOT>vc%?J?;7i% -b&1`XU3EF1A1v;bk-$2QFv367|))zBH@hxy`T_r<@vFU}Q(q|H%QQ8_~qq(cmeUU5mp>=l=;gHzb*z{`=2Bn`jL -EDqw3!@As7upGczK^(%Lp+iv^H%S#Qf5GiRA7{UutIzZo#CiU%%FKQVdHrU<0;*jHUbb9P!R$>&fA6k -WQUQeY)iwpe%&oFX7vWfD^~7L|O#-6>-bn%gi0F6M`TRN{d8ZC`JIY6sswQv24zop7Y{zdMZk4Zi-4?QI -?W3Zlc%zSn7&+uGQZ}=R|tmBGpLh;a0@6)(h(1|rPv}QF|Luvs06zU=DG@!x2qOTK>0Y;so+@%})hL&}8KHnuvzmX@j1`RLrb5ZLJV)J((Y_F4U&2SKIZ*%p5z-YH~$CbAA0znQx-0X$vtxyB$@Ofm -LAQxf-l9ztR?1pu>85zY^L -~4M_APwHmNVK!60HretTcT-^Qu@X=J-N$QD};?y4MhqKNEJ|ep-JhG4}r{6FF-6O0p>1e-BdwC8r4H8 -5-j>%**t3pLl&thd`^mZJ4&T=t8s=7N%Pob4j^yPK?K@sn<2Y*UMsQUUp|W8G3Fs6)z>)Fq{`s -pWX<&~o(XLY5&#wQQq?)l+lY0>E~q}?h0z98W>f?2O2Qk|Gy&0Oz$^mvA_A_Zc?x`q?GmYvETR4i_*J)-NkkH*n4#_ -4CgKs6%ibhNgxWa}8%1zi$H6c8W@OR0R!!5Si%pG-*0)yKW9NG!usdjOF`O!V@q4=Y&SwtLyrKSS~_3jo{TnopZjlaOg%EXtM~95g&q -gK@{Ary*zYKq!E)5(QoRA`(65s9}$ohIWnqb|8Ioa&M42RL`qqqlMlgKrbOELXIqW}6WP<+ -i3_*Qb{+n!92gf*^<8g2-Q+wRflw&n@$Fu~;Fbl_jY6amjTMP}b{$(M|K?kZS5uC9Mcs_E46Iw#s{mT -_b$P+w2oR5yWbipZUi9-!&loA -E(C(fAGWBZ<%01k&aSZOlGHSzsX|cf9TC8pdEasagMt>@qN#wLR_0YFUl}SC*hS0f2S9tjg`i6!N+R# -;JodKDOt*tnww!HA*}frDC*CuMw&6!{zDOYhw5h!~N4}0KA+)MHgyE7P5QowYMmFiscns5HrA|k;fKr -M7nUrnr;jD*a$#v5y%!EEIi6P{toy_yc)TI5?$YSr$Rwq23N8?tTh{ewXtamVjr>X>9)Y>Z%A+GfA(( -&ctUIikjLo2aCzB!x{3-+8Lp-1p{}NR%WyrQB~YaeZi*t8B&n@(Zvj?_F_OHtFU`iP$vnY^lGmwd3s&}g$j&-l9AutQU!a1V8}=`!?>%65YQ@ -X>|>M^qEWdNcH2F|A#ULHlYu4M4CA|U|+zv3}rk|V&CYnSXgKkXPYBPOD2K7A$ -l1_XS`P@&+sB}v@4kDXW#~_`D5lyP~FC?7!+?#mfa}r9N7sh}h`K(brAClJsm*Tk(ZZkxZ`{A>Cc%d& -{!j4uog6aj(iNz6gL75CCJD<()5~S8C3w&YlfQn%w{AQo-IAv2d78Rum^{YPXotS$`wiE-up~=j4uv!XihIAG%X3vPbtu>C)H!p4Ez -6d4MVx_de^HUq$X=|eDOF|Am@$80yk-Y=qOx~x<7^DVDL4L>tqvJe-xkBs(z$BW18Vdtn#;pH={b6Atc;m-ZCz>m$w#(>vEOW)J0IjEgR- -?l_f>%i_`K61t^N0qtJFeW#}}`n*J>zLJl?=Z^})?jH+D%r9qoT6#cXGT^%`IcrguoqMZy$ -`c-RR)^0o_>O>9GCC5eXzx$cRT79oiVZ)mpdy(E443#u6s!S{&WT^Z}XbYlrF>RO1qU_mFio8ln_M`H9iFLO3zTd`S|`CbJ%aF`tyCGV2aN8PN!m?S35_z|PyO<%Re-WFvUL}&4%QHWb`lU3Kr -rZ9f{`siHy${HLL -F7JSGpmdmxKd($$*W8_U)B; -Q7Hdrn6|(lG%SZRn3usH)gjFkxO%YgJW7!56_f%r8tZQXvwIm83=TBqADMCn(*vVs}Epfp~bN9=9F9( -qf&kO4lo96_+-*xt*`-cIK3MYD#F^k&$kYt*Ms0qTCIOP()70tvuPIL3)G1@bMZ?r3u_~s=I7-z8O$Z -F@_MQ$~B6^6sIjHc2McnzIhjAoQ$KIT;a|}@HoHF!8os{?XSc7G_8~hwCm{#J|}xR#AWP(*EczbvzX* -A90cLOQ(_P95?}>nzqrwJ7yE4~uI24;{nItPQBVlCMh?c>a4_y$Kf;BR!TFWFNkwpDo^_{JZKRsc0CL -eW?$iq*I`FaU&Bapt{H6?%V$j7f#Qo@0Yo$>taA|rc)T7D2+OJ({Q!C@0V>nfuy=kh%v&SV2lz&D^Rz -7;9DsBCn5?yP1LSd$gM*Q>F5q)>$EnCX9qpN8Z(hfjHYzStDcG3Bt!rL`3C6n@XdN{78N}{d#lXp(E4 -&9RHp?G9R!;@1g7!>j1`4e?mMgBUcRbClUcNm3K2S5Jx+f%64MpQdTRDx#|;0oe%S#cNUNr6;Mg)z}y -t0}f4SmzuD8_|&$GC}Yw(#nXoBeS*7y?72YkYN7dhc~drEsxeACmCFM(K_$RXAEfZW#FBo58UV^c&;P -OIkFH0S)OPZ1x(O(-W#|`Wg~k%*iVOIf(%rT&R%jfq)+v6F#ERyPmAhF{hyBCR1wH*bBq0_Rqquf+?t -h{R7_x78zKdMZApSFe@Xk&^i*+WQIAoHXxBnJmHd*85HAy~fFt6eG;W7t;hA}VB{If%E9t}b7y5L4v{ -!+bg0WaI@be{GFJAi#w#yzF3sJ^Xkh-UH;j2%Cs-|3>#*j;zR8uR;3;*f_juSLE91B0*JvhoV@56EI#{3z#Y2Z -0E`a$tSJ5V6CZ$mwjqZbw#@K9Uw;4HeYqjl3+n)@!mKn|wMc-c01t$huqk#Rh^=!u8nDYaIB%T0^{y#qTjb8PUe|85vXf1p2E3UrjcoCs^-h5tJ4VIRr1CUcB-{)d<)2dx -#9@`VG<+sW@JtI+mFZ6H2OM}NBLFT@_p$C&LV8i6W+r=p_~$easOA@4epFm@(J;kAys?x}T|}hq9COK8z&2KMP6UmwBY!GDCD(q=HltJ&&w^+0Kraa+_3J!|9UlLm7{$(ezVWgGtp|T -{(=>_fz?Hp!>;vFR4GE7bieC?&ydP2Y@TFt~0H=HwdGa309?{??BeqAdh+#KaJ2A26_+Muefi?3it3X -NKO4sJ>~s2rc_ZIP(FE!v>Q-9bxZjWzH~8OmpoJ)X;Hh}E0xeXypLE?-UrEz3h#vJ#wg##GvotvD7v5 -Y%^=HlCoX)~0cOr)r|>}%s=tezQQjoc4l{V*^i*>KT%pX%**!Wh|t43mWtcM$75ug17kRCI9 -)6aeYcjb?n4-P~mfY4c(woaqhay@c8wY5j1xE#p{noF3|pbDV(ve{|u+!yx69n`Ju>BDyPlU;=3FrV_ -Tz{3uTT>8}*lgtRYJBHX|-vEsnl-?SpGgys#_~M+|p~G}NKv7j6r3{vtY)xtrq2+hSU|%Mo7wJvsg5( -di%m?SFkVemMEl#h3pPpFEiU^wZnZ*E*3PLHG|R-=kg$LjuUrZ-<`a?`DE#ZZXj2)1)J8|T8<65a5p9J^C3NtL+l47X2mxGdE9w$aTWtey2dNT3urg> -b&he`pq{rXgpZM9##b(U92*kW)d`)k6vTH5NdW|TlD>s(@XMOy_EeqY%P4OO@+MQ -S%#`x8LBp&XqrIC(aTP_A&F+ywy4`IjGW)NmIM$u3(p>Y|0fKgB5H>G2hVjjC~ee8kAh`cvP1-*H~A( -+S&-Zt7z>7OZ%N5kRyU;bk{FhN=vi)|XGwjnyMv=_Wk2M4Rhy?E&5{)p*4w1YvF;P*c&ASb&V((rs8* ->fkvCGhr1k&+rY2j#+=LU#+e~JyH3>y6O$jkQH58CFolPiTb^I6j=WFCT=tcxV_d+sOM>CN1*-B=>D1 -Z`MFlPMYuf4*6cE~mB --SxkTN!==`=I}zzrv392X-03RxM__?u3a|CKQ-l))kHTNnO$s_q|@t?BMUrWFz$yAU{yd7unfhP+GUJ -2h62VnrzxO%H#km}pgn#8vw9Pm?7_&=_Q|Ic%u%P(?ahc^GGstPIwnnxIgu}sCfE -v+X(C>1f!G5vLY@R_Uf9qcYrARy1Hwt?)oBOLOW=cf!PzV^eIEIMFtlBv@+tL=M7=o*g>J_%q6-i-lf -?A~8j5XUvv;aEURm<`O#wToJS~nhk7sA+21#>T9+InH$!I7|2oHm!HE8zDTtqsDAEzK<0U63`P%{b)! -8w%F%A{@Y2sFiwfaadBfolMCSZmz80FDSH(19SzV}nW(R+IRh&6uT*2J|I0Qagf?tWFfxlN3vd5j7Ck -J(6uL1txsPi*}5zuTgs4M@r1sUq{!;AiZp9X{q!yYvUAAdWuO8nHOLSd^x6c -`YzOTXd~E`L0(=G$m~*`0I%-(hrn1_sg*?|~U&g!dKnvh(wr#e|I}~}!4O>Y-K*3*l%hkz_#Sc@sqwa -DFGql-f2?|ouL2C)y1EXqAJZh_^gk)=@MDg6tJArKYba98;npoB@*Kg}?|wlz^9W+->zzHTgPDto -6cCdK3xP~X?mY#NEtZJ%QCn81d3Z;Y~M_y{HOE}pS^wahtH;;eeyP`)_C>9+rt8OtMCrS_wo7LX|hf~ -`(*TXoy^Pn&GgHk(vL2T45&UT!Bp-G9HgEU9oJ*jDsYa-V8?u0lZ`XD5gvq`im$0#}&dZ>CD(Voat^1d}(HNr -7WgX(g_6X+1n!d1uHh%^lVwNeqyRZwxlLhn-5X)*-C~(X9dvaO14k?WG7yBkk7ZPq+??>a;#DZ(;l7I -67#$U&ME(A8a1(_){=P=Yxwf2GY{+}%KxaJn6iVfn<>AOOGr4Hm+K4>fR6LXHgeDN3{C)_--eI)i88h^@%(Z?%fyQ)|7)|IEiwQP59ebA^EpMLLCkS9&BMmePD -c4@^ePGt|0F_74M}UWG^PXT6hqSKr?^OC{PrLlX1iyV`G}kx8Y#I_QuzJ{$Ka>3ihiIy~g2SChj)y*s -?}E9H1)GN>oFB@79u!mUFR<*g_ -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 -N?nDr>7?^5@8wSQRgFU^h^9OgVtM`?0v{ -yyLgPH1ufGt*V2ptu9To4x>;F{=GivYw;L8-c!OEHMKDmY5B%#L>6#=B7l-Hxs#(p)61brCS>(HAXs> -)vsvqetBA6b;tmMslw)Xy5C7&D@tmw%2J+#0lnUlo^?9$4XqAp#vukSumtY#`-YX4hjK`aGk*M{3YgC -s(uOVCPi&QO%!?z+5*|jjIGJTPjy46wqENZ8$NpW**Z?Zb9x$vhgM^CGwP00)2i?JA}AS!OYUyE59c^ -uqlu?)_2OM_NS3?N+sF$AAS#B|0{ax3UhQw>6JIQgwN=J;rQ36#O5wXncItxQ)p&%3dIP?9vxRnV1AL ->n&Du@UuEzU?-G@WDY!Pr!AA(Vsja^{>> -H-9jRSn8nKdDw-v-5xp9Ceeg&htCJ?tz3|757jRV5cY+T)B1K8tJx)>fFLr7Z9;7sM

s?{~Lz)_oP4U#H&MpXvvyVj?Y2ud$6H}F!OiZ|e`u?NQ3g}`~)jMGh2?AWO&9fODpSy -Nc!2um$M)h6`&hh?`_pfF%=ROWcWI9-qRk0Qv=Tk`7-3ZDoFJ8Z=r{H>9+TQ!7gmMlwc1oU>OZqTWpm -^ESGkCupNeSO_@6N^<6HT^*yeC+UJI7TBcu6}(cZ_o?abTnNB1mbey=8o&WdG+M_+H(<}x9eT|P}XW_ -w%_pDp2Bih{|8V@0|XQR000O8`*~JV^s9OJ*9HIpeG>ox9smFUaA|NaUv_0~WN&gWWNCABY-wUIZDDe -2WpZ;aaCx0rdvDt|5dUAFf=y9KY89c~0~?IGK-algfWi%$G#HQsE+f%4TZz<2Dv9^{?7QPbmMooN}ILL#Qapo -A=A}4=2BTJb(Y;3>N_x9UUD-r&MX+wgw(}EEBg}Nr9Y5P1P5sG^u%^6jh -&wtJ>CpdU14V#e@xAFZCKW3Ka0sS4-LAyfM4d`@Jv5aV^H`N9h)v|0W2K#MQk{d=^ZAhnD&6y@G5L`vAP96WKP?abA!i$${ -J(9Lzi#{t@xj;yzm>K>hdCrRx4Uns}-4(%NQ)fRHK-QuVLe2@!{;`{c?5s`uu$HVF?JutbDv;iHeits -(hAQo4euUjO7?q -$^-cj2J7v;DC`u)|(9M9+Rj8wF~DJi+n$ZkgNA?(R6>qEYc`S$?`y6K+08ektyU>^V23ij`Z~KI7)rxf1FRQ)kW^y~@ajXj7(*c1`VgB16EL?Pp#L}@V`PVbNHnX~8O8%tJ*@rg- -t=NML=}D(A%H#=K#XX{oJvrn$rDE3FomAy$mb?gveZyt~jDRjdMzUIKfi;+)C0V&^0 -cPIBCaB!Y=Y*oY~t%f^TD>)5JM&37l;WvS}ozeKB=3^x|@ -|=+rVYN5%iT!dSL9W?gWD81BsaGZL85XA6^taE~!nu@uuaGKBUaRWn4fC@X~b{ifbuQ3HOJpBq)0Z8f -8{YDCGmLPBCl6F8v%uNW1}DO~KWJaL_zSobl!)ZfyGz?yeY1inH8!&l+Z=J~#|qo&fo8YrS5}LGJ2r;Kg;ocZG6?Z(ZgE&rpJC~BQb4ugv>MsK6Gfq2+fq#p&_G)YPV35nHD}*>!=2 -UM08`ciX8DjHw6y6%3_M846eBXf(Qh_%I6 ->&k1Be^ -%ED_rcpV2vRQ`{M`kUQL>4Q9cUnB?krwM$s0B9ZP7v_D=ItClqKal5>VOSmW_JcpQKA&{p6)Ss0u(VY -@<)mDEtYpM%LKPQ?@a!NJsX+n*)}!P*Howsa>}7#iB9w2jgsaxhkRG2QE|1`|;9A5cpJ1QY-O00;p4c -~(=#)IC_^Bme-#m;eAD0001RX>c!Jc4cm4Z*nhWX>)XJX<{#JWprU=VRT_GaCz-L{de2Ok-zJ&K&AQt -bVyoaob;+zZ55kNbg^YWNlu)~3Jeh|2^9!104P~a{J-DK>^F7+QcCW%*LQr0MPhenXJ=>UYiAc*!RYZ -Qn3ZW(TiV=H)8F7B%A@~R1;(8ew?udQf3Iq-LR^LuW>Bu&dT{BX=IDzrUH-$uZKZR-{R(O!Rlequ#XL -W7p>+=bq#=Iv#fxTEh^TC;$@@90xhd#*2L#`@in^}2A8af>6sY#SYRaUV|hKhg83ZcEIoa?9e+1HIzD -)P7#~eHHgMFe3ijmi$58(zJ&WSFNajHI0T5!ACv~k@;8j-FP_|7F+}c%Nk}8S3ZgYXf?u -C0j{Nb!+{Kaha|E>LbYNStU@jj1R4k5lnZvT%=0@~_aG_GVScWX;*zCM9p)6ILr@koh722g{`3nRe$7 -P-Kw$vp?)Hko3hhQ8Q2OCNaRCUM)=ow^d?Ul6pP_-Bd|Z=@p2C0;0|j&XZ5Vgh&MbHzh!r~WtG-g^P6(cMhlQ}_ -+x(w!nxAlMkEY95ME|8R@4lJu@Qg1^YphKhnTRDuJkV%9n^e=i!cM3K<%Exs?EzJ9lbqziN9Z;*JaKc -R(pWFsmhxyWrW5Nmu9u1*Q@=bZ-L#2Uhv#;Tq?pk3Z*56{>6(59~o#7A$FEQtNn$(R4nbN{ -D!jsRyjNyGFcIgli(>F6sW`Y~&ki;=^L2(;(`p|GE#xv7}NX7v^xPOD)X6_-m_qBhtN{tOtBU>j}iY= -5~6ZV#AD8Cd#;#3j!{3*(@i&6a2>mc=EQ9$A6b#+5c2$H74{%aDYwy2&sWGit6AjE~BwR@+Wd&#Yj96#(H+4%>F$xIk&@IHAUdTEvq6hC__ -aocUtXuT4{Q9wVL=^DJr_HfTJU5_Bk>HINsqvH{S}9n=(s-Bqej7h8v*&SK|UWg*5f1{pW6BLhdA5LqWWJp;CRd-(0)>+cVNgAU7r4QzzK$!@a3zy$jpCkvz+(qX` -{S(Knt&vPc}PpHw-8S=F-f=T&*lia -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 -7jfq1@uPYZbIz|L|onC#LY>NdmspwHQ$R_7I<8 -6R#t38*-Wzpx7?aLVd?(_wy-EHWlEa2j@7@A$#^D`D%PQ~KfW -cu=99xE9`XG0knI;&$Ke6O7w51%O3H;1i8VEzLh9pg6p`|IcTdWK7k(-j{&%CzM-G2iB0> -v={LhBv*y@t09;uAl^#&6>~rt9PY -oulS1L*4%7a|c*AD6@%=Z`>C4z#PhGylYrdR*z5nb7e^vP~bgor&j+4;e6VrFH4wDEaVc&BK`l_b$Bh -h~!zdq!;UNxut2Gxm4a=z$p3GrYEvl`r1$pRInebj~ENIw(cWV0)f2t}GReq<3Qbi{Ri>%!Er8Vr3=B -#{=ec?y4;{UZf8H5Rwx9mtlfP-yGF|M;U>;>e)yWK}FI>h;fD -ZHX(B7wP$*@ES;4BLDJ$7d=~cJErxw|Ku9>>kLd -DS$Ipcowj*y2KB-wo_!(`OR$u^ZA?_zT3h1TO3m0e9J_%rc3mY@dIy$Z!5`wk7Yj^%`(EntdLJ -MiFAV?4R+I2!WUiG>I6(A7asMQglJ#*$^e2VM;Dx4tu7#ORh=fC-3Ix{IXu4vDHt%Kvbw0LXj!~5$9m -6ic9%0d5W8ksK({GhOfE4zxvv^1XtP*($&S|=oDc^(|~Jqinr=Q4BY8L{3V*=0su2iAY#~0rYT6~Z~F -#~Rzx?n4Mb|pKnHr2y#WF=Z>R1+LM;&hF5^m0g9Bs7tUHV;+DCkL-F?4oCrmHMPk{hKs|F -8@?i?v-($XYk1v=Ob#Wi?|w0EZX3+BX!F`PCv*+b;H9@ocpFBn`^k7^%kFa|n_SDJruQGcvWTtOkG3!J>vfJ2$z6{JAZ6dl7isPu>UA*GD{5#hpOs?QS`D2WY1ZJ$s<^aMj_*OrEq+! -h$!YgNBsNUn1W&*CV~2>xGYzmjj7xJ?V96W!EhBdrD7g4#L3PIo1GGuIbNR#sVVGJ=o?f~uR8x+C*f% -j|wu7qjLq@jh)Rbqv{iaTrDXi}-?;7`dKb7-~Z$SF#2sxnN^02mMC(c+lOJtG4QnwmX-556_%f9>arAi&bfGCQR ->`-kJ7q1(4cJ?YnQV;XCx!7W|Mh1qWSS%3Zj2SNFG&PFpvtp7n5_& -r-4$kiO9M)Hvyg#tJB8wYA$dW;?A^t|H#44-uJLSNED}f0I}ts*O1N>CmwI!YVH*dnxD<3~fpp9Hyz} -KPJ+V(A3mJ{@T!5?D(26jUnqc_{tWs(T!Lcm^c{lY9< -P50GPi3gu|;q&zVg0W_4p8f0j^+8vxNUc;n?S4Chh2rq?$c>#b9w+B)m8B^(7w_Kf$)M&cQucq(qn8u -LfCP{uK&eA(fQ`BV#!xrTCmru;Px1|f7&S1@K-0Heq&as3KLytHf2ehW@FEqV~_RxY%&!^t($Of>?w% -J0mn)rH*wnSO-(k+uP(Dnf*BWaAE+wHRCD6)jf}ld7e7t8jvOylRyXMSj0A;!aqC!8Mk~ymyBz)Ju>0 -V-}hQ>TSspZ%6XYYrgi5UMLDOgVP91ShbyS=JLJpd?LQ_fu2ChZtxFoR`|?&L^RxeRPUlz!@NQddQ8u -sA4Og!-&WEQnb`{Pz00#@g$Wbj66E=iUS*Iqavu=LHO~sX^cXC^852l%0K?lY&KX@I#@~ISHvf7!jU< -96ZuO|vn>3sbw)*d$51Ec{t2kS^2@ZX*jJt4T93Cy1S=|Wu6toYv6b-_p(n-#ZD9qjd -(vKR=Uug_hZ^GBDXiu@p~)NOxmX=s%sZr+M6aNr0ArJ|$Btel{&Z?^*U@HscfiyDt+6C^4bt9DYmsNn4Bg^)I$LyDNi(K-qo=wnX;n&0t>0pj_PszlutKk%Q?b+=pzRfC*R7~zM^Li34qCy>d$ -=Lg_}VGr(|B4?v>Na`z19-_`j$WA>cx0PwrNb!WlxCzX&AOnD^q*x)^6%c2MV#z>s#F)&k%|tWfaG$c -$JQoD&$F{iozNbfnuj&D`jY4%iR*hXL0R+?syoTkxTHa2(YGqyBV^-`vA^YW61m= -^WA)2y7KZpqBjMaEpqN&h&7Jr=^@}*zajM`d>+x42Ymu40wo?l+qj -T<5y)%-7$`JdOijnZ^OGp4pjFU8ts)uYfsL)2(d4PpH`hFyANP@t7!RDiS6U|t9&j2(DIZk|i>}*Zerk-nAuOwjzfsAunfO|At)^DGudu;WKFTI8pV%V4xN%N2{WL7E@l^vpVsmvmKF4Wy}@ -Veq=gx`(>Aw)nj4F^YJ73)|g>%P^9c5PiF6iDbaN=-7r7r6-!)$j9wH6&DWr-rc;G)gl!9FntbcH-Dm -omV)f9715LVsv<~J8FlJJgB#8sC9(Zj -WEoD&JZ_J3)OmR{Kiz&f{-f4)8lLT33vd%hGe)%|7M!JL_%Dgcb!F^zn%(oPGo|Bfpccm!nsFp;$6s* -u_}N()LL=&%r9pknuRTE4RD(^Vo6J){p{jQ*D$ufd~ItE(JooQOk49z5fdNHL$YoqL(KU*8uHJp6Dl{ -rhq?m+l_695)r+2o=l^oHtBM{h*IOYC0Lisip5Di%Q9luA&6VteA=7zg=cW7Q9jq68#3}^P3PFmw+0w -*43I~+dQRlo*gqI=AMR?kKj=n})qD112d)LNFIHqq_|8lS%Yk;cLz4V9xpUqfh#xZ9K^f3alnJF{E7r -5?%A4~|S!*Md3j9;45TZKXDz5&-+sAmj&Wt_<_vgU$(@kQx&Lp;JJO&d9Ouh(QI1X$b#;!1lfqO)}k+ -ty`{6Z!E8MiSv+Zoynnp+8={>wVdOF0X1Szj0xCBLy$`xyFFt!6e1)rQJ4M$p!a`VJ6Etm(f9WTgR*A -7!nW0Y!EFx6IM`A5cpJ1QY-O00;p4c~(=Mz14uQ3jhE_DgXc=0001RX>c!Jc4cm4Z*nhWX>)XJX<{#O -Wpi(Ja${w4E^v9RT6>S%xDo%~pMs5XuzdBZyO-;=FuH9oX;K78lS4Ks&>Dt9TeQurED55#yUyXhduN6 -Zk$QN&cQt~nEzS&QhV%H5)RV!H*)?axBlZ{_XwP|M8zR7W>bA4}nWe3hmCHqt7^R+E27RcCVb>R~NPG~EoNOiQxn ->zzoGvMNeZ)yEmziaMc($%-p6V=wXhXoNa~*RHLyG0?rGXHk;md=K_dN23vO0@b@B7YsgQwyp%PVO{Q -bB4@b}sYsD7<4Um)LKTN0gVdd#Gv8Jht;_a_+2VbS -jdQ?S(e7NdjT6*2`5br6a{ID8jB%$X7{WX2F)-(SXoD4OINRwgcay^FsTGLJ$>MbjD15lt#%(*=!9rc -)iQGx~+sN_EO -J4E48=13@Ks~2Lnt!$$e`YIzKGY!-x0z8T&4}Mvv7srgNG9_}EswVnkUTl?RR@8~*{BX{O4`T0R*rRG -g4jErNt4;Y!r6{jDT8=kWn$0f7mfe*R5VlLVOYhMa|hh|PXUF%H^&qjQg(L2l|S}4Qs!V~{~Vl8L9ky!QOCTL~8DI#aHpHW&we@+3zA*(?!%6B6oQ$ut?%AliTT~_F;o++t|B$Z -X9e@#tr2jXCu{L#oWPa-{0NDVgRMeRtXLg|KK8tRE3pxg6X0R+lXosuUWpsXr8b61|uxoaJ0{y^7FJ~ -?^BC44K@+xieuSJs%_SI~7RNLI%h3tTIL{HTRmzNWs!7RUPV@i^$z{R85AQ9dO+oZWQHt~ilku(r{5(q0qJ6z`Eq}PB-L=!VFz!(5YsR9l55N>iM0lOTVBFQl-p^xurB~BE`Ak@ -einlEnZqj3qjgENaeFfK6(nS?xB-O1vB=I|PS>(D`Axwtn1o6-Y;%xbwGD2FEOycnSp=v$*z`+ExrIo -oj|6U@Q3?c1za@l0ouUp`&;>Q+ySjzZkiNfKS&ZD)E+K}xi?aszgIw2w$t@~b#gXhYfsb~fnx(ySGfg -aFP6$U&8D;GOG?7JWx~on&EIvaw}pOuZq5g^_|7y%tUTS4|#{Ond-ETxPrmmVFFWpsE#?+i3EI-|>lG -*luN6f}!G}hOG^hX$4-2CUJROPoN;f--7;<))O5?VT(COf`>L+^>$yRhpba+^;3|ezl4KX!F3r1|MD8 -(uqJ}Jl0=Us3Cv6=KICj6Y}?*@J$}hIBP(sNhO23#$Wuames4>%NKb>niYF8Yx6s -CdJvg5GkIcv?O9bJNpSCcXCb-2{@5ioHA!!o7>I2;Y1Ep`HPVxcn|KBWq7No+2@E@2uT+9}SytSOG}?4Ri)M^A?045#d7*7ksw5LS5`)+r*b0a|)jf -?OuG@vCJ*i$ti;ZrhxXT4AGssumxP=B{95?zd!?Tk0K$H$(fiM;N92rnx;zBo)>%D+do<_m`(pLnJ{e -a<-THtWb#Q`>h$N=?``TZRhe#aF-V^2fG$0TwxZG$=uA#k?PCjIav9`sqAig5y8o7$*B)WX=k3|)jif53UsG! -2oA#9xEoL(p{s0D8b-(ORnc()T10%G@_~0AmG5dc7EnWZ~M;v=>)P?48gl04R4;$ipHd -1m)|2cnB)*}WcFL98Uk;XqZsM;XIBvrp(Jn5FGW1ttSIko=*XHyz-JjQv>aw`Bz -VHd#~x;)h_-$L}~P%&wJ)EF?8L;c1uU>gY)^r5UD6cZ;WayjZmNCqMsU<}_28+qllof+E_xwD-0coEa -v)sc`Qw&@+7t@87_O#P+Vfd$n&b=Ip7hd-Wm^;I8F1_Bq1Oz{=p9K<8^PzA%isOof=p6N~j7k?m2LgZ -mb;O-ww)9>_Y;jk}Y6k2{>!RlMEOTY3>ASV}kP$<3ezL`bB!! -?~jQK8hS%5{ullHh_=()QHdc-=C9^HJRkwR -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 -#YgcscrBLH8aX$a-jzmMq%$5Z3v(1?ZE -7il!~>|Zlx6rtN7W#KO2jtt6^2w)*fPh74-XHppY%>}t$rnvXvP3E26ijka9IbSz`>A8xkD0~Hbh`_g -5;dp05-W$3e4k-1M6VXo754xdfi&`(8*g6Xlu0ywyJMs{!RcoWYCn*-X#Lxzot=CL;EtH^AO$4FJWBh -8SP-qvx(>*=7X{};ciFk5yg#q?4WI~3_tOJnY4*aY{;TQDgaRC3e2a%hBdv-h{5n{X>6@uEu_*kaLt! -_ftkAAkIRV8x@dztC6H$M`8TuZacp`dtGw=s#KA5SAru{>X|Q!lH)vJN&5&k7SRTYZs46K0I$GgyIh_ -uI8pn2r;NBu-4*ySRW}wR*Uk);@%W4%cKum$e93ThY;KQ>|Uj-5y0328*ge$vXz$&(Sa%$2bWy+Ct*^ -ycqYQoIWt<=FB{=^Y}IkJ<2)%u4t-{J-uDsK2;)}+W{$SUR<^3SjnOCL*`5x_kQF;LLz=&-c@?(t&d{ -HL0Uc2@FC@5=nx>08T8+bCF7SvKMI!i%!k&Uar^z-Kz*d_8u^5C{gQzb@L#|GEfBs$i^Nb=(B7U00OcRelxObRMb -@ai%pe?Yl?Y)u#z{JnnjN(RcP~lnef`cmlyn9I2EjaR4es=7!~yG=TXT9k6A9!H^sL;)K4#-(oZlJ!1Bqq#S_ -jOW=ixe(&)Jy_WL22TP$TP%mrh~aYiwK|#wOtEV-7Bg<5D8>+B?Q;zOWcj& -e+EVy!3Ga$qIiEQb}Kf*&zXV!T6l7LHf#}6+JIgrB7(|?iWA@h8;iM!h~SRy;NMS;2mIAynE{JBPDnl -*3J2>0LGmnizpqSLw=iaIwyDp%e==iYo`WOq_HX7Ir4E*j#q}QPYlT_6&9PdFr$PofzGf`4DlXe{O$A -SL{-gghThgmY@p8{)FTqVYR``SFon8`lL)H|ordg|R<1(LZXxQXPg8ks?nIU$uL&gHpiJqr{+&h?FDw -j+q-aa*?<1`x?aR)kN7zs4)c$Ff|G_NxsnOn2MqU9Pk0q&ah?94b06)1XcvT)4LAgGBYK1@erY+|No@d%DJTSQw -QsMKq-njVUn4RH&v%F*9p2?WaXtgBi)S%4JAI5AUYLHXM-(WFxcqUyquPg!t2pN0C9kPX!{b+Og0iE4cJR -3sNdsM1i@xb)Vw(Shnz*<(_%H7bi7i6<|Cb -6G7Ut&eT>1D!w!U%q}_Tk_Eihp^C8S#rXgypr?L~Xw?8a^FHPuvHz8NN!n-AG_k{Ymul;DTa=e;tO-Q -0%+n(CkEkab-Az-sG{jb;WJmFUB0jJHw8KmFpB-XLmalsBXrg`WSGu@inAhD@a>Lk-GfOf7X7p&rf$Y0Ch&wv#N0$&D+@4bIngv8t{NvG^}cu@A|soksmbdXZisfJ2MX^py-y;rnkJZStK0T#kP -6+#OvWBu`EM52lAb=vA0a%^`NcD<`Qn!C-jP1mAm#1i=))8hjPZ0r$u -AsESBGBg^Jx%}XVit>tonBAaBBjRfI$OXDw#*Xjukv>0t#*$zekfQS_gj4~$C#6^U4X~>P~TmnYSA%m -=OJ1mf#1eBZ5G`ZWo<4*<^w<0HdfkrgkJzvew{eb{r#nx?*M0Y|Lg!|;s(8L*%2708o@!(BvP5`vr{@ -Tg?sGuN$c#BTKR!(2-F~)~ESk~WLGzNG!F(lGoQX`oMS9o564o{Q&>fE*$x49Lx)H=3VL!_zLm)tvf&P)h>@6 -aWAK2mt$eR#UKm)&iHg0001b0RS5S003}la4%nWWo~3|axY|Qb98KJVlQ+vGA?C!W$e9wd=%ByI6V6+ -nIxOc0ttrS5rU#Yj4mi~32sa_L?yTwvrB{otq|8OwHRgqD}khwwV4d7*lJs!r?gVE53PNkhqm&=AK)& -F*`TNa5z|67wp8yrJQ_+!77{Y=Id^6^3EDn=zn{SR>|2D?NFpL@gRFz -@&Gul5V^S}S=fxoP2uVpc>q`iCfe#7E-ufEUs$hy3>4c~pZ;lc0ZJ@nwCkA9cW`_}5b2I0}XM;^^9yQ -eDeJKtTkdQN6$dcI!9*54gn-_iNYnTfyUKfi6}5qN%3nKm;64xLJ9vh7-8A!E3cGXWk7>MBh -o7%MZ>P_DA9=`!hJOq{hhM#shv&o --dV{d9=Fu>D(5g8Ns~+SZgmIm%_k9lh{b@8D)a#t1fiZ_~!Hfp2OxAEETxd?ix`u}!O#2FbX6}V&?$_ -X)a~f8!`7Qu{mZjl~Be~L9vl#1Xs{ybE|?i7#QqVRi -6qx!iV(YCJzHyiifkc1h;QNdI>}P9K?m3ZgL<46Rm!On#1%s8M$)M{x10rOsyjkttRt7t7%|#O~3RcY -8t?rHeyXoqNYPR-%yh&QIlNBdhKeke9+Auw1noAFz%(K*+z2&Lj0sxF-lHzu!2#J2P+J+i!BHT%L)gT -_vPryS}Z|4JKN*+{S@j9x(4m4?*RuCA~g<5jXmTMsK$Q85#^o@`(6NurDU3_s(zza6$Z(IWeO7YC9nnUU)EEcII07BkfPH2IQhOD7Kbv8y^52!s2VshsJ!C~D!$_`P8R~)gI#BcVDjw9T@BpTSUh-%*zV$*CC!tw#k3Hn*=bj7s-hggM!d -W+|#%R~s0#~o!0@c<4@bkm<5??wZakgA}##GtuXRfOxssR5FiXStieBGvXN_sKe{9yi)Ue#VVW|CLrCEo;v>hLPjY+~}kbE$FwGDO!VN(^Ms^ -Ubel8CDuE}@TnD!$ht2TljQ2bLZ(fDjIN%Vm32q7akYB>`Kn-q^KS{$PMey^KuCg6N!ks}^-2J8vAdwQ3yb(pZM;0k$BcRtq>arV+__pezgw`ZZd9ARhzfww<3EfMC{U>yq41nIeqgfoC34jUsmUV#JOHKxw)32QR4Gw$@3Nf -n7>aw>4wBrq!-8{YR+Fqv%Lb@pv((eqK|x}CGs@!Ct6~!mbiwzrX_BpiQm%_4ZaV@Ihkl#{t8^f)toY -@3xIPm$85LP3*_eYsysAG&9On6BPtYYDE7XJL%B5gY(9a$-MiEcWMRW1^0A1P{B8yZn$OSm?M50Pzqo -`XYQq@0EI+?$Ck}_2lLycc2k2dl-Bh)3uGS2jTYH^_h%#HT-G?q=yCK@9@w%fheoJAP5RPUiJl08 -|Aup?-wqz%LAzv%p)qeM~(J(KInWouBAu-DLY1jnV}x01s-w!UvEcQ?6yjc2*vUy$kJ!7r)gv7t16Mv -N(SOA>eaWKDQMa-7V0T;1jvrzEtsWE>Dn^Zv1HrQpd~BjApVy1GGjcya -f!mjY2^Vd80yM~2Uz8$zt|xTs2|Q~L)k`Um>LoVqVCU}k6sF+3r^?UzVwZ7V&80lf9LC$E3Jsc9y7eN0fio9rNp2`9cmQAIXwYq}msv4nt -PdO!E)*O(7MFWu+Uj5hq}XW6aYzXti%Wq{Bne7AUr&`gWJ;~H#9W$kN-NRSVG3eSzcI+)#Ck{>HabaI -y%b-EFMupWX;`IHs_wxyczv@tAZ1S#xhV~?QCLN;fo30#{RU9($i&$s7|%yy*IfmImDOjn0aBe)fi_j -C@r*qLT+Zr?81hGepOXhcq^pU@Di -s3+7TEPTaxEnK$Ja@PG($g9Nk2j{AQRAHV0o{XtE~!K?|7RZ3 -nRhpV{v$U>WgeVu!svflHD&B>mlAj97Pk-)X)<=u?y=L4D$mFpoBuy;M$;_N0zCSlD -fiOwz+R}V5Cl-HnkTIy_sM%LaZ -DfFmsu#;X~sm5;trV^4FP9uu%3F0TsE%dsJ1}E^qEMeOiB($2uN~2AaYCWWdvxn22}nN5Mr}~l|7_TM -S;FY+?dS>B?X^w`^*k8Ueb^$#_!(%(>ssb*8^<12Um26&cNst8XbZ~QLVJ`W%VE=++OeK;Umg?Bxr7* -$xUv!jbR|G_MQs-|29g`+FA?&Ny;sI5-#& -4}Fvb-`M2qUwKY=bgzIsY}x)Jo_YOzU#`K=DAyt|H$>0CMvLM1YmSDY?8hvY*K-VIoJ59^Dc0;BuZ_2 -Sudr$L%R4#nIlFD!1>%6u%h+Cnq*)=Zod22MAOBW{)%v(@1nwg2g{^SzW|_{8cPkvFyNT$!ZkCdEy~+ -^Tj6MV4J@157hyL4IsSop>W(Hn*&WC(8`y@&!>eyPm#?)K`PHW+Ice+X_Rh)>~}O@TniaCy|>aHs*8| -^N*Si9coU+Kw~!m*q0Ym)hz;2G2wq3K=;&#f-aPaG)V*P%{9Zvv_@YG&+m@0YWen52#L58Ut5MR$_B> -Fme1PB)TuzKQfjaan;F+*+359JJ?-aKp4>T48SU16Fm5suqNyqqLxVOKEm3@0 -t;LD_Jb^Vs_&=Jr8Zkh#dtzz*+?y=U(}~eAa%dZAHGd{}67#^5fgm#Y2Xh-{BiXV&HGqVude6V -s04gl?TFqWlf*p!)E$KnY0oFLOv^z!wETrzb}sc`#W!{LqnTQ7sYd`{e2w3PZ@)uneUeG8U*MW -H!J*vaLfilD+G;7$I9Rx>hehNj-i$KdztTlmyVQOLB2cXb)1J -)wQ+o6rMenIvDv6=;rcMig@osDu??x3r2HYEPiACiHV9DH$?YJj@snU(5-;N1y@+!+oL4VVNu(%O_?t -Zx`wL*Es>-i=-b4)!bjP3KAa{U9B2~s6?*Pj{ZV_d!$7y*X2Uoz@e?hpYl3h23R!TmBSpW1`E$|Y@JD -W@W-S*?l`p=1RW5cb%vNX0B6}=7P$aXCFup`1xjxtAq`zmAHB!+4vM5RR;#D^GSft$%)z?3sdYffUOD -o@gGEyZSf)wS8Aw-gyzaI^k6h4|i!4gW9%g+1Ms=Uu+GJHwK5#!PD4UHoK(16 -f2y4qku`!$Bvq5mUiu^b`$()ODyC+2T8d=B%)^7y~!G65IzsQxn{%UbR<*ikI&><@-H3h4W4-W%u*u( -vIV|IVJ)e1u@HTKA#@T^>(E4g}=)TpZ$^=y!$TW~8{2ja_Mkp*-XbFF{J)ndTW)nJt4j>bXm2d_hfSG -@%lT_H|ZEM}PNHk%C#4UO@FPk^ys*ElFQ_QV1J<*4m#SRpp{GW@hxabd9^cl>dEnjG#IdgZW^Dn~F7D -LABL&~?YrwZSMy9In9)%K_Wa4- -pnG?#Aal5SLEa5qL)Yo-*c-U^G+I?2#{`FmC08w`g_G;YA+D!Gt7A_i)Dml+94!F0-7nZ;En19CISVq -J8)Z26IMl@?;Y^WW<8Leyvia+agk0eUS~`a!85aK9-CE)Aj!h2a+Oas0qm`wk*uvVuzC~82@Lew{yGuv;Br!e&B3Nhmk_8yVr_;iprkJeH;j*PzUfl@7TVF~37U4Pso -q(X(GF@i6r-i#yOy-~lxSs+#^PBi=@~bh)7Tt+0syw3>)t(V%c*zW8G(9Np!u+;+N!Hy|-1U0MOF7>t -?p_$9i@O^yCK0uCKc2$X7r9sg1o$Jc)*H0fbs$l>K#)9p189u<%q7Yclx+Qmpo?|mCiq%SC^lV6ztx6lJQ9J`)=D>lDp{D%Q)HIT>d-LYLliLf6oc~K7%z%WbGUsAAzMALg0X -V6-f?K;hC>f%%nU??Az}>bfFIV2MT}HA$S(kn$nBdC3JB)R;r88xncD&O5WpJs%(pQDX2wjy35D+W7~ -mbc0Nl|vSP<^-niQJh8eCbIWQf)(U{A1`ItGAbEW=IT#c3q78gS_Gk{c^aK+z -gE);0Lzyn!fhSE!STYPjE7kQTd>F+n+YK~RlbLny9)j&)%NxsGC^NXB1vUaQW8I#7XB=yuk2=X6VTc* -N9Q`5+|zVhx?m{1!*R0V0_4G&FHDjnu`VdT<@m<0NG+FWXY2>?1C(pvT3(^Gn*nR=)z!VRm#DlSmxXv -i*mNj7V1b%~lec4?pn}RKT5BQH`ncbStRhavYn=fGCFIGAU}a)W9ZFi${B*&jx>zR&IOqEpS@Eb(abg -b2A3wcMXI_t6P^X(uMMc;R&rptEKPfyh18$g2h@+c8opeQYu`60#YeLAxw5z~IakE9c~xHfDL=>&HNnO+Qf>G7m -y2q%fd&l2RlS(0(ZCy2GqNGMI|lg4Afn`wKV>Gt8g;N|;U(%ii|34zKqR5N8Cs#9VW4HCJ_YD`NoCT}2ufpx}d?W;&*)()2{)1Ia`SECPx -RIoz3gsciEQw>=~hW8JW-1Zyu~bLw#0!&ch%+6b)F*g@6{vWN{-CtHzV6~@BkMd1e~c`8*61snO)nxs -7$OE_jl;cT+j9ml}>>*e{SRUY5WCMQFlp}J5mFh}7|vJ0!4KV7ILKZlSsU%5}t_3D7WA>1v*ZDDEg<{ -^ZiAT|Qezlt>IxvnKX%$2K51yY-0KT46soW}LdC2=9w;4cEUu|P*Jmj8vKo8~CnwHSxtk^vWFv|E{27c@=me2Y%61$KZEziY? -pjGT4J*9M|e`P6disR04T;-k%2TRmCx{mEeqW^Qo6-2)wBRNbHwVo-r%Q!fEx;SLuE#i<-_!D-@a9*D -U9=(^-^u3&~&jmbWrE|e~et0evHNaLU&snSoSX=twxS3v)tq-7<5F}Pn1*Y#RVISg)v?{f+BC#C1f5>Y&Xj(P}Fgh|6pKJf@ -K?xN-viwy*)Tg)6z77hHm`Pjblo~e_)d}dsvNzGoflHpGk`VL=`~Bh=&ySBxsl{P) -GA|6bzLUGU`2TJtL_0ED)rTw~&Q5Q1y`fpdhiG@{?J}5utI}KHC^*-1qS?s;_9?3qa>WhR5SKfZD$iq -@Mki_oIkp!3!e11&QaD9Sb1bydPum!jJI9s~J>`YR!ch;k3A)hiBIM59~*Zb`uD~JeDfYkXgBOKCu4D -1Qo*?^6pGM!bTR;i0aiyo`Xc4O{Wm+pG2MkEwe_=;h`lve}-Eb|F#My+8LZ^A3mf*vE^dv6a+*|m+P# -##fU_0wyPC+Z(*Q0uo?#Gv55j+V9WHNlNMQLP>p&5GJEkirw4zhoW -}d-NAhL_uId~&J%{!SwJWgkKS}{J*J*cWtO -9iAE;J+Q1xN{IN7`61kZH^O6Z*{y4xTHx**GrRvJ7FK{clp2fztM08*b@g~(;poEAJqedC-H -f=zfvUW@=B)&b082;;-DwF7pynzIQW!&>%^Tt^9GKVF3*0|piso8QLUi!)EwABKr@# -@eWwHTn#-Ef$s9_6^=GBAhn=sU_Ud*rDL}O>BRSza&rs;XcG{1w}bqcqiD*j!( -*?vSZW^^Uh3@&h*>d8F}*YW}$wdAeT#-(T>ANQL)zQZu+@l>RC5ZB#q2i^STf^XwQ!j0FUd3RARza019P=u=ZHKJW836@_LuLvbsIwa~fT)JR`@U0m?)E`D8pYHaU}SDw}@?O8XMu3!GjQWYzXaw^9X`#tGw~ -0&@9pP>{A0J$bh>DG7-Cwp!K&yP*bYB?|Q8n;)hdyr9%cD_Lopv~sd|k4;T1KN(2TQ6j=rjK~x%;tk1 -_98{Q{*}!9Ufl_0DBQ`bVT3Ce#R6ezeEO-F-6<$Jq#rPKo17{%Mht*cN4JIySV;W})1`020{F-tnDbN -kCQt&kXKG%i6+fU%{o=*Hda1_t4w>}rAkKkGQ*xo@Ofy4CiQiwkO^9^|X?CC(yWn|#yV5M0K;;DC()` -!ef3J^@Yb5IJN!2nQ75ZfhJ+8~Cun-R0zkzi2y=u ->Kyr3Ye$IZzOL~f!yjD#YpRP@qJ!4Jpp-EyE0^|RlBd;M*44P&QW{FjsHkg+ZsxajQxni{O&dw3w~UQ -c=3jYbHR%L5igF}kmb)@1_Ne(_u1(MAV=bWz}U -l@)wovr7o3N@_i~^_K-K=DHYaf`}T={zHLQLW|cY*FE`0$`8H`yzAXrU1>IF1FQu8AT!Z$v+|p_Snl& -&{odP9SJm~6KwG;D2_+Nf`<&)_jT6J*2djxxH7GBKrHn|GXT^aber51ZnE%V=#ZP7j%bo=NwFgDyI -0&ZM@<#-#f=Ug6&_?H$MY1a{ccHTOUQ?sW@cDAe=q5ikpibTKAo9 -;X1MSr>L>;c|LrS4^|_?!~I-Nu7>sbUNr~f#ltL+FvO5&4fG(VXqlBC>(h|(f!slUtVPR}apa3vwCem -Pqg(aYPv}{wT){#IuZNs?KTIvEvT`l=KtM0{9Z~{Gu?a|)c+Kj*d>agPw!An$xB6c9(xtxLjwB5vuFs -X_v#X?;I#m?P%UFP(ZJ!{owTeSSyaURZD_$Dn&5pmXpA($BKDgo!HaP=YEIwgogrhzK_M#NDu6l-f6~ -`D~UV#^7_o}B+cE(+KKxPDrHTRd8Qy^wP!tr*fkA)7|JBS5(S1tQ;)wNc2tqso{7V7)`6b8B>8TaFpSIXMiBQ(cWx4Wu4{bXENf&qqC8 -KXb3ov%twLB?te5VW{?AEw9OE?_H|$`PpR8kX}B^BsCik`T1PbR61Ad_h>ee>3;>VP@bcQDzF?GOW&R -F{(wE?)K)-!_g`32LN6~b_o^QPkH4=!&7`jj{?F(}Un`SXEO5Ge{nLF5Wr~+R -7so}W!ekt$*sLF^i||EClSv~L00=K$?(fMOwly^VdIhB9&(1Y`a#k(l^fM8f -hj3nD0pnz%p*>a;o>EuwJ})y)avhnzezDw+u=Luq^?Vll -;Ra_R~R$EkoU(w)NYgbZ*~~U>Vz~wLMmhz%OCXnAlB;g0C4}aIaqQ7+NsJLd-&o?7gB$e2;y>YMqbY$ -$Q1Pk1rSFYxrEyM7Y5FY@$;Pgkm8UD_ponxXN^1j|S4##w@mzO?1N+`$;mK7n5o6z*KJ$rqzp^jdO)u -vE8g^Z%Srgg<0Zu4d!MI<|Tx|({LTeNnuuTEw|FZBV5ZI8u%{PGLr@bP#2+*4*;Zfmck(BT@Ac`9JV+$f&;1l4gVmL2KNem -0pC|m2fQ=|jI1h=E<0o@Llw)aq)!>Pape+V&AIt0jxV`t&DpAuyuVl2?X4C4@0uHiHxRgahH>;(x+u; -8ie79fK@3Y9m48n8;PrQeKapF+lt?i*0u#Ypj`vlJ(L@Fj4Hd&&mgX-jDVAw2~`8vEe(hL*aCYn9Gk4 -dvx3ZGI7*&zm}9r|^~`0W@EL_5)I=fL{Q5Xdkd^MABS_2&eDD+~j -ZkayX14poh>h9m7#6r{Ex+Mqs{^UDNXfW0$ZA0;B}!QxZF -#RbK|)Tgigc6;XheS)=N{McAKJN;*DOZa*ax^Kj;|LFAne0Jtwx+fns|6}^XJHf`6>Do6teXqtJHb|7 -o~)`b^jcBXdda5{!0L$6lj#1RYEiDO2v(j`v2DuPDW)JIp*3rXav&JMf(qKp_2wZLr3^@-Y>QtBM3ySIH_b2P?^EgYl_|w( -T&kTZREjCvJCmHVTKe$(R?_dT7w0p4su(f`P6>Z6py_xY8A}ik;A{e{@4|^f+px>Y7yKGA#+&^&C|SD -<@GMYLb{Y9>e_lME3E!3}Ll2zV{Gd#ij8 -Tj@!GV!ed);>vORaYCIIxwD?B;xR&x;hbw0X2TG7IW59YBDZz^N^}l{sRNR;7hWnn1Y8u6KyPUhmM?Q1)i;l_(PrAo~GrU)_TN(*FUjb~LU=)+Ee*K(tf}1^T&Be(nX@Ai8FD#J&wJ -C&r$np!?ESFl7(CLKlEB7n0$TQm@8*aet!@H#purh8rB)e&cj9&n|Q_?eI6_b|MS*HlX@kmAq6Ulrwd95uVU6Oai^R-dC6Y|uTEUy_p~!x;z> -Mr4OMlVsGqQgShGi8L-5#okqTS?Vubb?+7E9*#b;x09vl$hq&DQXzwv2Fav+d=VUw)OzFQH}#<&z8Ed -lNIZWgA*;&U=LZ;FD9}^&mszScqF9^slT-s6LY|qjuN4KsuLjVOVcgb6$Wh(@TB0Tn^H%wYE^RK&K2UL5vt%Lc&QHuJveX3Ynl -jH`I-wZnUc-6IbH4X%)mn>}8xRiDWNisv_^wiBqNn42}fw(Q{%1+C_o#5pP>vSWREpz-KirPMKnB+iJ -mLl`(x`BcFp&c^Cx+wQfy8gej@5KcfNc>zm5G8X94}fnt84;^F#rG%P=dzrmI)Og41}@6QtlCTvJ=TA -Ve-7F?RAWQvz2Y_Q^MUT|r5t4%SrGfau&{f73$Cf=#{kjC+z815I(`X87x&!dtN-$C!GFIo%B@0K+Tf -0ew!2gJ$x^T=<|wn%O#P5_ShGs)x$xD&d1kO2@$IOCW{FT39o=;J4P)SO)!>2l3ONF{u#huqkSySWG? -E%M0}8nOZCivT+6l<+B8I0F^@<*H#DfU~wL7#Y0t_$??#nv#!N|Im;yHEO{ivT8MFHxx9#c93`Jmmie -{;?QMb>`0K`0*G=-92ykH;x>@eJe;T;#=NPrD^7Vrd-A%)*}PF(!B%=GOIE4WvZqhNV{9Qyj*!;|(XJ -C85%5|>~RxAKG5s&J8+Zv3A41$T2xKACvna+HGT<2et=wG_^K_dWY0mviB`ctG*Yp~Q7l;%P~_t(oe)k}q97*E9S63)rz$ww$My@FR+7n`P-osRJ -OJ}iDSuH$IR5GJWC4Wu12l7v)kj8H4L`sa2|C6_d1%)p?f2s-8errTjamU4)E70;PqO2 -`A}UlM0fyG-&5wNjH@-FOa#10)Nl)%CvQEEyx0`--)B;wGu@;=y^HuSzb)nW28vSH9NB1-?K5)Pl3qz -yS@Uvu$zi!D -lDw2!2$WoG6uobhox|_kX>5m{TA{KAOO=4lJ|1$C5)v%}qBMfw9(2X-Z@;t;8UIFx~C*>dPW~fb%$&D -}jN4~r=@+CC<5?twyx>(fYo1cj11CIGL*`%k5PYj~8P-;%RM{H^(D?kD0NyCCgLhSH$C2R&>R2Crtn} -*2x|EyVc%c(nq1fB#|eiFAM@QQwf|8DVo(3W_g{3JFFBI=5Ah5O|v_iJxg!P_`D+3+?D<6g`~Cagf=P -JZ&0Q2?G{fTx!ufVvmUBMXP)?Qi1*ke>``8OtD}Q-1O=yxo9P*-gd*fczvLS-*+OUqRGO)FT+xslS~& -f`wsz^4Sq;nQ*0Bq<~c8L+r5}X4jiGzp_`LP+@c|IjGPLM$bd!w)&3NLidGKtW93E -W;pk?J7NZoWQ4eXr*HL4PCRPs3G-9qtE_C$sv%J!L;>W2DYv3dg`4YDc7BV%s2yVMxTLjn9oBh!`+z4 -Gs@7-)ezVjb{gPD>`4K}p}xO^>0Zku5No=glFy>^Yh3g@%hD*W2ta22+aACJ*lfI1H?(Zu#O9#W%f?B ->jZtTH?z#q8AUcCWGEHMmo67qd}_kDn{T3t;GF|hs1 -}dVSi31;%13il~R;653xic*4_s-9ff%G1c4QkEhusHP&%-Y$tjc$>LPeC0nxbXuaHVFZMDR4g(=TR<= -Rd5wDr9Z76xk(Wm1UPj4nWdykO3A``VdV#WR-zI$SbmG9B^sS{YW!j66zR)2(DHbC -HBhSNWu@k2OKVB>@qb6>dVsQlxpg~;z4xCxEh#fc^q83_om6MSub?nrp5KhRQ`pn^4!YDn`X$b5bg+R -(%U&7lW^Dw(S59hIZQiIVOt5KWYSZ!+Rjiq&v&oby5^N?JO^ymv!JN44kkJQeciCJBipy!{lNqR0EP0 -t@uTAqZoJbUm;T1Hn8sZ^e`hw4^poReBNh-b6WqJeAq0bL`}_fO?n(s~#v_^iAn+kVRNj*tV)HCXXxy -?AlL26Me;zNy?z%^~~-Y7j9MWvl3W$Z$hw8&Sj0mY+IG1zod2$qQBw-s?96tJ!!8E4C=J|mqFUW74TCd#~tnKI5`!HF#;QpnjJ$8=f -vV?v7VK6{3IWS$Sl1ktDm7}2RRr^jmPtR%vb>U=wlG{S#mL5X9qwDMV1y6z+2G#VP_Kr+g!F^q8+H%X -a%|a1xn_vc&UeOEQ2J}CDnEYPVqiuHO6C6tUKXmiVm2}j -qNG&y1FbxaN)%C4ds?s*xw`l)ET^_pa&^&C<=Rf=<*4h-Ccr^+$f?y_Eu -VZ_@#~hXJ}NfX0vV@bstA#t7F%X|Iv10XpzV4~c~wK*<2odAt}pfGc{0#DFXk#(J93*MARD -v)Tl3BcJEp}8fx1C!SWjPyFsG~ZDupvprYO623MiTgzNp2GQd -qL5*n(5eONulJBByadfIWRG%3!Fztwv5UULP(Z;StK8%dEA)$Z!U5kK5P1-r<$+~ug?^P#cuC!ZU52= -^$+%~HwmbvlTMHSyc>m>nz4QHw~ -mJ7{ChL!2qVYTWbf%TR7zs5gs+COLKXqDY2Ocs1j=;hgWE*=X2m?-wF-w({!{8<9GII#MtYK&p6+ -edW!0n!&yaT7s!!dlgWK?&Z -YQ-|n#aShh`YDk(Az*>Nwv0k2Hag>(g8O7g;RwWZ;1U}a -yJ6*2&2-5PR><$m$AX>v3iw}d}SG0oMC>M^sFBD)6LnAUPLy)PLoweV5y3RbYkd&TPMx9h)1~~doA=P+h(7Mlp-w@< -3<5XaVlwI+$5MG$bPL&{A_5Cl8#_eJZ4i%#Q_ty)rS^Tbj;P9)6@mo%H?%OB4|d?r!96~dmpl?Z3mI3 -)PPKOZINX^(BJxLKv%TH>;!^?oc+YZ{bj`&Uampqpyge0jLU@*ja%g$A=ApFLyJZut65E2YM6M=@UUZM;wJrU9)2 -Al;siuoY!TD?4|Kp3~a@H#^EC8W<^K7r%Lw=D(4XRc@kTAyKAYV)9aZc>MjP_WmXy7cqqppOEV!aurL -!BI1KEmt^lp)ZU@E%@`a-)ZAQmHJ+QzOP{czMeS)nF=tU6w+^d`Llga#O17$fJ?kB -Ej6Bo&fHj7k=_TzU*)q@FK$`#hWu*lz7RLu%w+~h7*NBUGix-y} -tkrlG>6adEKMmU=1(`yh(PQCcpkr8wot?tHA)8k7* -jKz3tUv8+^HA4p;D@Iv1VIF~lKXew`(qsn*ux#ZtbQ3SDeK;Ve-!SLCe -;`XPLWsEj)ilzz$6c^qE9)zpBq;VfAlzWqzV6rJ0(n1B)&u{G;p?)SN!!;*x2-3-QYSO+5T?hrWo{0< -T{?rVOMnNSVL%e9ps@^i%dC5OEYMuQ-@>KKv<8&K4Mh!KRMy(SZ=Yfhpgnt_vjB8Ap7B32> -P-UezsKRUumvh#Sa#q$TS-M`Y7CSgP(djIvbasHJM_iiPSwFjN(eC)<;&wdbE0s{V!mM*-z~<@Z+UA^DdjFzspNlv3p3$0)`ERp~Y>75yAig6YJOXrtWNV*FODTFK; -Ca_a*N1W}?@U<3*a7n(869`H?}FTXD7UaxG1CAB&Y21}h9wmb@glAbzK`#OxT@JjF{Lp3J^KVYshUiX -C}SD-!$Ta-~(gh@KxVqUVRO*gZ`?7QSSm4jrBw<KRCKrmif!{|Y7;HwU^hmaO531sS?Urqx7S -c2WhvQIyzI7#AzK=Z46NUPtzj-VS+1rmz(2H1a-Ef`&j3M8BFjNK5%U=0d@o6kK#GkPhN(gykegTs6( -k#`k(=JbDN3$N0+QF>)tKTE3tHbo*SzR0LF=8I3UYDV;zBbB;6gU(eV1nb)4Peximw^31Z>%`M5^s|e -4yQ;gcX_#T!#J?)mr&^ZDob|#!zRx)iuN&3|08eHT;AMAxl2g--%N~{L7G3ZIf%Q=*;+gTC6aEv}rZ1 -eK$!6;~=QqA|E&07CupIROQ=Cc`zMEKgNI4C%Ix-$L;|^G2m*6L>p|hM+mNg6 -c_I+8iWzO_oUx~Rwj_}`WyVKHiF`aLqn+b{S2TiTuvzr>#bpF+LQ>L8SR;jiwPO&I69ko56D`CP5FixuG@#d?C=&dUnZ3g&KXb?KO`+hlItICm -6wZ1#ClSakTW`M@akpfo_4ghqL3v@Ettr$EAxw|c+<;cdA(%6$!m~nD@k0_iNH*XmSl{aMF&F#%_Hoe -+C8@C5Z+c<9T+uT73Ujs*kO!C*ak)F^wmh!z4M~7fGbtz7n>npnnx-_%2^}d@dpzm!;Ya1tXt@mZkEJ -c+s@pesVL6&)YyvWfnw!Zw8epC4UZ6P| -Algz>nw05E+A9QzBeXnPx&hQ5l4i3E^G=xLDJ&!g-ScnL`J>%&xwK>)c}ssml%-6A8`U*%;d+^OBv{< -Fl2<0kL{t9(<_FDK>MGH&P&CjbRm=OCD{Dw)b9-vQ48jF*^D}s7}10GKcbT5luD`-R5GhxY>cP#zxQV -@tCvhjDRHC}3|xf3OgXi^EajSX<$9!?R1G*lNoR;&A9#==Sc^~73W4?~2xuY_&;*#%e3rQ{1qmq2sHD -})j0OgS0xT`|LljkM`}~$VM?0+ef9^&^Dues5YQ@Mj>g=Vqaz_)8Iw_LVKA -t$V+3-w}VbR=&J?5iRN9!eZKrJ`3F1Fs8VRpf^k6CN~_tH_tpWZivHS`KV172`5r)N9wDYeNN!guD90QR(r&{T4{at((r?#x3!_P -QU$Yn*Lt$SB*mYT?5KGd1L;T)9ep%8gQNAzViZ)?^)ZnE_Ps%4sPn-Wn|5tU7q;YnD~vq-9l&6xa$m9O0y8RrO{kBL) -#|fZJAzVT!js6rX!2CihSb??Ft)VrW-&@X!@@RTUMwUDeyvKnda5*uiAj9Q%DTY>54e+Erl~$uahmWR -fu^L%8Wu!mf&1Dx<**w#6~T1=kFh*iQ|iWA{@dC=8+z)WNqkBPg15JL)ofuxGKkda>7xE|%!U=Fnm(4 -q|DvKypQaw3IJdEagtVy6KXSFKfEAhRMO -EQH%k3vLmnHa%fLYpRG*&)byDyB1&;*eS%#8pKNop`tC72C)PoR&g!m_QxSEb_Fn}}6&FwwpCf5{F{i`j2bXUVqmxDO(f|TA!?jTU9sZ^^Ljf-~Ub-0lbeRy -5(V5&wcXnnPpTG1>^SYN#c1J5s})>l9}`S8b9RB(ZpLA-7{lot)OUe<2fYjhv;P(v#0u4YK}b7mCApJ -qlT3+cm2G96XW`vl9Yx;d2wXv4k_Qv%xIb1g#&iz>~upqtt|bW=O}!+L{aM-#?T(?L9ic5f*Nb1m3AY -`=m=Nwm~&sE98_OAhT)uN|TtWJg0NtXGI%NpFK=~_@4_eFM6K{(teql`0rH6%wv?w}G-Xa=CqZxcQQ@ -%nCsfuM*iD~!!uQ-3>&7HN~_VR0AgRDAedvcD -HV2glqnVFqNY@|xKXB5O$V@@NlU5(z}GCP=y+nL;IKE{(HK(%mca1x -V)>Mfe>T_y@HPvYHNElO5s?&|BM$(0DOhuQ}pm1JWWK_zaMp{x~w>3+u{_Ay1DvdOfmQ;uc&63J>o|; -i5=9EM;OMB>a{}wf)datRdkoT&T1IYBZ-B9RMZoji0;;DVq5#f9RCBb8HW!4CL(qZnWq~6&yAEZSm{o -;d0uCylc0M0`|IQksWrW@jL9*hH??4)U9b%Qby&o}kA0>z3)9AmV%JZ%K&1uwd47tn$WbP4VSTB22$D -aVy5BNv$6dH0YXQp+cNQ>l&6Cn`m)d;ldpc{>HBl>nQii&AJ&g1XSGYK|5(L*WOo9%X`7wEWKQ!h6WP -D+>RscmNqCqm2(Q1VWo1llN$-_~~V8eWV64;P$7`??DB&;fvUAB)?m(Gk4*ZA{xujQTJ@SvZXW+ZRP} -Mrg#9$VrpuHUnw(0Dh{g3L@fhM@NU{S3aylDrd9aDY`R(R6n~xGsjp#eMeh|@6AvIKqLx({AlrYAmQ^ -&aMy4x#o2iwG0WBeZoE+RrTaNRktuxke;Tr{KjZr6WB8Weuwo_jTPUg#XPG&0aWH!NH2mE~ie-j}7&h -^fbE+Bqft!+;VHnRA&5lW;|>4i9YpzBt1;)(NV+$TpR7Uf+$t#XqEctnXACeb}V-6y@KN(Rw4YTo7K$ ->}asU~hYEgaWI)s;QKnuZ~n;pS@CnJ*Ti`-VoSr}qq(?0xkgs;7}x6Y6Pw -by7WD+E;3IdgLBC6|^ZnQ+xt+&s5oEHKZt|;gZKsn^D{Ry8HRBvPh1{v= -6Lzoa9nNji#{p#P@fe0MtHG!d4v~>UySx* -u?=(3*I0l$n*g<_KsJdHb7FA1r~xP^)sJ*yK~0wLqq<<12ht9ASnrcYyh3pB2=o9NDntO{!!?l0GZw(96pQH8Xpz_hWe@yyj<^I35~_gQf|p_YAuJ}W>adk))8Qn?oVN+6<1b6SA}InrrG=LoJe+G)irnyoZG{+ZK?cc+b -TT7fh9e|1_hJSYAorxgkM(4AIv4t%52ir1%(a$4~>s&2pgHK!GyU)G#fJc{~S{*_KEKyt)-zcs>Pg(u -lq7)7#Rs-~fBq#D|l9xF^qj}@jUWooq9{UbeAVEO4uD$+Ew=h0mCW4SagR}IEK#M8~Oz&#!oyd!)R-0 -kSt%<4V~I+`zPUMhmW)6}Z>{)on}STv|ry9U0&OGSK?mkN`vRz(tCDoT@HD(Ijmyi^?0Jyg){kMdCA9 -jR7rPk5;KYeKC`dZ?hqd9UzLfel5y>eNVx5qj0_qdZhB(&no-Hem?6(@S{)z1tLV#wd5(^?6m?B^DlX=s2lcLQT5GUD;7}3_jRuoi3zTx-9x=rq?p2>hMm9CX$4 -NG{}ZPbrw0;FD{!GsIIS4=AeL}ifs4|x2ZrK5eBEgU=1DlMI5BXg(~2KlNDS|lPAjmM(M~Hq`Tf_OR# -3+FO-?IDFybqnR=6kr1E&?Vd79ITb-(|=JYs#X{vcRYj39 -gVAzshBC#Spm|By-x~gvS>SyQD^M23n+u4ya1v*^?i}b1q>IyQQW=4Q$^0xsBH!BWLAhyW<|4;c?$kw -@Hg-KPNoF@egJ=)MQ8Fma(L|uXfxFwilrFn1wuxcNKX7VDQ0hgp!OlT&lq$mj-Lhrh~E!GR3{uJAz+q>)_ -ww_H}am!UczvTdO>1UhOm3@OJ0Y#kz%$Fqs&Ct9=PcYtOJze^jxFp&?8e{sLXym7k#F0(f6Q$Ibqilf -7F0a-!!g^vD>$KKIE%j3^Rp>$6F47_{(ZdfK-Vw4?t=K2JA$^ylg5R~?7H)ahc6#Ve8@=RzA~bSaLPg -$LynH2ihT#ds)>U(j+_RjD80Rrv^C;y1dqk|Aps0wknUsZ2G7+`wW2SMp`6K1g{n{CXYPX8Sd -VXywtCwim1sCJO=!|zxxdzWf_!=6`!SzY`OS;G%rP3 -nE&x4RU6N8L6gNa(2l6K`a%U9M#RQgQ>vXMezknT#HNtl%k?4ouw9yNzKN54^f#BmPKe(NIjMOi%k{A -m#@#Pw%SRY7}jnGu$sjAMyA?wOEx5cf5rE0@Uv=-$R5l{r;AF2i@2hG6-pXO-O~U8Tl>`X@FfKhlwi5_kY`HuyHca}O={Z93e8MF<*iFQBbHtN+BV8ZrB`gDSgXi~`nO^fj@X7 -P{)TOnt-BK2CO4Li`hEkwi^osh#qtx?^(>KPRc)@f_7C8#hZ-pborjj2M@%!BX`WBL*IqAfw3&o_*_m -COXZg_tKBxF0<9yyASjflIsh^)iX2y5TGfv$feZp~phx|68ne=ikH{)WzWCid>IqWU6!B -P;i24BVNy^d(ZSIuD>3a>M3pJ}BMh?@-@r>y(}_WF`Vu!;|`co`^mhDI1PV>@6}RdU?}(vk;20TC~?{-*zt8F}-n;-?rUB&0>lSK*y^WpnYi#CtQoKU(}u;vL6VsVn|b;vGMy -rYtEACf@NHqO!2~>BKu;R#N5^|1k01hVN60_Zao}Aiif6zk=_n@Qzn+)b*?Uta`jg+Pq3h#SSjTm%0a -*)9bgjNe2-Ya_oSLtmaIT)#M@#A}?Q{F`8zyl@IbM2Glu)FDRD@ze}fMjZ}<05-9#1t%NGY?jdf-(1Z -+T88R3{75UM-l)<2RTB%fXD=qq^p -?t%wkmtbBc;lJm#eK&ywrr>X5!Ou_PJQIG!EEx)57Tjely55_=oA-v!Qal6T3i7g>{5&OKCOUAyr%3r -gD2D?s78jPR)9AoSKSTeIQ>{TH8`om$lVU4i!LkEvNXK>G!IB-87KLTbc&agkL~O=Jq>O2B#o&lWU1F -sJt)8{ON*O?ol$ -)wJ>x8Istt+7K250?47i7xp2SaWEoEh81E5ei}rY$HPd&(uzB^^K%80Bn-;cgLsv!sw&vnOVaTp94@pDDu!BbUw0SYOuMMAy -F-qLKQ{)=oFm+Di<`8gyNsS1$9eubJ-cbh>%jt!Tuu~Fu@gTVBGFHX*h7j*yRJT>>4~MQ%iibsSo>g5 -eUV*Xx(fBBt5E3i%Ec^x+rZ-~h4L+Kyzz`bg5b(|7ymG{jhpE+ND9axGAD`R -FL!l(6j?!+&MD62h)Lp3KC@gWS_g}9oN4PkqaX3vgK8-1tW+RU*coU7OF>aq7Y`i0bk_dv;tEpT42%I -%qB7BNh?+m_KIQQz(TC#88<3pI=oXRgHyS&1gj1p?}G*_u3Zk5qfQwRT9VjYnNaHEWYjK254U(e6@hN -_y0Fg5Hv-RfDe6Zql_#`yxZ&;}nM3kGm@YmreM;*n1a%sH*LMd=CsTDms{2_)1h%3ZJ0{j^-esK#(X3 -nh!)71VW%;2DK}K4wU1F>8)GZk!_=G{r>se|`2jXE-2`b-Um1@Av= -R18ep^`?=QMYwx}G)wBy -p@b?Py-`RgFKR(&PTJYJ~oO1M!+gJc;cdq!5{Zm#xpPQKuZ>X_v>HMqidC&BFm-3k00lG*-z*yIbM`J -So1byw6GoC&V(L>8fj#Fs2$4(#ciK_jZ)Ta+;Cz5PubBTp8Z-%J%nA+{iDB!N^-w~WI4+cs$wqh#y@c -16j`OeT>CcH4ug%5=6qODq>KGZE1#7R9|j;+?MH&25=oQ%~xC^;diY_1a&78MXJSbUhF`p?)7~kN -nj%p!s0UUxk{46N(gi(f$u9<*%R{cOX=+lRqwv=kAY7aK98kP0{%W!1YV@*rZZEo6QEV*PNZdmO=A}z;YZ*)>VP__=#uUzJkrJ9!L@7cMEHChE`P^ZY1=MN=MR@Uek;O~%chG_obnH%{NG0 -teN)`8k -x{sComJ3<)l1O>Q35dirHb<29DNtb=Mt!o%z=JY_6>T0UDXI)>gAqtLEpyc5X_%-aYRow&y03ON7R1{~2iUpiD+im-jg -~L}vl`n2gmBx~lSf%tVi#mmJJhopcHDgJEIFamO%ObuomSRb0N#UupZ@OraoTIEQi!l -oAkyA7Im?5>+Fc5|sjD -Eq##OW~UvY+1zLB;pQWv<~a7X;L;*&*#4b6qXhY)@hRet3seTu&+YW-nx;Z?TGVDBk<^R9>`pHNjI%C -YpthJXe=$zsuyEO0klwJ*u?L{~zr(+52aj{EaY=&GnkaUbx1;R(*0&4+r1;gUZt(yLmJ<*ebZk2F+>9 ->l3{M^a4HX;#6>bc+v<$GP+Q|E8GgsTB}**i#Wo=|)Ff36h^FjJ}TC!x5|AVS!_ -3M=epqXyTpPZ1Wpk8~|J+dgy_)@i@n0}r}(O4DnVn~hIswL`h6-`qt)AdM(tPY69xlz&*^DO*rj;R(5 -9-IoU|^wd+lrT*$bp~^#h3y##2Yrhs0%0u)*$pQp&0~Uok8Mo=6@V0Hb$-{1d#>DL&)!&h$aa-%)iWv ->=kD(~arFc4q~~tqw(yRLNycsWdxw0STo!4-o! -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 -h{*BK=hf94CGv -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 -^tb3(M}k0tkp%Y;{M1#)P7oXUt(oNnn6 -HbYeTw>BoemT0l<@>>j6~~Y#%iePM-K{TbLcOh+$Xb1Y%CkVLN)dwY&eT!Q6inm>>d`wBG`BqDO{rIZ -v>l1epRROQm@AE;7CtNE&Vs`((DR7%fIerZY!gaNko?~@2ygvi&pB~vlXM805-G}wIBVDA4w2%(cp -m+&Ut -+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 -LKh^Zv88m|5DEhW?GqG5+<2{*CGXa9hjxZ>? -naLYrk$ZgOUtB_bo$%KoCz=nfaoSQ^q!w53H;iD72o;i1gPqA)#SYye73LzW!Q$|M%4i0Kw3>O>Ziot -v+|-5oXYo~Sq)gVVCJ8FMj||3nfe3Iqv>B!nchC|#n>O8+rL%?J^Ks&2NZHkKG2mmHoDBcdPhh_{ -2uARdDKr};IO~}mz?d(IG1k$|w>nGZ>C+Co$na1`DTI4#XS*4&g0d%pmlpBSnLcGtC7Lzl_YMCdvwOZ ->=39IfIh#MY}z}THA!|_q^u(u{Uk_h88!L7u>>;wHI_gS7@u)ELr?8ck>^v_Pbi3;_v=WUsDs8;e(dK -w=5K+_T@WneBFLsP;i4`sa}JT1*CYNU@>GL@WQFqVrnOfvp{lrU-M;*&i(Z-wbU>&Y+=hQ;md#Me0 -Qm(sIvlrynpJ1^HHqDtMlSx@Wf>b7D&bd#ayW;wOjEZlepnqY$%`$INCgqbaN``FgVWpRVLoyrf8JC+ -%ZDY1I)w+;y{8;eRM2lR>Psr^@?j49DUe>%5AyhKSy0O{z{0t;E#-5%|(mP`hp|mu8j-L2P!7SH0)`F -927PVcZF>0Q9saA`-+>;SoQ+`RQ)=W%el3AkclQuqGu9aAi`V}c)O%j0Ix~Ywkq$qQXf^Bls!}fol{~ -tf)W5eC1PN(tkyFM)T+I81EH}LoP19AKB^Z)+`{B+NE=C@z%{C=eRj_-j~Uh-5-tnK5h2Cj(2CxXy@g ->`oyCa=326tk6LwpRSwX#8$g;=(j4>G~;dI3IG80&wrCxDQj_!0{i#ac*^2u- -#>6f*&XMCxiB-YXTM1vR;JidJmn!}a#lJ*JXEgy%Ui?*gU$5ZvoRZ%Qiu*PNzmF9E&lP`vAGs{lGW*X -ye$76ze?YU(e;l>{k*E31OxeHr&9?tty#L7O|8*Wvp8wlfA&}$)553_z>X2tGUAVNT;{3O-Wpn-Wvbp -4$6XNb}^WtJ*Zlv%P8;))WXeQYu*JRVIm|Jf!e7Iu1PT>zcd+OOP?DVtJv(uVSfAM-@n?M7|grhT$2C -$T)Sx4`U&yW7u%pwWQ1S1JT35F63CI})3An+&fC13=#7tHJofkbeM-~_>Of -+GZ<5F8}fN3fe<2SGW(I|N$@HWI8OSfhkrMX-#Zn7~GmO)!gK5K&V|L^nfKfiX)%%jocsXS_YGfA$43BZqkC%y~tdZe>9{g9L98v^wisK -8+2^#IeaLy__frSO5K<^gR-r=+drqsH?|bzj2$-0kK4hZOYxxovd4o*EJ4hz{B{DHjqd>)|9Dgz~7aOc=oQ_VMD2Z;u9qG?%!X3{74TEUimo?+QrWyB*&A7kO -jQj3p+>bQlE;ZqfvALGq4YPGjqQX0+99pYM(=psDtiJoF>)1v^{d;+ij%9e&zX#dKzfb-9!L{`6SO31 -??-X8N|9;{Xo%jw~eUJX9j@_%t4~hW;82Srfsh*^3`WZNIAR9Ms97|44W|^6p%x<^K6;)hJ|HW+0nl< -c&7hYg*zWF9AFE3}GfBreUcI_Hdnr^WOOfR_PnI@tS?<@9Y#$EI09}uqx_}RK*-@5tp7cD}7vwUCO0l -x1NdGDfk1HGK3xb!}MKHjAR`}XY(Yh){6#f}GZdNcRNm(wFw`P`dU~DwUWD-b4y30a* -rmMJ$^N+MrVj6vejWMap)f^q`tp1BB@uq#z5_dDhf5;;uaUp}PWf~0I)F`CxB_%E$n -zOQcIMWliEYZIr(P=LB@N}t}n-r3Xfwx{+TAQIA1K87RzSkmFFEJxrvY{S*kJKD$XJ3t{;ptuZmFFr9 -OR1P5CYxf^`uGh=^L|6d1p9vqyYbm-|10eiA+AwKfZQX(5auNOj=T|4cUt1@<5uVDo>Zw{TyhZsV2}$ --wc%f(Hi0m!W7r4pZ%&<&(WN%j2qrB*ZXYpuafgT$0B+wNzxuaAj#CB1OZ>B*BPzn>mA^d&M=?u)&D -Jm*_e_+n?j_oq!xVMTny`&%a?y*F1P{UXl)b?MS3Cb;-dj4kfbgZugP;LKA2)&i(ud=xOs<%BXai*jm -trKnGzJ}fvmm<=90n2j1WiiL-VvzV9|QFaq1Okhb#No?xWscgoK8EkRdST=X=T$YuU#pcbM$E;SX=sy -Yz3)y=6X!ghYlTLpM3HOJ9_je -`|`^#+1b-a*x{czJ9+Y?;ESrNDt6)Q5A55^oYmCSuuGRN30}eDF^I;81jf9HX$-3vYfXI9h8lZ2YQO> -fCN_!>Vbk~owvgY?*6`JAD}R-J!VjtaDeFk_dr|zs6n_-OkE8fgDgIoFzks)A%PIa-6#o^9UrzBqrue -5c@%vJIGsVA`;@c?xN{as?#otcx4^jLh6#pxV|1HI@qWEVi{zXmvL46rZps7zDO}|&!7~8ax=HxHZm{ -v)R@Z?DTe41Asp#Gi6 -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 -_*uVOLc+=ebSc2H_)Dmh9>%%=AOqqUw$e2 -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 -BPHNI383Nxi -#Gv0zdO{pWB+xW%3yOsVtQFOVpR(^E&TJ_Z`dX%=sb3SxG!zs+RD38S+TanMf<++4fbnot`(R8LLVp* -0gw6XU?D@-StDHJAwBb9iGFvfPD<|e4~SP3{rWx{}Is`byeo5-6N}+&yu1B+;dsC`w_Jr^&D7}$^wfe -);5RyzKuH%e-3%g8+TT3md(ql9HJ?eTfob+xd@l1v@&I+7iGc@jF*|=e;Q@-y5-t@6c4t@>1@Ld7y2zPr=_Yn45wNHXDvRXgd6 -Wa+tzc~eJJcd)j!>Ts`PJv~jmm2SPmjS4jXiWgXJaOpr+p?*b!aLJ5-)7P@f{=USc1Ck1YfzHB&$KP3 -qRk#&}Z;?s%0kZ0{IA}JAFo0P=I{IV=kMe8NR_y%t!Kg67?Mf4yc=paQhNyUt?f}bf1p2^&2yM$HA|= -kxrFa&wrr2p($z*dg|AgsV@)hZ6W2My!7r4LnT!Xgs|K<#fudsUP`SYUhr4v=DZ2@3?;Rx@M@ -su$`r{_`7^HcnjcyH^I5M8u1hMopHNM>8O5Zffr9-s0Qhs6Y1>PtSMb;&jH_Dq~B+2_GxxZl!*Yo&?& -Lgtm)J_hvQtCiq}T^Hd+-E&FmMPc_LHeW$X9W~ -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 -uCyS!Wlc0! -v}Go1>Z0`lNi4-#`Mn~iS>gLlkj?yPc)q8?3VUWmV#hDf2=}i<`eta1ujP@1eD@=X`7N?k+)^zS7iux -VThQ<8sBg9#bp)>UhsAvF&%&;_@P1928XPiM2GH(qb4ixA<+;q)n8t$lw61*Vo+T{s3&eTuMQY%r!Mc -16p3Ty%MGcEsfaY~lJJNDJ3*>0W_w{NZ{{?SO1D;2;wXOezx*Ih7ON -|;GMnjx;X6RW%f>ccR`Da6g#2?~KEI$X&glg-+w;uuK_h0(izW?~$$e&~7!wvbM`7{IzeC)kTN9PQU& -JR-`TMX=D-@# -J&X4H*;Jnb`M+M2F+olWG&+9sI%RftL@z{?<@D`w}ZZuTGE1&!f|Y{H%>5KRc5r+M0hP`7!HkP15 -Mn#>doVG8}eYjQOHm~wS$l!3*=)ndacgjeSPDU1+wDl$LPLz>@XLT~7vnA4*iMB+FBQxLEY>8srS8Q(uZDdwU>+2X@pBFx -Xy;rOgw8Nj3sPE)DTAwg;5sj~k`&o<3T1P(AKXyCQuSZkOl-XmE)>06xFT6@PwHY#dDTQwQB~LsoeilazZ5Ea%oxwm%wFk3FBaf-jqy}_l10kaZU$h@tK`txsK0h+Hxcr2l-Rm6)ce<|j9IBCwuLDo -d`c$P_GzU#{0iMd()+aZx|4Zb;He=LAUWzk&oih8Mv`La)gYipmz>-s`;T{mm|qWKr*8zOoTp0_HcY;;Jvd4`wc- -(301{JUSrzXkuxzyEpup&illuS>_juW9_NkKvzJ$3I!ezgMya|L%z4pOVOGcV2;igX8=|yB6l3Il{l2 -H2&S7Cy4{#>sAHM1Nmm1;UT*CcaXLUu`57T&90Uj!I4^H6WAFq;!uWLL!Jb{P6<%Q_ -_UXF)<3IEPr1^>2XNBH-N|K;ER82@7DOYs;3ZL!59RjvF3a875no73UD|AYS}vy5v@zd#S-o;jL-`w0 -W<$kpz@kia@-Y4^|IJ@x01-))v=aDTg0;`=vo7)d6+Ue4Bl?@pSlN^{kvX^Y%QNBcX_Tpm7$8S?QS?k -OLwZ#LuEjqeS4iTR_Nznzny#bL?tYvj`6ek&`y2XKeVX{{S;ra6apGb{F{8f&^}50hyLyu2=x;O#w(E -peN~{6DWr;{K6hCp?4KY|NqjvjgaYhd -h~w6)#=gwIz77aD)s2|SD{DEnjAOB$T2-%?e9XI^i#F#`OCo4mmjM~&6*y>=F#g>Z21P=yRaU`j(?60 -@qIu1Zp7kYUFuJR?|YdxtXQW@|0YFr=`CYKm+rrUE}b-dc)HY}^(87=8|FnD)=0W!;JOsB2wiG+Yr5o -)(WOS6F1hu(WMIv-*5V$}X8427YA^JN*4FOg^Lhl%3p9D6@g?YC6{RyvX3Hlav)!aa2BAaFX4i&JO@} -&TbtsAX==-v99kOs8YPo_AZ94xqwp|9HbN_yoI`_r%5uMv+x;mY^>wL7%-90pMjGE+olP33@V&tBlul -9E#PWq|Z_55XE>C2DRyG@$j#OBfKU2ORV-Ma~T_ZgYDUpaB~&V7Y8>@#vi?`Yj^Snm>bde$@OW2>2kW1!F7r9{qg;XE+t)7mqfXg -=g|Any5##4bZLC9ZbX+hXtKN^MwaRMYJV5vq@Su?&tC?XzWi8S+Mww|Y#zNX#g=c-y(3+UowrLmG(Of -~jvi}R-@0=2oWy#164YQ1uB&cR1JrL?CnIJ(2OeI-g59#(%w<^O>wT-KKRW+msfOZ2KVzpWq`5vf(pB -Q>oY_|-_I{g7d)^FC|7$~aBu3zAE0 -RBXkbFnj1XrpzXU+Ab?+k{Y6_Q_W|7W3dumRQfnG)|0Scfz;j#aVVlqx-}w%4taeCyr|ZRdNiQNBfdt -gbgaztw%MnBQt%Z>pj(q6VaQn#zK(EoYpxHWc-pjHdUsuDnR=iH6SkyR#=CmJT)oQd -Gil$34T^uZv7#*xX}*}J^0{8`C9%NgQKv6l&uv@qMOrgVdJ0=adP?&qV7m(I!N0m|)Zjtr={T)s3R(@ -4?b7mW)}{R>(yqCN=8*LjxAd~6a5`G2w+8hyOF5kW^xZvI>z{lI@eb`JA&dS=&}?Z-u{55yaf+w9F+n -OeH_u25=N&d!{EcMmHQ8`VmI1dW6SOCwetiq?Z)9{fSM++BE6N9C_9WTiE6E4aU9EhaI{SZ!d{`p#G4 -BfUQA+X=rqvbXBjxWUACIx-4@Ev=#-;Q&_Rv|{gH;W3yT$kJqdoMa`LMcmkgO>FxG9)N42D -Pv!AApZ4-Yx$hx9r&xD2>J_?`6%!5)!hxwl+3X3}Xn%h;x@0}-NHV9FXPd;D_4u!^=JU36b=V(goy)} -e>%1)juICMs>(jt(g8kw9V?n1%Q5P@Lko|i3-O%=@Aln>|=A1OjOYQN|J -UT-%+jX4JWl#6h+*#T;_c6!}$v^I0`Dgha)B*F%#F}C9pR2`wK<;bAysDCYVjU1YTN3zQAcqkadxgZVC3s<@?}g{^_x^J)JU7|8g2`m4;eTc5FP5QVYD*zU&6CK`9dTr+`7$y -zTPH(5fA?}Sq&82GA@jHlHOG^o_fjJ=^e?Fq8H%^QV^8bWJ>SOPxo_|JcI>>Y$=YSu&C%6)uz|4kBwr -*~phIngAzH51xIBR^GTJ|CD-b&otq4DNbzd{?n|T}ZY+o_%i^FY3xDE$&b$CQqhj9CL>fLuv4dZWg_| -$N$A5ZhHx5UA{4Y+$Px8|GKp0u^7Lo3=q51pxNVhal;Z%#XCn*6TtWzZhc{-eF!e9y%>J};1%E|!GH0hZG0SZ7n#q}xS>T#z2BYCz9SuePp6d8*mfyY`}ibv7X2;Sx=veUy?d9D750PQQ)vG -!g{9K^$F~tXC4H67nPIhS#%^ivmlAzS^P16y40W5;>^VF?*ZK^_+uxVWKJQz7#ng_6^G=p$o|fl($m2 -ez77sDD+-UlYdL0zaT~m)dQY%p_dig6b5!}}=<;Lxr$2du*5_z(M|Ekj(vkN|;Wrv+A9^)#T%I_eD}H~e%==3dbp53SkNSQVN$~BfBl`=MUJTV{vdBCy -YBvbpDx|5uM!o8?OuDkDoNP(#l}EmJ)`>WMrZIF%h@lhh(;lW1txLQTo!-ddd$e;ptpc90_1?;&>rMO -JSakKK{yeHP<)eCs>#>nV>had`iS>x~PjzE^sDsypm`|(20#l?8StxTf)LG4aXxc}D&IJJcun%31y1W -m&L+c(FpzcKTW3$!fE6wApbg&Nl?1}RWNl(8*bNICS_wHZPMl_JX`wZ>dhsF0d$M;CwKfx~>k4tpxfO -PyWjK}ZEXmnv(ghsT^aS-$g&k+n=LVOxi*Q@1skq&?TnxJ^=F2#9#vWxa+=rFQ^gVZm_ZI5My&n1Cfp -?&CSy^&e6CDGgu+F!{5yRjYZj9tna$ym632d&Zk#<9@$S7l*i%u+^MC2R_RHfzsj?HMq=W1+&ma@Gj- -Kk)j%ci?@{D5qOik{bA$rmJ5+`eD`M*$XX4?$+1C^!Kbs{C%B*2BUwmf!38VIv>$Lih9wSVOnFheyUh -ww%EXeqx*~6!t2aJa`uP^`k4b9`f06jW?xaPuJvo{iSrGtXCLnWXcu#4cfq%F7{pq%&rapGTC7FuW=; -EO4o{r5$^E*u$pq_IqNn;*d;GQ3q`Nyb9CJSqXS~q!#5HX3_jiiF&z+#vC}@=q+-Yrf5VSh4qm@geRX -XZJd*3~v(Q0X;(pF-~8tK#M^#SY{t+jT1MBOmi8th7gSc9EwkIaE4x-pGzQ&AR+q1#oh=?2}bfY}mfO -?Q}1gsT*XZg&FS0~*~X+hb0&=Pz+u9ZxrO4}M+hK92|f2K1d`mb2-&voUtd09?F>u0XfgF6B&M*{v>=3+Cv=6jLEY0Ctr-Msp)! -v6^_&)(!qu`N7mq5mgDG$wSY{0h)qu4w33p%>zPKm6WjHh#dZ#tG=fsP|OIu^yyu|P-1g;${C?}(1HK -Mr8g9yxkC=Ecx))+P+Ax8j1CWk|QvGk{sDHOdnHqT3Ay*VlIm7 -zIZCMoyPdNPl4y)q -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 -C35b(&7VI@?0qLq6 -c@`uY=kW6b}_}U8>`JK_@l+pbk1S37y=%X`(cA8i(#pM+y)^i)9n3EN(!}IG#agH3?wVvqsR^dJZ*}x -khPDfWC)9ra!e^%8Cg+&9Y@m$ -mn-VLxPFO#O7{x}t90$5g&QR(KzvS&*&zT+N?=+|$_qh7&xLwFZnCs_C4Mdpr(GT1_wawAK(+{kL;^{)}UrO3NdqhqN?$E -k7X_`rqG_E?HF3SkK|fY(^DyF -#|SZJ#2=MoeclsB+-`&-=k+0w;36(Fq}6*1HJ7ib?9ve;)y&_x3>YqjXEo^9gKN@R?G)Km4b -cATyA;cIr!p%u%}7^#BiEnmC>nhD$iSM0p`=zo5P|dJp=@SPZQ42r9Ua$=*L$K}ILG&K%NQ`vq;ty3kL;DDk@q)k*+F{q0P30(El*RclFN$-30;{uy- -;v1p-s*uzvx(-h=Lk(4FE|I9cOAdEcqoAt(%v~-p82_=onGpnARhtoQlMMr=jLvuIA=Lx9QbN`oKMra -Di&V1N*+HA?4Vf5t2>icvoXA_qu7bZW>NxQn?&aV()@4!u0FxHMJ9hxly?yHAUM)Kt8X)|j#8`daqyk -Llaj@|L96d;^e!QihvxHk9XA$xEh^s&$SkqqSlckmAkL))tZewqjdWHBy$_!wEcEc@Fb`eFzb5CeG6; -PBp6A_@Z?~Av5navKtrZVt!6&YP&cfduo^9x^Kz(Rmqv73#{1a=L@Az)`ukd4!-O7B;q{CT!%~k>RrH -AbR!IYVH4sZ0jC)c-fd7oI*G4gp0X?|rP==h2naOEmBh^<6@cd_xaVEOYtQ~1nT?QGk07oGbnXd`Rq- -imK}d8Jrd+U4Ya^JYG$I)SI9{-EQiAIUsF9}%$@Pb=m7$e-qLa}&mK(9SdK3Cox+Hg}PHQJ$UQ?}MiC -e0p8r^7eFO{w|HPmul@#Yj`cW_WZE6o}9ivsP)f<_23lXc%P2M{f%_KGxULCUi18c9WKQGTeNZz_wch -f39eg%6Xpet2@IGbanoZaF|L&$e8xSBd6^iW*3R{bJ+E0wQLF6hQ|_nO#eSXIbWGDJCD-X=njSN49l; -B_a_-T}3B#kl(~=aG;i -sJ9*Rj&3R(?L^v_;NAv6i75FkYRT_)NuHtT>s^B^2LCrm0V$Vj4@M=LFriaGsUO^JTv8=T<(B+Q{#{e -9Wr_He@r~S?$EnA$fE3bm1~k8j&5&_ZOIusa6*KzE|`i>c?C7`&^SR>bW%XJTV?N_*z}YvvgrpVv_Pc8PewSwTJfmE2YgjEeUKP@q8 -_G758ENR&k}okO7=xcmx0gur1_tStG8C1F$azr%&u7fh0}X|X&_P`Z9auj^f -g>E>h}NC%2G6)u^ck_D#liN)iJa8pw`z8-^4Y6zk_;yPI}H>pe#N-+HP{AQbms!HKX^8)!P~U^PvCk~N*>u|VzpgT -=0N&bc{}9rj9K*@cDAl9U}qU`+fnBQukKimx(!GyNa^T1rK?1mlhnU)?ZF*M_}-t(d}xdL_jy`cN=LBFfI(^JAzg)*Cd -nkyc#)<;QaG&c1^qk1X_Cw=sP$W<#aoXk_&51HTpG8bG4~|EYShk(rE?W8fS)T+ZiB|p2JrJq;Bd!+U -kQFDutO!u?1fr9S0u37AY$3`p#Lan`wZ+_M;<#wa|UUg&CBh|v=R8qwPxgf8urA!vNX6DdBg1{wV5>c -o64LgiLz*oiv{W3DEkvgyI0B@q37keuaiD~;n>Ac?Ri78{~Ug!h9o}jcw8GJ^YD3F8U9v*TYb4Xxq~G0n%_ucC1HC@cI_DKE%$>o+crdVVBt5BpH!jQxEb8WMfg&u(VO -q9sPOqw-2Uzo?dDcG7e{fhZbcEeMds(e94R+Z?mm7Xx?Fy?;zuHW8kIeAiOT+Y*zee?v|4=JcRpm`?M -=))0ba-7YVjEP#J>rN{ZsUIE1Z7I#prJ_@#EHXNsOD88HCOG(QI+eo})bn#o3`Rk44}_pR*)j@i_xWL -)&S8&*=FGx_Oeocg>}ngJg55zqA5&w_CHjv=2fZz9H7z>Mcw0Q9m`w$!H?{Mm)PO8OPh+$>L#xU-xQx -a0Gc;UNr>qytVFE<-tGE^RKjLzZyJ2&weJJf1n1B((?}@ZF{`>kw3sn^L-LC`sU6S`v-n%^F89$|?g()L}DC(I)(j-E -$2T;$APMed372#XgOBIDOXa{PIO;qo7u&HFQZsH`Vu^K+q3EBU2?3Z^WnV>i8ChdhOlr9PIj)FH7YeR -!|1J=G|3muh4!3sG9_Qe~a?7f$z(}_bK4|6s;4muqqk@Q -ef-IUXV`y>dnyhX5`OAIe(st_A-(A3JiV4_reaIg6-;uer%q_YAnBN*zhXLUh;jkJ3Ufg@!PPe_aF_? -u=7W(X-Kv&I-lmvp?m5(q_M5eABfm>KJpYG52YW%H^OZ_@Y!3&n(ASnPZ<=yw@f?3Wv-ZK=H1V#$Ora -5B8&Ih{4?+8q$)n{cq#JU`$tw)8D1+x{j`!eH|GhI*@m)dzm`Unt=}_|;yD`sQjiut{-IvrPWZh|dot -2K&NNRGX?_6u5gpgydJpKt_hIQsYJ6QS) --BR`EaBTnvK({zDV)o{^?C8==;g(TH -fhZG>@cZ$K0mb^nU+b@qWkLs?YHEA8A^hMDIVMz5htn9sGTM({2mB&)44PSKZFv&ucniruXx-_wz{Z@ -qR(mZ2o?M_I^QCIe)*fX|+P{7i#YpR^7fRmQTtzCH2k+k4rRnN3?UHI -PKi|yiDHCk?a;p*{-nNyO-KHS)2vDS>}Fj0pAy1O6TodfC)K;57O~lt(|*%l6G#h)<$?Iv~#pKOSbFD -u}C|&n?>5W4QS`GbnVpR!E*c_u^0 -a0jobd33uoiTI%=Pj(V4sn%Gf=kjv$|aA5)1r3#P^GJEz_raK}+XhK1vhso -SvI>>6r!>ob$!TBL(_&8eHayq>J@yKCnlvDRjM<&H@^*T`d;2kk*3H{Cn7yz@KRDyEcDlKH$D+99_!e -_kAA^kp0M`dC8Bc?O9{Dv?HG1S|laCw?<+qkvn522Ur(pGK%Q+;-=Igo)Roh -xm7T!nT4#pW#<^_TS|qwR%n;*z)X&+YrGHrm<0~PO70)*6OZ>pD2IKpC6V_L{$k(GdYx4}T#*OCHen$ -tFV9pXT?jEIH^r@dPWbm;YnnT6Mg2b5J9%Yv_-nYIMFwW54yU<&L+cy@fZFq#Qh1@#XTF5TDr+N+OQz -0o9_aKemMX&4JqK%*YD$Y0xje~{b;sQa7G_t2GTg-=#?!SoFKPPB%0W_&-QQHqfwuraHXW}pMkMJaV< -)0LML!nq*;qdhvmjHX7IHlru-pfo3!&unKK*H}aM=u%CF$PAqgZiy#Y>Vcv(Y$u -z56uUoaYKdXvuJgrdMRd4HOc5oNm)ZjN${KYBnsD!>ZRxR&}`Zd67e|7rFrMwD0k>1teMK9Jj39h3wbJsg$xYe-d8*i&kb*fWy1Ol@!olc -6{k^MCXtC;O(v$0OrT6_j7(S~GSR%&B4lFr`+AwMUQQ->9=9y!VmX@aHJ6i(EA`jYTE9`q0L|0B0H5H -Dn2W;K2(D>sluwA!Nq}*~z4F(+Ag$QhV@f-t=cG-|8#QPXf&kINH)_&qL^Z0~=j>7JMWA4WJEoOD1 -3bOja|W#x@QP7v)Q=umSHnQ$fFNpT4l0{N76|i)f!Q=fNSG-&b%i%{4}vZsbcs9>6Iq0&d~;cx3NsJ_J+n|cpbps&SY^G(u8l|_1e<;1ANTdU -|mjo;pXuGzup4BY2Sh4@N;M^-0@0h^|P6LePJP&UtQbrCj8IWVB@JQJ4_D}Fw6%n2wLPZ -$W+me9jgouLNGH96h`(11A%w#e_J-_ -pQ-hbXd@|m5z*Y#P?de-wi>$%jNRkj}E@BjDb2hJJipve!sI;_svqr;K2ka8oBu4lFlZ -M3Q2dsiR(ez!YgL0Km8tv~NR_E4KWjpG9a*E^^(qs9BJyGN68II-`m>VH8nqOdS1Mm4M%C40BLYGl~T -T~l$!|IH@G`|pUU;N&@$MW_Aj)wuSo4>6eBFuhh^z-AWe$qAJzB?xZv%{GGk?Eqc*Vs4L6JCJbWWa3U -Jq<&tfScpW)Q)n@`(e*$nB8r(f3E>E2n+6;FBkciGH8$2bTIs&rTa_Rca6!)2D8zr?7K5 -GQTP?3jjs)F?n9sz1k8_8tn|4D)O&u$tM4ml^n5|XFd`Bp3V}XC-F=_+b^ -Ik)Bb!BZHCtF`Voj7O2iV -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 -K{!9;nU)!-l+4(S*TLW>9HmGGuV{K5cMjhGvlv -s8F@SOa>tz$fCf3=_7#66vYj5u0QH^ro?qbF+nvQpUjGZ@;ab_a*2}gl8&30}=JDV?r3m!i^+6LqR<6 -vX{o;_B>-*PN{W6a?#(jUu&|L;fqK#tB74d97RaSeXq&@XUJcn4&}O_y;A=E-wY#=Vt!eS-Jszh(Su` -f6`H8#((vhiB>$X3I58@G~Fr@|R`&iclWbjPs%G>J}r%6XQIGCn|pI(}3?16^kzbKDGYq;6c89mU~hU -@J`o;wX-FN9keI%3|s5>S!C?(Q@_a8FLaNJ-|F^H=nc1hJqk`beuGC1J7ZUDJmEi$g4arn;Vv0NO%#* -2Rrwl4fN}=o8}aV`4bV?B#u3+vwvz1IHbKSHmUegVg$vC25QbdiSUTkiYV~_AgvXu7KmHPK`xxh8*k_ -+R6KYBYje<7R -gxbJ(Jy#wHC>^)7xwZ7KG>*D%3ueezK;YsBMxSD8s(^9_jod5#rhS?ECZ|JFgDTOT~!^@jfC)V~im^^ -fMz~PT)hfcV85orUOn*n2+r}<@;)TFXH=(xfICxlq$bh9rL*!vN8Moub4iE?|DMW6Z9ztz6U)*-lYf0 -1K}NhLyX#_Hyh8*6LER@{&?vTpcFxn -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 -?rcs_eNKAIuzR~+BNYoW$| -j9Ui+y>P#YIoH`mNGpKX0)zela+IE+)9r-`Uwxn9>k=IsKTW~YB=Y~hHpP>~qgQ6mYwB6@c}{QDY4eg_Mf>to;1{++9=%tiy>RGrlD{IKCkkjYUu_v({% -1yc@EQk$hJ6q1iK-m5CGO1TI+9oCZxDP^HOkgZ5FzTqXwMJcumSu`R%|YG+MLi&oOvaf_o+aWc1NRpI -Ny0Q+~zu=Y&5u6YcRfD7W-fg);oHPD~&O3zR-AR*EnPBwu~JkdD#DOFt3!|8#}I6jEi|K&g-7#x%W6WAqC|3Af5>amB==G|{bXzX -=wD%roD`wnLj?MQpEZ^Mn2?hKw!kPFMKqGw5BQ^nb2Wt(<-0q1S=pZ&Bb4wsAKAA1;c8?=$U&ARAZls -4wj+azAM4^&)!SLe~70md;TfYE+B -sXU6qT0bI$aK3>^|}eMZkeQ|3hsTJ!y3@V|DP5vGnXZmda*yLGbEH&6C64_?|y<+lh~qDSd|rhxaEEb -mNvX)~B43N)*G|53n`u^%_1{$9MFj`uZN^={rDQ#P!!NL!;F%uU#YcHe!QwnR#<`w05rU9+9`XwAY`v -sH96o)Bcb{chl!U7UQN6MNjoTx6iJ)R)rU`=d_jM=tZMxg?A;{2VXyrSjbb`Of8iihDD~-C8w6blX!h -TluX9bRdcSf<1N#_C69-R8>?Nh|9GVT{ -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 -9-J4nS2F9_W58;e4>~h@x6`IP8vh -qF=+LR!6>_C}Ucz3=*`f&HmJ{)eFo+f(OhFQ1NYS?i_)umqHcN0ZE^{Tv=`6cAis{52)^*KDVe&s~q? -FK3P?J&!A;M?X6s@$LO%Wp -e@E!sMhh`FO*ww1X?bQ8vb1d(s**`syq`C8`wCWqvI4u1N$;+yYMcNWwOFc*%$*`auWKODijBoJnq`| -aA~R^t3ptEgXk%2U*D-O$c5tFvW{gMxF%BEH>WgKd$gBv_HmgoNbag*mBKYm_3- -FDBeIQI?G@8XXmd*ZyYC!RevdQWWU9S!EoeL)#j=H1?{V6uz;-|5&FZc+I=;CW@M9@?3tH_$*P_diGP -i#Lp%L6xm)Uw}W|*77|)^tj9cDta3Jg?*7Odf0|p*W4F>i>k|gv2zl5ktm%1*Fd3a(2lK~^Pn>$I -hl^<2^1(efURk>k2^ZOa>jTa`6{%7u=0=m>9^ShdRW6H?hSa&XJZ!|Ql`k;H`N5>qem9a1)E2V?K(t1U7v;KAc6cXChJ5A-3%itLrSje&p6 -%R8XX`^p5-!+WuPz_B0m?p504+eg{t->lj9Z%$G)=nB-^T{~CC7Q0D4uNaz+y0@c^EhzI}{g@wcvj%w -BJsg(fyFty_|I#@3UBAKne%)`x>Dxm7S2yhJN9 -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 -iiuk&7|UROQ;2Vj?)eJ+`d4P%S&Dfu$2Dz -%UbiM1aSAQoqgIW4y}ajS?%7`|8+vaK%5{;y1paNpUyY?HVCV>*kJS-)aSqeYC3lJ^y{umpQ@{BvoayLu|D0Mouaf0=7ava_D!j`rCO29#YDr_bE|i0R{sQ)Ey7 -Z9@*tLw>QX_|GjU~fTlBU;RPkBx1_(f%IslM3Lkaj2as=3YTnL6xTtyDhKf3w|)**{lvdEX{|s-jP-& -EZK*kv!N>jJV?*Z=++(Jnri`q6nWCg}l(=&GC)AwtsAo`)6t)ju%LdK_obc -`&=c_F9&Jh;{hl~K%BGRCCd*OEkCC&6jACA9De&?pmwz5%1co@)7T{s5sTTEEX9bVXe{LCyhC#NdL=i5=R(&%qHU^s2s{=#(L -%&ebHy%J_-BqS6Uu@P&bJpdDhXVojC`3v{m!UlEvgsi>;P;LE7+>L%``oMHf6dy1ZMEb(T$$b=;a>C~ -45bMS%fpuGgz&I%-AHJZbBgE&`Pr&eKdiP|LY%Npot@Z_Nar-IL}J?q102wG%j&szSyQv}=>+FoxhJQ -7!A)wPaam6Zksyf8rd@X_J_4{GTAd$IJT>59cOqSNR=?caeLt2tQvFMN>&nM|8TNsidJcdsD^ie(6_R -yA1q$c|wuNZ$9v|SJeG7V||nNNR?{@xTRX;+PYp*`#)3Zkt)J7MT4S2E^jq>3z@GGbV%60xSDY)44*V -F^I6K=T{w>-d)(!1RyL83>cf$}7r76P=a072ymw=~b9t_ZaIW{sIHZZ-zY@IU)h+{Xa2&s9OR!F}^lr -+4i|jh@SFWp`CAw?vBETzbnqlSj#d86=~PgF=e0U{{~<6j^tmPDvp0z4} ->8@^x`|e^>4f}FIdjJcPqShO{51G=z)PRQMQ3hI8&26;|-e4D|y8nhvBC;Me>mE*4>czn)6e6yz>e(C -m7ahV4faG0S(AasTI`_*sQ|@%lB-eAbWUE%Ib6lSn&<-U%0c6YWr -6xS4qeY)UT@T)w+FL(87SvxMheqVf=lpFV6I=@#=70r)QY4qk6~n4GhYd+7{3kEBNLF=0=dQAsJuTH4OeXPTZa?0iKm -Arc5t!aK!(8#j5FA+DP8%OUVbL^V^NkcPWQ_Efw^1k~qbfD?HZ*4F0Pdzj@6q*~~>-V-XFKo{rcp#G| -JsFAg4C<{S|+KQr^r71~4(?e~9_D0&*OM(y}}7wz-xtcIM!djxq_C#nB=?;vxRIUsv#qJD<8y`8z`q& -$r=U!Q8+i{KmqZRnBbu{O1?o4i4iIJFRK?f_qI6CD*hAV1mi7I}}mB&}@-CN)(!ZqA&$;To~ZB7LbvL -yJTBHaU1F?`mcFo8JxZID>VeZ@_8BQXDSbXgBPCz@s-X_Zr{d{_?1t$`42#(Ku^Q-|)NWM~lA?I`6HP -ACJtN=k>y=~LMYta5O^u?SuyvHx8nsO)gmKy# -a;{Tt|xRbQb%eXv~RXl@|k59UjXB^>~%UA=$p85g6coEA?Q!%_sa-DZlP6`h1!g-?AF;{ghzhB-|@ut -<`&voNG0iU$KW_nT1x09B9KT#aG19EM(rcw7+%)DM4BY8FcI>*3veEV-BAGnvgwFfabsk3+yeEs8{*; -k;wlq9i<@n_BV|1IwXMP&`(bu(?EwcS$wqW>{-XYB_6PCLZvca|1`r%ul>Y?*kU$9y$}g=}k&VT1A*c -y4^Bt%F^bNszLHS%!R~Sw=VPBjWqbkM;YaEX%w#e|aeq7j5lJN^buY*HRNbfLA@`g0kJ6*^EtJvO8}O -nQ{Y&cZd@jCaVox+B>7l#P#$8D=52uW)37pIq3CbqghB955qG*1CSj_$?3>tY9Jkt@-w4WL|F3JGcZtNw(mb#TfO`i8fo@0O+9T5F3x08;7!m2OB0oj%$C4h`GVd4baw6*$5ifBTLO5qHYsxi7#lveZYuWz=|`f9WBwNA)UrlDWM^K)RW&=yo1Om83 -EEtK@`Y;qbM4q4i=B^5=)f6$D787YmGx*_S3O*H$hleMNuL(Xxty{E=(OrLbxgmF;O$y6Mdc4PXL+ZV -T6f%<;vJN6v-FMZy9~eApnpD3#P4BkVjcNy#!Fj@^1M^`OJ1Auy{P2fWU5=_2LU_gKfGpD?arW0Js04 -aV`4n7r_*+S(U!5OTKFENf;sYCOg1U!BvlAVV|N?jLoOnqCyFacxd7@0F@FUGboLBX{bV>@mYA-0){HeX8?ffr=l3_ -awZ^-}vI7{g)kceyz)wBMxJ{8q->GRELE#*nI|N?YQ$QD=d%MsL}lY(L(p|F-uJ){euPW6+LqQO^=wS -M}Ro&DLt4W$kzi--C_JHDeoOyq<^e)erp!#D2M -+US*!Gcb$aP%VxfJRF>cSQyq)~M$cRa_K-yl2z##TE@vO$!3ykH%_lz&Z{T++bJPXIZqutN=_b_iiM1 -Pg54#<0-WHEeB-o;q`=U%pUGgrjB;8h-TicP_()JfsoNfYUwHR?_;yiVm(Ww}Era~}VnPS!)+IGv?aUmL7A&I(2Vjm7gio(9Mi_<^mnLx3C`97U1E13XxcW*qr!I>^p1Mt66O;F-&_5h -$|olG679QU@`*9$Iqw*%FGli-RcoF(FDEH|7Iie#1Id_@sfG<`nyBx$$e5DbEIE|pco#O+f8qdW=UP! -feIRJPTYlFxA9(@!+z&XL&wIp3y~C)dH;Dr3(cZD5-Ua;u+8;1an9#oVbI0(?Lj&NssMA@Rr@u2`6~! -r*zJr9l?OFYS0qghMA?LJmj-R0ot=5vgC{LtUYBuKsfZ3$o|8coZY)il6V%4D#&Z*FLd)^lTXS-$9<+ -&PokOt44XRYhAJ3(8{CaZp^xi -Mg7u`?lJhE%+nvTWZES0}M;a3p_n(@p}BFu1Q>SCGdjlounm~qtAo6Ie%_X1nv%?jcUf2ysdgCZF=5> -%>J5nme&qCSeCJ{e7$BbcYqcyWeg?j`Y(2CPG`xpT762hNV6pY$3V|`elY)EA-)}cPOHa$to#kyd?ra -O?gU+?uX+piwO!`Ybk?n9yeIVgx!aT-AMH^}jQ2sjx80^_B6VFhEy1x*OLRD9{W&`~=P%idKz9dGruM -e0cJi!Y`^3$`ay>vB)zD3Pfb~vbZ*p!Wx8?3sF)CB*7-yB=&jHRn+vU9;o_~Vh^HVK>A&({S3VG}20J -rD!1uS%?mpmZmw~VSw$b>y920P`xXOc>oEs&Jd6>)53nmCuR{5mIXa`@w#a}F$78~iFsU}+$@!Nxw7Ar}yq)wqtS$prj%HMALMBl@|g4~-f(q;U;oZjpf(3azPe;#Mv!rSt963;Aa?mZM7v<9{ -&i=Ms}Hs8{ZCZB?=T-lFuPyQm@R>(UQ%(*4)r`g0O;#b+FV3PR%GECNdFqm`!CQso1wZ$qfay4M_mqX -#UY@CVf4$+oqKK9uBK-tY=@{e+c=5^iM)sc;}`L;&ulE4&^&)gZk*lQ&n;7gl-n$4W-^pDEh^9kQvyR -FrP@k)nYeAfiMcOjR^393KZm%R4n7C-^I(tj -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 -FeI#R`+N#dJLdA5eZYr7%O|@rJ7L; -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 -$^dX1@|2e5v -YGS=>LP4KH{_zX7_`5_A5Qqc=21414?%_-kB8dM@lzvdyvO5jpC?Ysq={Apu&QT?OR94`snT5DM;@fL -U!>cyXeU-t(W+Pl+}D4d@pZTee`AgTUejKGJe)FyhGKrVPQdSS&yK0tb33z>lW0Cbqzw>MXQW`;5-v!$ECCfiZZ0JaM7+ReDuCZGiRHQuN_E2Y9hGy%Mm-pLI*|3oNOb&gI>^8Ff -RQ!G>J7I99^-23u}N-PQoMaKCGWZi-7c)TuwkpV8FRgz>|(Ydd~-iT%iG=nsZ5*ypGywLDl)E4x*Vowuh5?0huEI>>v^AszO|R{`PsPs5$-d2|FE3*e>1LnU*_Lr)42ui?J#~j#IAts* -6fvXCi~N_^Ubg6pSqoEAl^B2ywAQ2@S8*)GN$I&Bwwriruku-~=8o8DL4`eV2+{vO-|{%T9Q -O!(~Inr^8d>QM_e5bp-p>ZA7anmIpBzrs7kAL>(_(tTJM>ujH{l!oRiJ>b5ReG#5pri8(LM{;Ly-+|o -QzA*mxT;udgv#z2=}(M)~ -j8_E_l|T^BNctp)mh)rL}*eg6$MrY=C}ZHQ3BQ5B2Lj?6j&!C%BjTbsTnD)w=`SGjeYa_iea)wND7y? -b`N8*v+5p=1+F>8-_MJ?W4D!16!fIdd1J*2zu$EJ%IG__F!$3tN$0~rT1!X{Tse-y%xlR@Ez<^7BXrF -GmgS<^q+x#jbnMYApG9^Qx*vBaifgjzJ=I?fcqxy9v0-w-Syy}HmZ;rMlon7)^lEupq+q -mUm>O|skx84bq&JtOgVnyzIN%#t^e{I7wye1*Y{&rQ0FHf6Z?}F>seZD0J{=y%;?cb=U3wx4+9&}Uhx -rPoTybqCd5~uTet=HKosYMb&6>4Wmnz;8`VNT?;U@}ZH;3{{QQWqAh$gmUVjJM^>uHyz7G0;XT3T{h& -?=UEr7DVKlEYQ+hQCj9QRCx-^IPgJk+7#Xv5@_gO>g*78DLCEv!9|3LAaFl`PmY0zo;N4fG32a$LHWO~$$t}>$tp -~Yc`W3gtk!IfR?Y3p?(ehu@ -~~{p$P}-&YRm8$TC?`}bM&s=u&74dQU0MX%wzDE3j~`93x{whsP&$>nk%f&Ny~EaTf1I`%fMeVWzZc; -yuxw4KeIZ42#q)r0VZcloOI@OuD$>)@xJSK=-9Ce-W2`8D-$|Ni@8&Jbn43-{qPf9O8mDWA8vpL_)M4 -SD-S&(^k3Zq9l2jEGd)uIazRctnps#c$2?(3kXs_S>TP?e~Ll_>L^!5BygeH%wR9_5p74WIU?;amHP^ -rry6Qd~JMxV{t+oT&L^57S9uccYzW92DUyP+Lj5%{^xPcHqQgxCmiru{;U<>6|ge4e%dy+{%LWRR833 -gkdA0-swmO1^|+sN2F;Vbd?;&y2;-#jh1}FMM -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 -<-DwZu)v3=%`oF~sEzI< --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 -6ZD~IlSW|9XRdBkscz -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 -&sb}|LrOWyV(n%Ss;U)(r!cr5)>=3LYotEIrQ -56h|gv{++0(0aXt5II0(XQSb}ETa=F#ACl_Uz3$U<2D2 -(}#$aRz5Sy+HYX)Y^r<*l$Zq@u<<*=e0^TV}C=05%jQy5-9git7}+EsFa|bz2qp^XZ;cz2TK$HZ9d2S;ttq*Y(b`3sNSTt!C#n!RC1Rc}Yjd;KgKT+j*#Cnz|OldZV>4mZW?OCK`{~yDEI~A_zi3n}u96astUSQ8Lf -j;sP_9HJ43Cj$7NfL+3jF5 -OagEZBVug9l4l{iG?{iX1fCyLb-BzW}&m7(22T#Os$eg@eG;IDUgLwG-mMJMF4FNzfU%cj0+UnWzWnJ -<+r^kzpf(lzgIKYu%}K(J~n`{k8Uy4TK{R`ky1=ac`Z@U>S`ar6F%{6^=GH`4j#fBw -aMcnepVUC0k4y<-0=z<>A(Z1EC&LyOmcxoZEPTrD5$DflfPJo(?N_kZ>E|9ZL5T>qCB6Gi9k@rZl|o) -j6Yi&ys~{48xGwv9HtTD7(IPo&1%N;oYC|9hT3{)48pj6DG~qZ70RK=qyB4HIZBP%DmQhcH$IbO_K=ph-YC0WASq2GkG=Z33=Q>LJ!M74*QpjLs -^Ddf?m*=o=>7%ff^^Gca}ma4t}9NY5byj*I2!(x3sFr89e#_hzh(V)m-+gL|H3Dnt6>ylO4W1=a3jzJZ`3LJ8wJIAh;2HH -cgQ##jjLs8<8UuG2nVzQ{2Rv$z0!!T1L}nQssP$isZ*+>(Jm&%I6>%BIsw;&L%Jv69t>t(l992K;QtU -%vy!PqO@#JWIp@^~9mw~vd`w}ir&6y=n~LAA*D1J=r}ZqbYaH&OV^R$9xVK)WQVG-u?n9Pk|Dehox3X5*f3CIvT2yA#4U0IdMpn1 -*{>X6wr537Q6v&1`Tlnh*BXfnE!sd;mR^!B~HwwOP1VnMujb7Ivx4#<^L@A2XIKlQJX+;#X=}!$LtTE -R5{}TCiBqA$gEqU6!uA0PF>{!2#tP;>%qI82ZcVjv-|4(~g0#;SE_P;i0Vw2c#BO5%9kfa<-jhr|LDxef*8f9HT1)&^prgAtAx3J9 -cUT?^^G>-eJ9Kz5DE~nV# -o6J`ZVCfTsZ~lQ}Ms?+ZdCqQ8#56hSoLq{5891>g?2SsOMI2>AJXipeCJn{s94T-zg7v)*Wd?mAD`1%Px}SjgX8K|) -UVNQme*moIJUpecU@MATEIZqslN?&IUI6J7vdDJX9&>}_G!+hJwaaCwCkg89=UwSMvRsOxLiBz=9$lT -j2_l%@@X%it3bE+YCtpE^DKh?0Y(<{-LZ!>ABUntvpQj=Lwy@nGq2&hZ%1pTYxMqn*0P^ix74*fKCIO -OE<-#1Z@`}cMz4chdAuHLI6(V)zN_t^R<=PO2ak<>mt>VNmC(MbL^9wy+~>22&!B#z!Tx(%=vup01fEzZrp8GZut;fw5G=UUPhwjb?Qv4uJ8D)qu++PBwMosamfx<5(Ys`F8;V2l=0s{ -4n;Qk|X>K(B{kFQvNt61NG_7snNV^W2ZRN4zJ*v8#@{hwQ+7f%8c_b-S+I$!GPd)QesTeE=WEbxS$*a -*Ug2h3>yX0DpwP@c$I_xE*tk+ygm*5C6SF%)~7>h@;f -^Lz+i5+dHHs0+ylwkYmtOK+ogwYnaExckDmfE&4m^^=QrOd)^Pf`90s^6Qk7tnsHy!2_c%pzgmC5xZt -?#N80Tnx1^uof7s5?d9j#HFlv{rSB?|M6=Sx@W74kxSvoUR>mi#`kg27C)*&a08cieuifc8|-EFL!`FglK99uj!C9hP# -)1W7%wWj1+)0*d@H#caQ2M@VfZq!JZo6k)e>>2YFpz`-OYp~New;I5=!I$H2npj4@y)>~1;~w2c6E^m -HyCw<%{qNL7DPUP!P3+-u2Te441oqoe!(0LSJ85DXjy=6K_~jV462N~@UwkwXjq?#*zz<-luO@r|lke -7)KGg!Yquq7YgeT71{WQ#Xhul)TYoe1D>sES?ruq5`m_j|M%D)xee4Yj{@j*@bvl75{ -r1v3B#Vt0#O^nBeV|*S({|~c$=sn;x99thnJAfsRX~GUzJxUXs0pp+0gqhd>u8BxMbgA?r5)ku=o97r -!RPuhn+aH6!iqpikfFTK*_#FP%^d$74*|Dq2CMvyh^O;0`4`_*iX58mBnfeR<6ofyR!f`pQc})XdxbB -gpiH8C0i!>1o81aIp{A)bm&Dt0@>x()}T}*loXb~@gUyOgn5*fKLGW7i!9Xl@-zsW8F%MHE|5>+8vsTPa@VJ#AZ8d!GE)F --6}R{D$cCls0j=yw2yd2-S0VI3MYRNlwV;zetaI?g`__=X0V*{ij)h|7*ovy@%8XOFX`oAyKGh7~dUkz4* -QsYVX~K^Bh5hFjnGWYPa$|CzvX&>OD`IuySJoF4)T7D44Z|?P%(ko*|8#SI-7Ea-JpWcN^aNiR32Er# -5lk-o*KmCeD{NalW#N^VN-<=l7IrHT`!l)h_B+yACm+%l)Pe@HH+wX=PjB1DjtsHgCuEP8W_VkKx$!! -m;&N+~4fNarJq(`gfPHL`XOIrH1tB?^!nq{r$h1X!iRXTuQfY-NfL*gT*6{JR%Yj62$cB({+Jat@vjZ -si~)-2v|Qtqf -Lmpjhl@&`Efz`;r2;&|pv9#@y|*s(($s}zz4{yQn_;?(t-vuZrP2eN$Mc|5b+qn7w_adrCfT;9Kiqx^mS#97t;e)g{)qkk!1EZ;%WaE|oVmH)CIM`w6l(L -cZLg&lP}&ae$0pMU7_P~1>A9_{0}`Q{IAx@+Z*aqmw+283+PYMCRLv@MhtovWocFon1vF9TqDrgKP~A~mpteG -7E2-z6PF=s8p7-d{LxhKii{ZnEi)WsB<{}+WNl6iTd3j>px^+s&_wL;*jvP5sujh&Ui5Aw9iH?meD2Z -nipOqWpwu-r1Pi~ESYLb`>5yfqla_-jeqM!*+&HdqY)CT-9_w^&Ar2H*z_J>DCj2N*sZfK8uz^JFfCO -;_U`Y1VPk@wb7BcjAynXz^K2)4Ir3EQ75<%zRr9YSgGvqH>i+g5tEXe))gvx-YpeALd$vfM9w73c=_lTp(?={AH&85oDo8Aw6ew0qv52?k^%jNJQ1RNl(PCq2qIm!1A)+GVDdDq4io4& -DqWk+&1ZTvqyaK#T -Vl1ufG;2k5r1?KS^=mzyYNf$BrEnr%oOhhklWwrlv-mJ$qK^3X7Yc5X~ZlxC%<+ks`#^(3@*u?AOD9P -4WiOPj(lhIsA9lg- -415dl9|Qh0;4cAwF7V$0{x0BG0{pmHEV=C@#0B47|NeO*f{v -7yz`q*!@IN&OW~Au`A^QXWao|4#{8xd$0r-1d@Wu5aLfitIZ6{KMFU+==D2L|PV5J~iKPF`NL?MSgC* -25_-&W0$G;8u{AO4H@E-#H7~szUelqZv13w%1>wy0b@XLY! -74W}v!S`^Ez|FwF4fySV-x>IK1HV7;BY{5|_=|y`4g9UZ|H1|T;u577*Hb}30|EyG>aU3!+Qp|+$M)? -z53au&I3O%EG&ndcC?Fs(II63Uw|A$Gg9mpoS_lrsW&8`pAHl&tW$R1H%H){J@~VfB} -I~ck_l$o!Yl^UJnho1cn6$1qT72balJi*SCMv1tP=3!;CWMHm_Tvx^>fA=-9rUsl#C6qy4b3(15V0J8 -rzy3)gXjkMoAkQNRxfByvz#)NME3c&mP0(b%!Q2}Fk0w;y%$O*h^+q@TLMSKZLgMEsy&UJc~+n{K>gh -?BITUDyNgqXGiM9|#K@7#7ur4czK=%PoJq_S$Q2AIcthJzN#E-{ztlqBsn^9yBmKG%PH9V0hb$ZoqZ# -E~@gE9V~KPtGpg(QT -bjW&Y@-FV}TxS{@fRIjU|EPeyg!2`H{=b%A~gxl+{2eoS6%Kv)vz_8$OjPjtiiitan4;uvZ4h+1w)+6G~e6O-Q -$K(TnrijUGC8%DColVsHg#f5B9mfMe|;!xu*5?4 -@L!r2lRoi3E|qlUP3%u-ya@`aSV02%=UxTWt_k7-k?6A7hl%(2ki$#5km*|?A_q9OZym_(7@geE{m{> -OL&9JQkKrGZDkFpLLgE4*Ti#k8>x-Pa#j)0AfCH)oMOat4?Xmdibv+e4z7NN{o>%kgW~Yv!{WQ|z7yYn|GoI>$CD~HIDPuGICt)xSbA29H8|e&n~R_IYln&MK1 -_75%ak#g=$^(z_cA8BY|&b-6W!$&F-(?;$#S2mVErQ9n)^Zf^C -jO{0sBkf8&(@#wq{5#wo^kncB2zqaI1RReeaRjaMf>KR>>D0-)lV5AkR(D;8<2LO&czfgC)}C$seEodI?d`kx_}y`}Uu)cO{dL#fd3(D~K7Q9+?Q^H|gvsR -7#m~dzS|8xtb;I@7weh;8i{CXK9#^+)-u&9TTD5A^?5Z|h?zqO|YM$`(YDDo4h;t`uX|tw~pw)i?44-U*C4Vioe@Y_^e#JcKQ=ob6W|~mz -#0!&f3(&qngV-yq_P)Ypd@M@jZUg>K4-_{p=MK?tpReeU(dXE-riy>3_TYnMY?yFF)sabKACUIjF3Bm -0{*=X7hkEx*Jl|c{#O-uXS>&4?nY@=`kPj-Ui}E>owrV&Jo)VpKm2eMbI-vO -Cr%sz+<)rSsgqcj9{%d9uhx9dK&k^Os&yevo*R7qjP_9=yEC0UEd5xdHE>-*CQ;*I~o={1N)koH-*g4JrNi_xJBiJ!U2SJ9qAsZ@u-Fgv=#kW@W=1f6C*-4?k3uw3n5YDO;(mt -W0M^fe;k@EDJGrM6cw_lcW7y7qs*|M}_ -$BsP_!!Loqq&yT4q?cpPaV0;L1!?Esl>Il~e4{G==9qK5NiX$)ypV2=KV_W$y_CUwrL=r1WuI~>@7p2 -ey=CBGyOdqPL+7`pZ2zW|OTLn_uC8w7dPbFNIAGt%An6M{^S8N==H -Ib_kf3q`=mTmCuQ6&Da~8zQxjK2|Jk!=smqJ+z4zYs(1nHM0dn{qFQkQfL>e38gX7LIr;MmK)PMS2tl -1Pn4}$(7U+O#z*dt|s(Apb3^n~6dR_gR8?3S|Q=8J#NbUFGlXGp{UQ~xm^N!ErkG4!0{N?A~^s2c_!v -~lVg^`3TNK-vY%@B>ml03O1i|AY4FJoKxO^8TGV4`$fH-S0^0v-OYD4_~~rQ>RWnA%`WjGy0as_@J&g -rC%I>Ugx|(Jo*{eGV(BNlstSbWhi(U2p$|K-;<0a~9C1j>VO3IwlZVfp_6(irYuNLLx;@h;b>5=ebJxwfO~d2vXo`Nwd2x>(J=( -*cFC_iclZJdy@2S@f`7n5)u2YZp?^q{uX7-gC)BDNSr}kGousm^0%150%MC_MxC};}#+{K>BgVUb7(I ->&Dc7ji82Oip%=)M(YUSssPZ{NON|1rmNp8uUZXuq5f3|Y_~cx>=M`r-Fv>8oR9{?mPB_RM}V9XzZ84 -=ge8pO1rwNYL@{L7fM~p2>q@&-6*%Kft)ZE9G6}0poAl1p6;j&3w`r{qXln|6yxO7!T3r3_c8A$OGqt -rgV$+(@(CS7buIK?IZKRLl$^Qo2Ju0^`w;1-+_l}DIYna+cSMq*q0aVnLHTwOdbq-rcd&DOEq)0A^o& -7&M%0w>MMCLYC}F6vk`+AmW^}omuqMBmPOC>k-16WVMafBy5_jt_p8oB6nGeQ6g;@tbC}bf19$5_$$z -IlKN~*D8$5J)t5LtC=tq3NxM$Cv?Gd9aB>nV@gwz@Gz?jG2h37ce8gUt~alU2D!1AW`ez{@J{c;U>C< -G67@UVILBzfxOu`6;m$H33w=NRJ=Zn#o1CjSx -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 -tQxW1MRzt{=I6`EL_7dUVP)p0Ueaz -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 -s5KST3}}*HQR!Jmh!z>0__#z-c$}x0o*g3 -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 -GrU2UtSk{JZXs%W=dlME&e2svVX9WE -ZoO#mE>Qlfv^{l?bn6ive65#i8E7y2`h+(>>6@LEL4uejr+C<0mzE-uZUj{&i=@y2Ay^8pja=ep -BueuA{_>SJZ>U!;5o%HS;&XrP5oVq6vb}zHoA84}q;~^`eIL%~67}=Dpt`9mZ6a&-1O8qj<*1T572j2 -QP@Uqp^hi0a(ce-rc<*J6KDS!6#0%5ngkiW3J((`DZC-Xv>^rIf!rkFvCwzT9o}6P^8s54Od4<`>pn8 -Pp<;34ZlU}1aAKMSS*tq$F%1xzTmzM%o&QYUJ>Ig>dYS7P0K_?(u4r<_vy_bkMuv*D$;Je4zeh7vz$`SM=<{&ob`Phox;=X+94lniH;pWS>#+jfIqy*$t4LNJ -4$%yl%IWyz_JK|Gb9j@J`{v3*h|HzRox~jJ9V{CCq^Pa&ih@!w-fs}1v&TL1l~P -Mtr`{YtL`A)J^BeJ_HjALA6e{Lq+Wvk@131hD1^RmsBJ3d@fEv5=^v0MOXVFzHBMSV;(e;fY*Id(C@t -+Q^;t%?0y!Mx0t!70ZtO19u7L<@y$dq+&dOgGn2fcI#abM|D1ZQBP-~>rK1)VJub5vgk5HQ(@xI=Pec -h|WhP?uN^5;>Axr*6tL!>Ch#9i|SMT+v#hDdP?Q5cF8Q*a+6vs?jlX(O6ik4Nc3-3Jx!)&L4j-g-ot@ -U#Lrd#n9&5cliagt}x&yNncYe!ysj$gbvkSflQk{Vu9%V8*OM9lQ|g8il%DlNw`8InKW~??5r@03oiJ -q?HpR;{$qTBnhEEF_`Vok>FkDV>Y}_)7}NPyTV&AM$Yo0tmbhy03!!1rbnnts_;Txhk-1OMWseunA-! ->sO6kr&U~?Q0V)S~V*>3O>ix#xGwftvIL#+`nutZcxJ+S?bEiR~v(!rmhxFzM4o~+Yk-qT`czDW&boc -3GrQ8N$o5*&1BD2iu!}sGlg&J0MvOBZ-w%H(iAT4KwjWtIn;@OcKKqW^so7guwOYObeEb5)>g*JFj&*2G4Ur2ck+$>eShOd=7kU9z;sI?+O%XskNg -SCoZ^^jZ-fy6uN@>SGHCBi8N*{6pL_P=#T=MOijE9Ww))!1`qid&&ho+tfm{=~if+T27Xs-kiRO=el! -Lo2b_Wd{JsBHy3c3hwDcCoEYOuog=(IF+9Ft1H-uRK32W<1R^WgH>uDwj-Bw}f$Jv>?tf5b;Ri3Ic%k -#s5`^+@pt(q5BgI`FAk~PPPXfQ!q5j!RR1;vOcUZh>g*P4jZ+IvUeJ)!iplu(wc-2`XMF+a!uftb!1{ -Uk)Cx^6nyO0WMsYQ`wSJ>O-XT_4Ad0{UW0}gsm)ZaxesaGxopz`U(U8_JI_jc4~;V-U6*xR9v#FG(_! -D@;)61xOnV&3H1EX{7bTx`5d2DB^`Ea -mRerVy^;1*up%Rr$bmV>q61)C^K&2XlQcC`%Lg=@jhK ->-`=LX_L@BH~DX;jP-SwBe+uPVSC*T!2!7=Kjh{wD8881g9n=D-kj0xEsa0viON{+j~SW#5OjVGG(&cd ->nsO1r^vdn!{@5k+K0xcs%$9l{j|^lIZZijmaWR^q`*gN@F?t853u?Bk;Z+Eo3;z@l8dDzfzrXxF-r@nyC?v^LVKuU!&WA)5{z@-oMn+g&}Qb5A8Y3&lTNDf`LyDgvUgJ|&e -iYTK=Shr!0Q01t~lMIdh8?p2(U2^KhPLKeA@;k(x_IaceFf=X_FuTW*mVRp2Q3s -ejT?znX$dijnHn%(x*c*gII%&s8F$s40K=a^AzF8%b&FAg<7x*9LR}kEYembYfP%n*}s< -0N%V#KWoFO9f~}|5X2(cu-}HMzs?vO=adX=g>xakoK{h&GXWY)Hv5!@qzdF#BzUS_b7fr_!z)bbh3IV@4b(TcEq?E!r);VwNJ`s4lvmHU#TeJ0Ia&@+=dlFuCXLkp$WWg%dW%NcFGj@qXM4w}7WFgMJjsDar#`Cap((^lO8XU8lz=**B1+?N -6X&MmuD=QD1y79<*71xl;$UpLamZm&&beJL$Om`*^W5*5spEV&LGW^d!tGWd*wO4k+~~)(m}eNWt^{a -3eh11XUKQqqAt?FI8qbCzwr1vQ?dSwaK(;Jn8GMjA`zMOog9im&dZ6$mUs-Fe_U7sn5r=emn(!+Li-= -sO4AFe5P$0hc7O<63>!$?W8?=?TxTU-+Y|zpNHDi*B>+Tq7qI$k%M(UY9yhlg8EsGajL8?no3(S6;^C -d4pIZ_O_R(ff7p8SB*YC4CtO(ODEL)peu;C6GyyOL7FdiGuY@0MH0xK^xC%`H8zkQYDZAC>@VkL8utc -^b+_pnqx|~~aM4v=4Y&R{pRCBnNpQ!yL8xQ_-cG3~-8q^L!d!?l5n}_OMG*x-GfuA^*NYG}9NfxCRk1 -L-Jjv^jtEKHVWXv09{BVwJA&FcYK%#VA^gG2|agUzQvUD~A*3%aNSVXF<>j2MqfgAMhTSe#kErdq5EQ -PBtaH}L$6AhpGMJtPiEHZ`)Z(L0dm4a2*&a?R7G@)eJD2Y{x}eDVg=tWJOtqn=xR{&9XVzL`(n9^G*lB( -pimBH8^=UG3^KHoS$UWp^g~%Zq7Br(nVqpXEJ2BH%s7S#(be@|lob2l21Tjw7i<0iXa7129{EMmCIz& -b%YQq!s71yy}$Qd7TBLpS(_r%(XEQ#@0}k%FJ+iGD8QkyV33*y&_Bd0z`021M|1o0WYSoWi1(+#REJZ -%$vkMcHgjKzN9xvjR%t@%O|YpTbQA$0zPXirh7w2WUI%WBTvU0_tG=+6vb@) -_@YLa+%^k+uC6v1VPXa`Mh~tK~<hKfy~rn{wBThOO1nM!3 -Krk|O==XOuduhJPzWJ}DeZ;P^^~hiTLZ~SfhU!(29z()DD6qY=C4z=N~kFCAXFlz>sq-IohQ##qC@2w -(Bu`^_Gsl~iVzwLFw)mz0@M(=9$Fcy6?kE_bG)oo*xWl*%fY8?dir{(mTlzBfSiIR2%EpkSYZ{}ek~a -i5=>KsP%>2OMFrm$B)7?fGYxxSHGr#FZz0%!TU{rUq!{`M@w`nbiJ2=D_e>$QlD~;p;3%XDtR_x_lfF -QeG_7VDbYr-78)TVAS!_m@Pd<-lu|t+&S`K8nl(M86S^h~`giso9Y$#;2pVJjjL7ouemZ5%BBkl&47< -MX}h*hH+Kv}`Z@jhhHttUpoh6Z#P4rGPB<2~Vk%^waxLp$JNhrcOY$p)-*C2~Tmi2u0=d>edhZr*{&9 -JBiRbY%Q=RC2p#-ce*W2@y1xm&*G9?Ouw8%F2u$lUyNzG~HX6r3B{p6=s=03JUlmv(GC1_LcU?T)Vm^ -m#ZuPOk;OMuR$z7@)&T{-}@37!|eC2!IP!r<5&l(t~=n>C-6$MN(gOwOLR31a#Ja83w8xo(n{8`*56= -N;f#Ldf9B)K%I22@#N>B15eG0qVwq1-YZ)iiv`?w6rP!9@I&IQiP{9KJwjaytj;-5}N@q*do$;rUjY2 -j&yQZ8*g~}G-uk2)*$r-IV<8@9!s;|awH6`$x23-zkM#q{>O@lJZeAFOD-f-DZlSx#Ml?d7lYs@4HPx -ndO0v7G04$O-WxIK{5DoqFpa$cuc|1)gyuh((3RVhWhAY~WZxuGEvZ{6Xgk-TCfi8xHZBV^6Qr%F+`Iu@V4Amcrg@-SJ8z -5@6P^JN~QDL<5j?M|C5ujyEIWAH=)%$hgy-?03={i9iSvja&_D3=wu$a-Ti0$)43T^H*IY)2sSvr*dX;sG};t{2P*v -gdpOyoXl8a+_3o&@fbATZz3&?G%CGEZZ&!S}s5mYW?90fuY^8pZ)4X|VJ2aD2Zm;r-pQ_ao`Oao+sCd -S61Gym%gvdS;D9ICG5Src-jVKT=|c1#?4}#BfsFP~yAnjcN{ep*>YzLor&v!i4yduTZNt5_g5~jM7lV -gTRD5E6jA32lCl{Esaj=e~r;>MxG6WnR)#QxND8ePP%y%Zq$)8NmBaP;~e!oN4$SGPJctLQz6gADd9wn#V^S&3(MfN7j(2-9?P2a`@y4AXR2_@UhUIO(gT@ce4n^c#GvqnI7VbCD8Q0olX4;ZjDz2 -`&k7+9)`vGe6XIbq$aoke*?;*Q1i-N_8nD7#vLh8GLuZlfU3qIbCAjmz0<2JQCHD6Vebtd8_dlXqQ*0 -nG^_s!v5kWR1$e2_q3wS?FSSS>J@6YqRFpNx1&PLX{vBHu5sa -I3pF-ErRHF-<7DF%Hjlz#@&`%-gvLK5m3c+UN*!qivphP!bA^3N?nJolgr<*&4U@h^3r9$xWB)qv7IH -vvwgy7HVeYp?>JY^!qyN{*#lx7N+0M*IwqyA0=NRY#b^@=5J6RT26ICXDyQ6ETSsE+2hvNe1N(%>b0j}o~u#_JqZY3?A=}{1ii_ -8C^kXKfZGam03j+vuSi5V}BOFYE7FyiLg&Td`VD&_l?{uM%S4Uo1?0przv%(sd^gqq+@M0`{8!UDtwj -;rt@di}5_EkkbyKGH7R0LLW(we68|nj{VhwNP}cokPoEf!I>q?ILMO#DC#7^9Da({iGQ2IV+Tg+atGk -;_==hoYDuia*@;@nBz7hbNUyW&&O_x1Bd#-Qe#4e;8(!vcq8{ZaVe}7;TyE1@C~s^j;^=xNfbAw#hXT -3#(HGAJfUuRNHK?ZtpLFxqpAlVu&OcC5JHQbS}bh6rC%Elm;^2kN=qp&hx=qNyQP0HVwUT8;H@9P@ZHfbl!g~ZzR_E6h8E8?(;cr4T41M -Jdw)Xy3svp}jVSes;Nt_6VKrM*Ks<46c=K)8T)(3@Z#yrD0I;F&Wh3)BObY?5vH79pr1!q#`eHmS8lK -Zb|EunLbm8W0UQ{4Kh2xlf8)U#10|dlIV@-TKh7)4ivrH&9YFRgLMKtE{c -B66O-f-_0CvgWYwpLDYPLlWQmeJP)(U1IJZ^aoIX!ZAC$-l45zba)>_PVpfkjbS%?z@^~Q-bKs>y_;~ -F$~S}}`|lT986zsUgyW!jWW`VJ~RARZPxJKw_sw)`ysfV;jF~v4MRg_MX01-Lr(_+;TP4u -y!(OEAYsBF}JB*3Dx;5XK@$xuG>sK*QU}%*{h?4y*81$K&`(=zE;@H-6BG#PcO8*kMXwx!6N^%wvYNdar5E_%j(Mx( -%K>e;*V54_|p-8;(MMkvOOq<^xJz_>2axmI$Hc(Q9sN`QC7zLMAN!L=5WJ-^sD;&PeGhSX3fBCHt{Vl -w#M;Z*Rq1p@|ABjII__#d&DB~j{)`)bFr#uWP5z)yX?ltpH3&9ey_W~w?7Kqjc2EQkUa@F>m<5L+ufJ -b_wa`uYARFR`Y8Z>P%+@?q*P>ZRw>qH<>TNBCHijx^xBXv;P0o>N?6gKIJ?xp@>g;$Ck5hhd!oEa=u_ -)SiTlKZ+aCHfNT5aA}0QBC!Fpk7Q@OnlWucawaR(mzSQW>XGdwMm<%@tctZ&hAFOQujioHfj5;!hd=NM80C4uhBsZ8!oCOxrT#_Bj7WbtBJ59A0R8udaLY=M#@u{E}g -_hA1Y23@S>4v)d7U{o0jlC82>N| -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 -)iQnIva=>FMVb>`(3sTk{#2w8j>7ClG{QVDN -$d$4&=uvszB(GSX6-}iBSa>#iIb!8c_w7saN5IVN?NRC{-ssg|oD^>gW#gnw{}TVn~4{i6I3BD?`4f* -)vf$B}Nq3-%j-g^3>gMhYg)FZP;}{cN8+LS%)u;_{N@zAnFIpH$xJpR~_c-UP_ePN6>}n`D_|>lH16oFQx!ElPKf^t|8 -F>d)KhV{M#VOncpkeOk;!=*LbBsu)IRi3BsA^L?9q<-pQ*u~Y1*H`KN^9fb?ngZP~fjNn9(vKPVJ8u7 -L9H2ggeYekk|>yJ4`JeP!$-hg44A9*;44rV!n+gK=Ps=wP4hl$Vf^xj6ZJSO~8>KY~IDps4dE#pI@;BoS7xGYb%$tMG7U{qaUow+0ARTi4Pn8LsE{F>cMo{0KgIq?=QrPC -R%+Lrca_Oz%B3tOl+A$us1B(B=9M2E5yOlO4^dNdOtU5-onxqU2$P?&`3qa`{Ge9=Q_(vqqw1GU6u}w ->bz4XopFiz3tlt<@vvb_{{#`^>=N53uvzo8?dIrjA)S8lCY8}%=OCJavmS=kPC95wAeD};7HPGKWz+j -wM%WrMK6d7KN@n>G5Bep}U@!p2VDCbRL+C%m~2k_)RQNPX`spQg$w{+vcBY?myp#jMq*bl>c2jHUz%_ -(Li@Sma?96%`vc;K7&DXu}wb-=ZCITJ10+T3~OJFtN~tnc%;#<|KYbXGJdN58n>E;dVQeD}-wo{B=Wl -xTx^g1AqO2F70QAKaS8e4E~@5{O}@h(}h1&2(Bvr^x#i_prfZc!)1!jJC8Ss1-s?pcBNf$1oJL%xzQ1 -*wH32oRfHe{z^jEq2o-v-CIRtpb%w>PC`NWsTFr3FK`T{VQ;DYl5)%Q`&3I0M9vdDu+acfL2E3T~gy|tGdmG4Riqpn^?T`K& -js{{$nlGAIV+ -DPPQm2SJzU>`C&OL^E9i{x@%oKQeWb~@&`Gr`G+2z04b-YBxJ7H>C-w&4YzBW@p~o@&$eJUyJR?rV!} -_UMijAYeL>uG-(jo!dG9QjefK&uu>ZtluBWJXovizDC`ud^vFr`{7CF;~-)z?JZsJnQnX4ECiQ9-kQ@ -$#Gf3kJ_j{J1C?#Db&W1e!O8jxJ#ueK!KD;J*QXGXl=peoFPok()}vvKH|dDjaaI(z?D5n-wn%T2M*< -wDv!=d|SIzRP%E19YJEko%o}b^RiS8~@nAHUcBiMtI4h3W#r}nkRSSK}u`ap*o-5B$@Sjy|9*jVk$P; -HZ$QH$le=CB|Q3D9nCVeG-j_KWq%v-Ns%0p5eZg4^}}+#%wPtwQieCepVDF=U%qJ2X~CRCem3jF -0(hXsLxaD$sOVi9+4l6Wq=9_paS{!&;hx)HZ5Mx@`0ppgChM{q(H0&S>mdMGB;wGY$!8kunaG75{hD( -8)M7jj}(9CK_N6_I%)8A$HXw#deovdU-|qGfi{TgP!8jCN{)uOms3@8-RtRF$CGp -1XawZv;`t%*q-Gl+Xs-(C2-Rg?bzKGY1vAt3-X1})9&(tXHRv1}-Y-GUZmEzEhSOxkHV~p-v{?!HDvF -ZDCS5ffvhw=k3uBqSG^~6C*^4RL9OVi|up4v&Vn%8?vYdzD4Xl|eT<3KvT4p^HlX<7~djA$?qJ9YJ!k -D5&Q5^8-rr0cSPJKov~w@??1$?twl6`sj}ZJE@o3PO`EB~LS%G=E*`Ey}o`Yd5f(2P$Mji+}9LOsSARd(s -o0Mx2CxVJ%_<=Z4nm$u;C>2O0+wE)-3siZZ-)>~q8f~lVA16LvFt3xoWZ>uD%Q+iFe1L&s9lFC7A|bv8I*r}k36;5<*n(~l-X06;`mIK13x -#J}yq3?yYjUONAJ9U$b2DyEIkQ4681-)fauEUKLxTWGUxQHk0Vc1)lMB(kUA7xhXc6+F+#$mYLUzLd0 -_Io%%%q9}dteB>PLBQ$Y6t)}Ke`Y$yEtr8AS0;B0M6fp2qcH=6@ -^Sk6i{1nG}Z&$1>1l9H4_Y)oqVr6UR<=&|I5sK~c@&Ri_c!YKXES1!<4QT-&K#RKfVSueP-g*kOPA!e -nGGb}Ro*DeU2*C9s;TdFZv`a4v*BN|D`4n={h6O}0D0ZKaZ9d@7e)(Hi-qQo3;Q5d>Y4_-kVG7b%E8y -$dO)*NOmeDQ7R)~scQ4D%4kjy^O{sHju0KAG`c@~fPwXtem6VaHA!J@y|#)T7Qd+DGzzbIGTp#7G`i1NF=)2Scj2CyaE+>Z$to!Oa!o^+GM>=R~E+L&yE2(Y?J4{^qIKFY3Bl))4cLeu?`>&uNHHd)L;OfAsU@AH5;LK -YDX2f>*oVTa;A@Y|H$0AZLVLF>$kt8~7)v?0X@PyG5RYoa-cic>`p;avesiZWWd8i5Nh(cxgjvu_f4s -8(720x{zhaI&_vR%wHFnilrxD=`j>@b{htoB}oZ`&Hk*Bw*9R9YWYg_XItalzT`W)EwYxVCDrgD_PCZ -H9JF}XJ7b>R#r+hKS8(AGWNY`V~u8(# -e^%>*(8cjR8g9l38$AQ{VdgCe^j@*;5WoS_8Uo8B11&O2F9a|KFXi!N`-0zwUC+ -^G4h7)%--P|e5{~pF;HI_XIsSexAemkeX+FG3z-)*MDwAKkhTib#A-lC${uu?ZV=H -?TnJA{qSqcYAB~p9{G~0V*L!$vr_%ojXC=u``pb|Guc8IQT1LWe+zEO#cY?N4T7nZaJtz75&$eOyll4QMo*(|-Y!<3c{osnP(s}+FuI0S)#xZPti5c -|^?Au>XrDxWtgxEs_N-xC0ME;Gj*D*3&AUzH_6$4y4EK1h*B#TJA~V5i4}QK1ou2&|wzV16fN<-P*xM -Kj2XfS-*YjD!>v;nC`+CTy`3JjENgw?l)-%7F>>#M)F`ws!^u8_eeYJZUxjP?;xjP@YD(*{45z7kZxy -pnP`l8bMO~=);tw=?Xn+D6&yTdHo@gXM`8^Ny)gVZFMvN$!BL6V1j1EAVUPtIbfH0LYG67zkAF^cS+z -c|$m_|ZG&eJSqyTmr9RdgaUSpdQ%dEci`6w`i_9zn{E>C7O(R^1~s0ZMnpbd+3ui&o+>eQSqzX7GH12 -Z_%jVd&t-gfveg4S3%I6LTzJwT%!XKDo3rqc013k2j0-8h^!Bt&kHfQpwV02QR8Ei>QlETIzV579hS! -p*33uyJN%#DBSr*XY`LmxB#YQTHuB$}KP$kT%6wuGMm5^b*6G>QT_DS+xB|=!dA<=gg83#1XVL# -(OI8H>~HQSNFNc3F=`0&a3@Vi8w3ct%x+&;51@FF2#?Rl{}e_0?6NFzRHXtjVD?AkN%>!gra;(CsjW? -Ua|h}Emi*T#LTj|kCH$S@<1zqls;;)Fn|(iw||;0<^M}ur2Wlo1k`ZNxJXy&rAnF9x%&pF8L{{a^TX-0Cw|1e}K7 -bj(|N1AR7E=kYWY40(a>(p$F2-KCdP2E$$Y!B{op3~y=V&d^8NhcA>p$$ed#fMF`hIT~u=&e5gOsl1? -57U6diY_#e#Ir63OJ53$|zqvvvmDga44xHsxMNjY>bM!EMTzB)4)aYkCDJhD9*|1Bi;*Qa!_~ZAoL$T -3tvi@hts9w7<&R;eDL)Q!Q1J6f)gP#RCPlsCmK93EyOTA(NS62t@YTrNNYJA}0?*IfwfzioiX532DRb -xJz?)XuF8)PA}#W=E1PTNfm7fr}d9)uc=?*$lN_o-fNvz?8Fl08=sS;qB{F*e$et~~5BL%PaRtD${q -)mRemHLzb+|8v|A)UDuNI*q71%le{9-8S;uyhw{p^$hZ7&JRY-yTpmq6 -pG?wW^bMam=-bgBRZ_F@w^ebM)-+4xZ#Wk(XU9dIb_D*_`o-AocLzE^cTDehgj8$)PXP$h6d$TMfmsP -I3>n|g4g>Hi|$R3(o$#pNlhxdZ8>jZK-yLtN?810conCgz%XT87NAZM*o -4SM?nin)Ed-cd|_jn@rUlNZr)hX2WUnJ7h`X3pQrZ%1vX{u3*WB@&qQ{-C>5vACOJ=JeKzhYcB7+0@xy_}0Kp_21sPYtKoQ`f~}PwkG0Ew`Huv1M~iZ261 -Xh&|P4GGb3XWyGGEOQOrKW1`EYX4Ky;b9v&inwANn1u3yuO;=mvv6^~~SWRVdekAI!UKq2gASUCzpbZm{pHZZ7 -7QT2+ego`J;rO9X}#v#IAbHb@A9$`=f^j&TTXngi53XLyAH}BZ;*vUkd`l;#eH2ht7*bwV2{ip*Doap --RWH%Lao`dD+?M(*qYqprY6Uu`pDu2T5kxIZ>#j=nFBaDt}-w3RO!y3RRlfkgm3y5~5K3&XBMkAPMXB -#3)p#2#rUfI+27yQ{L1hm{LA+oYirS*!VK!oBwSfs@Kn65QvJ)R)`F?P-v;hBgky6UW0L{@M{UKGL*G -&dhgek4o0H7opO89ztsHa1fshAlDODaokD?83KN1+rGoU83ev$clD;;a6O4)~6#tcbt|0dRO+>087?H -|7Aj}=WFh-RZ3rKa2K<2!tKxPqR64~1n^k*<6l@tp}n_A*@ -#E=9IT_no*%CIpuxMx&AvqETJF)$|?FsGfby^#2-->ev<(y5>;@-x-Z+2W7fCG -1JAPQO)4Mgm}ryaUo*G=%G^=M5QW=o;r0wRH`x_mC8?1sr>P%R3w{SEGiWVSo_sK^pRvX7?r9lK{ETO -kD9qiR4SwNl0FQMeB?VrQY|_sB-M=*E*g?*5f4fAIq;-|A*ra_+BCW-JJF%OP@5F*C^bJQm0j0Iq#I+ --hB{X*q_9-?42GpLI{Q{H9?&Ab%hQk{Xa}%w|NVx-N--ea(nzr8WM?P*u -a?p{i29D^%6jW+PP97iJ!+s>}SJLRD=v|C>-%zc8N@s%pLYoKRIyna>GTh1lubP*n@f38AX)Fvmhwq2 -kR$TI%ydRgvaIb)BF^i0+MtshV=JFjW##5p)7!1CSz-0mives-$R_DITRtitaM~+bC5efua609;K=@i -pMK)K`b7ns+7K>^M4hk>TZfsbvs3=x`m=t74j%m1r((!FBYO|S)vg386O#ED(nbK07GN|hHT<_A*%Xu -)UPDwhnT)RvfAin -*n%BTB|LU+|oG5?l{FlmKL7l1uE^$K(EZs`&S{}8dE)tB8y4!)&;L}fL{W$K;%9jq@Uy -&)ax;C>ts$pTo9$tHL6X>Pwo^FhDfCqq^*49Ynds%JFW$?-d=M#w;5>5321qqVA1y+us9}I!^G{h8CO -F3~LP=x^X|vExcA*jftIf~$c?vS$0vZxn#pmObKg8!_*&mGg82k!xd_Md+od9pi!!I0m+J(c8e~s7}_ -Kw)Fjd07`r$jwuKGrDI*(LfiOlRO*3EuDPm-NLJpx-bfy1d7WzA9r(Oz0y(r&qk$i -BS5*n;Hy+m-2@XJh2F7BCd$mac#JfY=|8KsH4Ki%DT#r=bbD+m4re)KS7NQ8HerMM{+@3fX6*9dm@4`N;Fm_6*O(TvH(!Qg#z%%7v*U$^97>Yi;CB2VBM({3r{&^1Gm=e+| -h0=cU7?PlJe$1T9oF4&S(<{it@p;TL<^(Z4A!MI-i-Uzo{n^5dQ0-53@+?3 -*B-vYCWkOP`RYp3wt}UUk?ygDT~aK_DB8O -mKeI~Gy&3{1SKg$+a^WaVclmZT~&!mqg^_5GWVi`zvd62(*(zVXSR!`j5n2cd -Cr~ODA1c(lGe4B&hJM$H#AT6x<3e|B?;)&|T@#T7ckCMJWsws$}557bct_#yThDdRSDX1!444pC%LAN -P`5co_XaSv-8-m-HTAvIj-55EhFIvVIr)#g-}KA=wZ3QFHgm*q)s;#hS|y>7gN3XElYX#F3bI8h7|O1 -GAysjwml?$N&PUJfG%BWdb|G!un~f-EH#j~1ceXH0hE*98-QOGvr&25uQT^i_p)>yYpZz6BG@z)`hE25I;$ -wZ0Fn1DG`)*>!FxnD~I39o}7ZW)w^;b+g-wc5@3R-s@(E=LDTo3nt$0W{2;mIBzMK_?kzX9i9hr_AHq -AD%io8iJ8!w4wXhzoPH>G%Z3!)j(+>$HnYCaelu)`foNZ-lzdF;+*vY#Gg;S=SBJ>&R4_?DAr2j -iF<8e^zsQV(xUj@g>u5Nl}2u_NYcxtwL7L_}`cJHLORH>b`l|L*2ndpBP#FigRZ?nBC2ZW%Zt&7(1F4 -|P(>Z0@7y66pqXooRN?V>&FAR@;;h_v6!tb;af!b9Xk0$!$7K8#d(EjZa#7tKbnj*l?B{TS<=U*3oBc -WtE6{hq#GM5r8cowr$0^=?X{bjd8W7z?e)#h*dTHCAafT*mUTlz8AR*r;#wV+T2cqsoiFjeC8NerGw% -9wiy%Q6+)uUh&!Pxy?sVs`Rf%q031*bv7NHdvx>wu5m?8VqciOy%d++pi0&P&uDWpliNqcfLG4#(^n6>qN3z=__Jiu~9mViRV0e9>`7|KcXGn@}3B$&V -T2pf9laBk78ppd3=;=>})8atzb=uJl;~S;Q=ZwBaI(m*-fa6489ONVgp4Iv6Dc=|&0G8cx2gHP{Yd$g5~6HJ`~WBqA0jP*Z$8 -KsXYOWs!X8DQ7;VWrn?AcPSm^y*T2)?!=@b~{s^eT^;{B4vrgQ-HAxUqhpFS9bYdo#VcROB-p}`VO#{ -=#>FN?4`UVLYf6H1zcA0<_M)BmxS2Ub^#Suy>;025PgiLRNL4Vui!RwUO^S+VL?l#IA6ui;N7TlQ%V1 -i0e5<~z4$3Ik!0C*?l;9Lp}m(<(TpXlFM5HE;2;Va#1Q@f(O^fD3%h_qO^gsxhcHq&eT7P*rqWrOsAD -Fu8cu~CCG^Y^>=a}u$sBCppC58bCq^0r4UWNJYCvVvY?o~M&=T70{qd4?+Il>|^dpsDR$!Dc7xJg3+n -1<~-2HJ=sPIgx(__hDH+iPCf8R~?N`AuyxY0DVm)5#Aa5)cKJQN -8r#`1=E-2TGUtY7=0rXXI@HZ)nb0qO$mKlXtRxW1m&V$iVLrh2Q=3f)@%69U)TMvAlxTn@4 -wD(e=(xq0xl>-DaP0GjnXBUI#(sgVYu#Axaib%sxst^BKsPcmp-%Kn@cTtN$#j*VkX&vYe9VYQzPMO9CK}<3k;Pqje6`SDk!S8| -J2LsfL+n-A-QhU96$*~fwGX$&0+ZC}>>bxssS|aOB!MKqHR1u)IHzZV)F>sUm`WVw#psr-$^RRC$2lMVa|W23HvA8|~tBV{ExNaSE<{i?> -W6sD<^WDR_5995DsY>59Xr5O36n2Xo*44dJN$LHg3!!}uni;dS~O-^6V$bbfw} -AB0R68gk^ZEQzV}agO4Ng$9II*YxU_&3H{@`~(g~zp!6|l!r=?{Jtkmqgm=l)>m1Al#;et-vwK7jWF2 -RZXrNW&Aiw+G5W52-Z%>USu5V=c=c11gb*U35;K5NDDSjtEj>QWewAn^1!%wj-IM=0w>jePCj}*@#8h -@h8;N3t`0)iea>BrOLq-u@#PZdICPKs8O57_8YJ8Cf -BsKkBuRweXr^XWvchAS`ERGlCHy%V%;VF -6NZe#3Ghqp2RRGiZn)oEw})d|x4ddXi-iN)R>Q~KtRni9VT0O=+8wP13XtZCau9msC}on|H`-?+UhT- -cUSt1m`TJfTR@m;4^rUW4$*bL7(sxL -R;~I&FV#TN~UJ*`@d3G=M>Zewhl_Jzo$EH`a+tjQ@n(?!#g=RM+@yp5g|r?W4pmD -5l@fRej@eQn2bI_R=uE*eaWR`}9R}LT_vyUhSadoA?|!O~ne@dE;y6*%2sxzE8@A}C42s) -+a0oWJe(~($Qqx7K?YMICBF-{3xH;48zm+n&(OIlyEbeJy5Kn8Fv1~V`%KRry>3Ki)W0}NzG{eW1qz$mfIL^!>tDT1a+7^`o6ZI@r^PZ8tH+dLcQBd) -#68m8^hNTm?||_Y=H{KftG-50xs{+E8BPo-(ZrCgH(YsG)i3ZIXvr!yAIJ1I1+|3eAG`!(*vz3p-V$U --e&iN@s6;hJw!WBNiTmI!*2cAaYb6MG^|t<*D6ljGNxxa#wby{^@$lzX}A%;B#2QFge2FQmc5!|TuY6IBC!Bp!hhq@VTsF6oPl=~ -~Qjc^@v$mX0iYjB4%v=uOtcj>EgNyH8ME26&kBlzcB;#66|npv^yl(-_wc+CEliNo*^7>I~^hE~=i_()vIR*_)H7rVe!%1$aX&2==FiJA&z*_a~BD3d+zcwY{ -y-0fJy)-#IoN6jF>Gz#vPr}MCPonpQ%0)k_ag@HQHgQR~Ve?d>G&?+8nXOybQ;_s{4^SgWKKj>ns2-r -i$Jq@!YTR>`g>p!-8uk9Zh3{`1jfKz~tMvOSopD0iY&^E!S~6P)bv_j0$tW4)3>DI5;8BWJdc%1gu(3 -}@2~0|SJTQB3*-QH}~1{1R?MEQG)ryJQ1hu^oExXm99A3zd -s@cmXxto!($p@Vqo}Q+6oVMqE}wk9|Chuc87>+{5d1`z+(#COfiETLvV*dn_(&$$mVw`IJL{9y#s1BU -&pkeq6$~swK}>AcdeGzCrnHWaSWypr08D<0(hEYtSxjKt&wjeF$Hul>0j5wI94y$leWhovClL~B>{9( -_p)Ylq%ro=#t2+ig91Q{f3O^Pu&}vMy%xBGrG!qfuz=8hy9;Rrbk97t(LU4^vqB>pBx-?~&=-xT{e(- -WYr{}fz@D^2y`_0gPI1}I14lFI2-6zN@?b2TgD=J~Egv8^Vm2f}tAxHZi%m*=@#^WbR0qYehJ5H^+P! -ZfKeCeTtO38i{;?o!1G$Av)9=&XJ_IK+u4gIQ-nL+mQq)}gPDau{9Md)W4}|8AgctCfkEqWm+a3h1q3 -Ddb?0Fx@59a+!EB$RYzd@L0gaSbdO2o%p%XJpd<3jTpbJS_`;P*sXkdj&;+?=8&afVRwO%UJ{0Uyorf -q?g7s9#Z&;a|6A$*ztI1$(*Li}|9__cU#9=J9FCjsc5N^TTXLLGvJ^gt0JFs;=p?t7VkdbN -94@#0vC5~W2H&BO_pWp;PWe@lPd-73p( -+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 -;!jVpNG!L_DEbNBJ*Q -p}ts&K&PyFAl2>Lg; -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 -ZH%7_958X$~=UdA7Q9GCv0m=$-+DGo5)lbe#xP -I&`wSk;Z53BDG`ljWoI@mq*yjvN{tT2DOt7=3%_1vRylFZS-GZ6@mO&q9H9e-D=3qFxb?!cKnT^YZYz -#S)*zEc7V%iDx>+VT>T}ivq1x4F|(S3cvr7Z1q^BrDF|d6HVW!{7-D&u*5#>SnVpqm1s=)H>oT6TnOU -MW&z72IHKb;UR=Z)?Xh)uX^zy8n(bk+b!_>eX_@<4oFg5U)WzWr7ZOsu?|8415?W?nLQfVba)j^1ncR -6xJs{weRLo}Ubdh#`GV;NnWv%@@ZiCcXpyCuWoUz;UnBCF(ysdhK(8%x%$9%*^GpHAqfmXEbd_qeX1c -e<6bg1aAsBM-ASKY9Adn9l2G*x+G?R9*_?q7Ers>wn-dC}VbZ?pll(LwA;~Yr}O-lP>trzQTbOo@W?t -T?0K$R))d4+9s~Q`Pb814y?wRDHSpb&D82*W=fRR*44RuOK%WwU4vZh%+QG)%=hP+=1JObUp}9{&Ggv -MKWLXN-?|kA%J@l3aD2&IvH&V+{Sp|1aG=Rsl9#pGmTg_8_M=PEv+~3x;`r!Mw)ExX27VZKa1R~nksd -&LMEjE-bM>`><^HzeF>I~rei*P{gD<*;`9BEwNjGzFA1&!Kwx=yLB_tBVV0;H|BII*i*x`OOejdbcta -oJZk%8j|9MO}$VmHz&JrL3hAo?gjyLY{|)+33KBX_}=Mg41uU$h?q7n{akL;Rs6nh-w{60Ij8V}lyI) -d$r2mFsLiDc(uJz5Ph|v2NkLJ&5;QkL#8X>*wYukWJvr -!vzSO@j@tf=I@}nJ3$kXHTIue7L01pzN2__mt05PNo6GJl0`=YxMgS}s4pZc)cp5-AnO}_xrFI`XiC3 -hqJpq~594QuRK&uO6dCVD7~9?GJh+m;u6VRhP6LZ0M#={*YQo_+*qf0*>wyiK~52iW{MpVWhVqtR-Jm -UFqr$AkE29;NZPz94TBB>IpbD1Xq{fO3tm2k{;271=w|pA3k8mJHDBChfV2#a#;mY46d#}_?f=KF~hLS9NGWF(KT_eg)zH+>H2o18@YM$aaFH8Tkd!Dx*SJZ -Sp)lYU|t@KP^`wYeNZbo3^@$A*@(_6Hxd-7uhS*x2jyUd~EFK3mzwd -$fMU0Eidr2-+{yO(UkL(&?6L2m{|7UXDwv2>A_|3sgFV>PV;vU%C$og*=6h4KCM&=vdo_e1?D>wtr5; -q~L4)yLe$m4k5pXZ?_tEUq9$WgMe<(mja#p-cQSxr0GyL>KEHccYwWg(C7M+UdaHV&U=mR!QAod=S%u -M1?8&`uGNyr2mKx$Tdr2bXyyO-irB^qgS0Kn*>9>1& -?i?#^A?c&IRAW%kF8w*aBfgAZ*B7o(9yr9>TY!5eR7jKO}R0umg|LHm9R)&KK!Ct!+H{&2x|jeD#+Z) -P(zy@aWzo{QEwQVf8(0L(03`f>VN$g5AbCHSCE$6VeEu2d6{tfjZJXSr6^58|iMRydmqpse&R$H!)yMn9!f}z|&M1RKLM#pQ;XT2cp9#P% -w9m7Ss!s#JgH==*pzm2cDd5~^n{UTL!N7#{O-&R69s$@9A;9h>fvmh%#f75Gjk4BYaRR6`*g!DK@$gH -2;+pJrEI+k&)>c0v6zImLGES0V|K|8tMzuvdjr`*e?P4P_fh*W9Od%Qmx5`CBF5KV?>XJdq+FQj)JUO -x6AAy+mV -_=@T8L>7$9%sPcu)7v&sp5&-c8%w;v884y4Qh{0q)dJxo6H!`4Yd`s$Nys5|WruT$EGE}@uhNfR3L!* -BtLp48=woojsG9R9QXbbhD+Ln(aqnISKZcLk<4eWM#v|D0s_xsQJb_V}*zMaAUE9ct>Xxxu+mTG2L#d -tQAw!ln!A&Jn=v*N;DQmUA_kZcB%`zq_q+~sWLl{`=4?N*qGtBbTiQ5u)SZih*}8o$nKVQQ8*J=Z=jD -`!Qvm4{soK69*g2pylBlb4%qjTi0N32Pu6Aw7vPFE49F4v!%@w=GGMu9;3V!q;vg&95*`EKq!Iy47_y -wT}nbCg!F)vaQpzvaPqJu13GldCtML#ZF6;<(cTn7PF?U7p=*;3$xO#@tLW1@*ppx%b$^uRT%d?FI157IjuG_Pp~XBCYfpQ4?P5Hs%^U63i*4F;znFHA>WgP9MNizw`Ylvk?eU;B3 -x1@v7Q!d>Kf}c_BK{lJJenBdK;#)qXWMf$w{rclGaB5&$RP%GHJ2W6V|0!Z5SJrLd3(`uONN8boTHx= -b5a_9V=E??MZfP97`4$UqU4VQqz&?P?ACOYiMU7~fJ -NQR&t3!1QwI6cfcP|(!utPLHDY|KfYXUoco&vih$-vPC7{Vgds3#mrl#WUh==8q|5+qVzsXOU(LtoA% -=dY}4XTm*e0rCt*HCpm~4<6&f)iXmnvBqAOF -U(g`$DTdLjaqOBEuxY1}qsyz!ARTq*}=8`@;gHe8tN88>T<_yLx#_7;r2#I1G!E>~bboI-(v2$J>&Fy -yWgUGM`Q17-pb@KPYz_=4bI# -p)&Onx(TX}Bmwf^6$`y1{7jM>aWwknDe@8&M>dj}>C<2anmVGM`S97b|DoI?YLVI1l>Z1HCB0*Cb+{* -}Wz4$pE}%V7D^-4F1SrJ%{@_+{5ADGY9%-enm6e&)fdDx&In18s~N -%J|%?LcNnjyKD-`zdP#}=o|3|3-1MI9WrKefN9=Wt!xzXopMzb|;a8`J(VsOGg}yHVH3T6w=lW_fx4OD -a}dxTSaf -f?{PfR^4MzW|P~8Yb&Dz!9G5$NV<})b|JfgCQ)yZ{Z68yb)mhO%Q&VhNJ`h3BDw-Zvi-N0LV&!#{gVE -fce=7u)l%f27u!Sf}D5@_yJfukfnw2o!6e`>Oy)hBJIWz^8{ook6~HipF?1n>}i8^O;JfKx#X&j&wo0I!T?WxoorH45?!_9U8+Rn -e@B*#N(SuMzBJfVYhU*$40>fK}r`E`s>00iGMr^7&VQLt`LsV2@`YhD?HZ5C`Zr1@Z>C4&bt>PzJy=0 -lso8A%6t?Re)dK%Hoj$E{p>nfcjbla7`R5OF6*$IOYc-E*v3_aJrG@IR#)5e0AWz4B+$d)dRj4;9ubT -0^?e698` -nm_HZNg0dj|DH-H>ux|nQ!2&|I0bU0X7q>9}$VG&_a67A)KLYf;gOy7Qa6Nqc!T&aZFWtfX?*(XusaP -G@X9B#inB@&&&=M9FVf<1;0suDwJi3(GR|9Il$Z$hT8xtsnC~$pP1!@+_{{E-2t#69b`AaF8~~EW%-W*xXQ};4dB#Fm^T3)2k>q9Y5^|?7@x&(6 -To8}uK}365@-uJ!dCbQ)CE}uv|7bzxDjCWDqdcIuV#Z6@KX-Zv>JF4>ki<%t64pM0q}SZ8$JA%28+-o^Mc1>oVkSXy{?<KUfc}*E#Qp+dv0OzhXH(I3-iAR;3fF70sj%;p6&3B&`>yDVt!%(_Syk_33w -R5**kds0(^1@3;PVfsilmzaR49L#oEGdfJYvNHVb|bE_#IbQ2-}C3UfoSpA7J>-HbjWz{|TCZLb2{{} -^vO0PlVrU($p+0{F_~EDx^&{ONHPXA8ifC-^u5uznAV2Vu}ttbagw{%O`X)&u5Fx&?msR2=F5Sv;U0e-Zd0X-3xpRcpO0cURKw804jTV|MM5XUk2X7*PdSiI={l= -PXc)KRiFdd*8&WEjr9Xz0GDyx1~BY(@Q?NYKid!UO2EGWxZwaH{UU%q0M8wSdI$Wk0PlDM_#SW@z-Qh -D9tIrY{f8jmfbRrI4zqCtq2Cc^9{_Om5k}{HfRRU`j9?!J(5Djk0dNDr_u<j90S(e$ -VkA0VaP4bq(<#jH-jMfJX!L{|L$gcmTk;AF()-0XBTZc=!UqH@;wP;Z1;ZzGQWj3~<6%z|&wK15o;k_ -g4TXe$Du8GQh9kYXmj#}O8CKM0@X_6XnPIKmG(j_`YqBMiLEa6CIdlH&;Ha2(-!j^n -xU-5f`FkmCs7<2bG(;+bH?5#qU5%p1ZiZjW#a#}V%0IKo#rj_@SM@to#&9RL6HV|R`{>HgqwV9{=b?y -cqH9^p2&o#y7xht5;>fIZGnpxesFaJ9I+Mo<|%$;W*~@RW~}l`w#~cNnSZ7>|bDloUF#r*p$j?D4nGt -!muuw{>B^s|)+*yRhHig?)7w_H|v@%bnQcI&2Hr>z;ys1&H?rzwF)kjE0IasyXag4S8PIW-k8|v_G#l -^YB-}eMp_{o>DGe%@4 -S`cJ0QE=;5MLbM=%`(+tj4VAg@*B7_C=+O=qwPu0}aP&1t#>)^kO%GC_&zIJ1y?g9i>D!F;>DP0SO&& -asS%v;cZ&8duxQ&rr2mAPkw{Z%wW{8zypf8b{ei=UaZo2mbrnp2g`qM62j9@A%LNMBK;Q#F`1u*3XaJ -$3m!n3~X?=6~ze?KM|xnlKJDx6WN+1`k)WA$~A>y?HF;=G`@?pa>aAA6?x`1wAgZwE(=Dqx+?JY_O%~ -6!_SI3Yj^hf2wr7jyco#Y2KJK(QV^D=1i^*DBn`=nSU#=zyP<;6FyBoVI55upN9K$4|i)9-xm -0ke4>HR6FwjK^zaQ(kk5lSum2jKM@L7K#Kc50Z{9qzVZ(-YJYH5-MqYgJMe^#auTmcW;DZmymtTJA;& -U4w4**%wws)To*2LYmyOq~7-XUAwx%f`Ts$8-Kswm?fMcML>a~W{Lsx9CBwCr_2wmkmjVnw-4dNr}z?X>^cv}qH0(s3*K!-gs3mCdoFa76;yu_~VI%r%ln?Q!Idf=T41d^35ZU^&^h%SI -~iokeP&UPXq#t&kCi6f&k#A-5h;$hhMQnS4?qOHL>x{;Wb~om0r1^9sp>Z~kWrS^Tv^(&`nm{DMLX3J -OR`NeQ{{zWc~S4?RR4e)wUsd-ra#XU`t;```bb*6-fEd&z6By+-@8H{X1dymxpn*?LJKuQe*<=+UF(* -s){e#EBE+^y$;&?Af#A+_`h)lTSV&7r(3{?|rY3Pe1*X@?^Ey93|&Lu2m-4cMS`CsUL$WU(@btXJ+LyOjILYsz29S>?3apOO#=KM2A{Lii~V-U8uohwv* -P{5mC=Y=`i>A^e{pd^vSKYA -!3Nk-uH#UMAqoObVT2|4uwjA<|rQO*PtrEUySKAA%lm=r7D+((qgzYwL#Eqn-s$F;af2tO6VCqj5!(} -20YvL3>3hwzU<_&-DV3J8A&!hh)&9)yo1Xv#JaHp+Y!Zw9{Ml^VD|=Y -;uP6};)gGWw>jZ`RzOE22a4BuALTd^X+ILb1@6GV-8NMgOzryfI44=yID;Rzq!*6H!&lsNPOgql-=NS -Hq6TStH(1S;aA`2rGGnd>-L6k8q_nDOX#Qa=kApH%F85OByM^t|8^tK61i8#qcc|-oWtv7=94Lk7 -xK<48NG+S26s0hTp~Tdl>#W!(Y_FSJOD~B*Q<&@IDORgyCB-d-i{K0It`ZvQ3-MV>e -*DaBzaEpP54>uVNVMc2U+|anOua9;;A~MuyF`CRKh6i4)TKM~iSnncfiL}Vp^(l|K)>f_55gPgWcsJ| -@cpl$ki7;5K4eHeM;Ol&YpLRnNE5jR%05(~yPt~bYPrWX1Y~<_BNJcovw?6sTI(2$<5jXgY8+^P0Z!+ -Vm5!e4(r$G-5wVur}1H)Sl#>ie4OSr}Ai2>?))UErs#~**ZeoxGR>ye`I_?{(hu!0S^ZVHc#uvj9)Bf -U!8z}K5L7vqOTnl0gx=E$y3dp@b%fa}ftTJ}`b@l9e1$u(;*ZqWEA#&6v@l35%XZiy5`h#4gR8Goy}b -tq7<@ED$5yVj{whi`CRx3+)O8X6GJ6ZGQiPj~GqP^j;`ZmLzKR#45raEm#Tx!l!D5YeEkrXGe)M&mP; -YgGv}SR<{G5#bt^&cAFBVQL#@3V6h=MlbCGcoktbIAN-s5+OHm9i`CR#Vip=Nw_3J0{k>r?e&fMd@q!49RY`C&$j7eEPEnLZ6l -B{$n;GBZ}T=&DIM4eG{h9o{NT)0qKx^$_sY}qm;BO^muv0{bD71pg=C-T7$Km1VS3J3GvQm-7d4pqt2nZXO4^RZ4AIue7G^N?$5aMpL1ZM#q&kbdKZq6&*YW#6G;DPvBrXzXij$XZTQtw=n!bh9A -xFGZ}sv!>?ud&l&!1ocl9R`7=-X|C*=B_b_{Udivm4PrRtv)1z@fKmh)@tEac8r-!Fcb1(OrHJ@fZtL -N+2xN+0Q0kz$0HG6{3J$)KBZOZr7cJ~VK5Aav&`!@FrXiy`dHs4V5i6@?}@6*^X;ISHhPitRzd;2vHs -8;Q9KZa@cmzqy_dem(m@L08KHL6#s@_4gawLBkr)U$bm$Ewx97Xj{GPgZrS>KlO8pn4UI|2L0Hb@=SD -hBc~J_h`h1l^8t^&K?_d9~O&j_L1o$^){CWQ7{{D^p{eAoeef7EFnO`3tajHzIg=Z)g^#=f -zm9ihuans#{`|)7Em%2ZM7Zs7Q_|2Nu$Z0-VIJo5jfmzNiqijG$RHmP2{I(r -uJCr0tQZoJ}~J%#w$faLhYYL&HdAFts3;48u-)Zcsg^5xHS+}V2h^5yS;`st_h9DB}Ox^(FrA5UJte* -H49OV1uZetgYmpM5sR&CSg}I5;>!qsc=X2S1?zAMS^LkTnAHUz#*&(vXkfRm}>*dJG&m(9oV6e}3xl; -luNI9ZfrS?4a}K&lAT}I&|m|HmK<2$&x3Fg$))IOY712_Df#lL -Rdx@~69o;~21XPyD?Jj`aZAI803{I6ZRMx4u}HEh_h9rG~t!i5WzmzU?{^PW9>=$mi85qPqE1n&5J>e -MMZcI=oq2Tihb&LfVjbJu_V`R8KRyIVYF(Qm)~Mpv(1y>{^6!7bl?_uWaJ>sQ>zbiT#*){b#V1?>Ie& --oPQa(3V^J7|y{GLaqlOFHCd*^jT$I+ulg$z!g -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 -g+&U-OB=!=qpNoA&{ -rsrCqJI3mi#~>GH5%0VP-B8V`hTSQ2RT3TAAPOVwf+uV&~mlA&VJTN!(97iYe)ekylKdI5B? -8lqtjYwO8PPeG~8k4g2e;HDzn{KGd189}0a$jS01l=~{hh(jc`joE9r^5c;_HZtdH3e%D&CYYiCk-#O -DD>RqT6VNVl#<5C~7FOK>OY7?TyrOiKia91%`H=RCq`Ld+^k(14yJv;iHcixGDEn>Zo{2X)x7w`bb{h -6_%Zispn_EqiL-u7~>z5ts*eFU|_lo+#<2WWa9joPhV|6yCNzWQoB`~TsxK7e%r)^^|td=GeWz33a-{ -Z=1QC&2!6uC~9vLT3}GGtJTJWK%R8(1bqnyf1P;{!2;wm+LdwA##3wokd@}1E20yGb-HmfvB;dRyTuj -NY-kNy7mYEhxzBsnG^l)yYEJ!HiUc}c*u3Nd~OhUfEVybxQ2%MZO*HAZK5=N)ahEO{vSE^q)C(FXU?1 -%2R_W3H&6II_+7+4_eEIvu=Ls!lopv&JKx~q?4@9N{yii$dIFc`$XD{9lRv9a{V8*hm99dMGqK>A9^2f -VpfbdBNR -qS2JwGViXBls_$Z3I7DV=@}XZMp=SF8_WJJj|H{=W)h7KJ{{{H^dt5 -+|9o80%4^`y^;O`&n2#1yEa2n*9sMB!vDelOrJh|IL5=CJYqh44r&W8z4Vf(F~cr!FB*6t_jr*nq -o#tqooRUIKIV4L@4APj-;+Q*8Z%~$@C|rYRqiW7Uf?z8hEFamEL?S;bBp;g5BnP7|4YL|=D=&#tf6Jg -mWjP$_(tqG$(rH=&MoH0JTm@5mq4$~!C`x{E(siq#QtE_9RkEZ -3zm#??kY&Y3Q{j_rB%73j|wW@^QaX1{8d5lq6Sy|$HzUPt5%*=@N^z`l7+1Wy#GKN9UY}1GI@TjNl+_ -@9`thacKQ>=g2VTV{7Ltk@v4f50@f;lyjEJwc%jyc -H92sH{$t+vlOsfqbB)(vORuYAU@eZd7uJ4Q_hH=`#OqGvFvzKZH~bbF)-cEy*Zq667(?GK-ct8;u#bQ ->8~6t~AqL53P`ADwF7hqpd&u>WYa-Xhdc*O~Y%vDb9NifY$nS+0UJ!L|_%QI{RDK3+Oz9%llgQnXw`1 -LawbSI7ZeosSPZF(WzARn3ROEwrMg(;=$P)VEnv3t0%OJH5qw)*jXF^cHVftEvP?CnmHrT~?N -c70W7~hcKfHHvZP99M9?7;yga19P{63yqq0g|5~eT;pKW^+WZZvFdfAO1Xlc<0bz{J3|o?TGD^t;nWi -R?F;@**{ZpcM}Vkg7hQlMd`)q)iOLX{4#g|?VjzC?VatH9he=I9h%)edvo@Y?BZ;XoS>YfocTG6a -?*2F=j7((9*CjT-#<_o~^(Jz7^T7*op<`+%r8gy)*qX12cm%y9++P -oH;BrAu}m6C9|w7=Fj$L`@d`d0Z>Z=1QY-O00;p4c~(=jc!Jc4cm4Z*nh -WX>)XJX<{#RbZKlZaCyajYkST}oobCD$ZZmt6 -YW-?;$3TynlFv524&jHk+4K6HLl!I;mGKs -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 -8R -DmM%@jsKBgpFT#I(AAsWXE{wM)IiWEqg=(K;BY{A=wB*aA1t*(*f9y4hUe|*6wVNRoG`K&=-TT7hKA_ -4E3{+`k1n3mEsXlL?HF>5XiH76=?RPFi@;HBJz=balxtuKK+@;Y~h{F^39t!!(g-g0zO0p&0oe$_8KV?>f>s8_7oJ`SS3@j>^64=@1N -vmHN)C5i%bD|0~X -e|x&$1s=wylS(jjA8)*O$18tE5Ihs>H}r%u~l8pUQA|(wm92qJfkad)E9v1E%YYsiU=u!0WKigX;rp* -qF8nOR>>w0`rOsa&}Pv{gZo|C704$|6XvfSm=qFG~=$8G^V2{Rg9t?!YyMwzP<%x#@Shvo_PjR9EQAa -+$Ix*h4A2hvS$>058}X;^4(@nLK8U2kPyARE#%k(H{N->yYtuGZ^m!8kKS4>81)nn8{;?BO -XxA7Selr_=BDbVGih1+2>74Js@fUfXkwPM=qrFmF%Syqx310L&zET|r)m1#6h&li_bc!)@mt*O1vxcw -9)O*@-^PZ|5QO&=Jl#}JtulPW@!&msWr3)$qaqMET>9jE>m4M<+=`%3^u!~NS>rmZO+V5Ol@RYTXjT$&I}F%AcA*^4bcUT< -(RHP)-$5k{n!!@tLG1Fyuse%wq3k&4ALxHo<~eS~xfFPC(rD8GPkTpQJpJl{!(|seqNAOOj^(h1a=BL -L^U=mRhm~nIZ!kdmcBn$3=4o}6%;^k?Q++(1-?N%E#!S2j%_&i(7%h`>I)bE8r7#vdO)9nB2c}h436I -$js4#Bpkm(#Y#P2H%dWA>2rUqIqypDQt;L;g~dIzDaj+GLZ8&pBo0z?oHvr1sIa$d;DtW1pof1nC>ltZ;ua}7UvZe1BG1qyNYC-TB_9 -Lhp+O)E`}9*AG=q@`-9^HPT&EI4lOZ~LOHAbxVVi28doyKSQ?fH_r(dmiQrrvyYa(cUd_97Z#y|1Goyn}D@69D1wv%%Lr@BnpO_u}t%5C6P> -v^y}5M42JA?4_KLZ7Ym}`v8g{aSbalo&~qcDKYEjL{ki8`5AD3`6Q$TzyyMBj_|De{Be7^Dou|KvYVp -D^I6466^u&p^vRwS)AkHH#)KKFoyf`Q2_=0q7ID#p;f3M=OvPPF` -}`*K>PGU&S5w>>rPWsiCCs2c=Zq^PFRsWrz3e(%iM%j#+O7n;_iqw2YVdH) -4GK)Y=gk*(2XIU3eI%6NgRBvC7gtr3N(zH3{6OckI$)94Uv~`<8U0haKl&-3FXvrhCw7s2;g -F)eSwEQMn4_II}5Z66$bvwggc2mw5m5T6e*(}_wvv_HfL1;gd$>v}Qch(|gL&|=5CO&Ob94TX1-W!_R -mr_ZD_NeZ!p{iD6VaqUv0>BAD~D3}sj7(!Wvb#PUR{@1p&E}^1$JoIi3ps{Ea{wo~7s|Ro-P$r)A;8; -IERQ|cB8)vx9fhhd2lUmd!PECWmFz2A&QS=pUm2$u=2y49^$IpNGfjQ)dA39;1ePRm^S2qT -x-)&?I(fpS<-fnXQ?Q#Jx$XItqVJ(XU_yY_Iq>{V -rlqs0RZ0+(=RR=|K#h8$%9fGfa)Q)eW+#29iY$uCP_fU6l6!C+6x*0>g1f*w;S{_q1RCqMk4MAEdvV0 ->UAN$;TLOSrzi9xCwyaJHdtIpHY#3GIyk@B>GPBQS4$vpPJ83G0xHbGkhhU7#;f8PcI?YulJGjJ*5Wt --r)pdZ#*B1@v_oP*=Dt9WpmlS#TB${a$T91uvlJi$LM54rKE-6ot!2#zAcIdtwqN$=AH$$=6(Sehage -+RADTbAzsh0alN!jGrjxL}AA~*0&rFZKj -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 -Z!2oG -KXQbj=PY?IHtJyEsqd~2hdxe=nEWd2CfUI~b&V#vdbTEp-c2=un3f-AJcw&qILQV2n7a)k)r^7;91MmD%{+1<2Qt*>0H(-2~U^KFXtuRe(qrdN?{8 -wzLdxi$VEQQj~Vg2pMxGWoMj6(%Yuw20EKTk`{Ms<}?iqqA099Ok@zl!|7x&&UWf>SQUo)sxgM>P3FC -WVAXph?g@byxjY&;@eLbbz6yy@fE}i%hVQT5vl7_FX#Zl?R!&hf9Y7?jZ(I^UvUX9YEw+#rnXIQX45? -GW#iMse$f8J?p{70`jQtbRD*YpWwBgmzSWCKeXbN`BXCZc)V{sW)4K(%FsXi%ue=hk{L>dPbiuoo*>w -}%F82$x+Sd_W-Fk#9+2ODnb%4-e|`PBuf9QxEu)7$rJV(;7bZ&XEsx~xlsE4{@Ome$TyBaQ!$9@Ry!9 -b?Og5gU>W~-uIEzvo9Z;-Vnm4PeU4oW}Wx_-vT%1hNl6!hg$4A-bh)8F2T%zP2#ASfY%7jkJ+twvECCMQ8roq-2v>;jL9hq@xMNyk*M+|r<6keF-J -@p}G)E}q>aRNm17yoYQM4PK*Z0AXX@+}%{=*ZOe8T4zOsf)yQ{M^gUIN1#NTIL~>y2e1iSyq%4z-?~R -a#s8pVCBIjQFI#7Ogz~ -Ba*c+k?D86$2KrCvAYoQa^2Jn;+&>aO3V>6&UlYEtmcUTL(*|0_qau;;N1iczgkW(l$Z0b`i#N%E&Wp -<{z9Z-^nKY(bn7~QfQw(y%6wpCg2pWF~(#sfszRQY*M9W)YB-(M%J@8Uw&>sN%Hws5=39?30arWNLH`Eia%ek7sY1_ZsU94%ksA7>yK0@8K$;tX -XKdt|VD#CM0h&b7gq8y -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 -VU6fQ5@xD}JdPGo`y9F=3)aR_ -zcgL6y8weOkjOCP4?v6o7Sc|C%MNtkZgi_cx3U#^_?P)H1<^XtG$;vG9iHQ}u!~;2=?~7DEmAQqbvJ( -T*aOC&BtlyK{7Fb!9VV{0aYxu%DJnZ(L4+ev(A(n}jSYB_Osn%N?HyJX!?cR19tcbl@r5()~(-T0dP| -%0mmlgV6!x}mVZRkLA6Lr|fST;D+2qL986za{qc~q3`b~Ky*oZZz7)PRq}da|KF5X#Jky4+wN`_FnH< -&JhrIkCfU0n=MR;~pDa64gUnh}oJyMP7%nk-&EWE_DpD2Q5S&Ae1r#QOs!&^z|0qpvq8eHULTigmuKP -1zjZdf_9309Q2vp^FG}z_kjWc^H##W?HU3p0KqzVRohrW{Pf}j3c8%_EpPVJ?a3%Jx7EVWJ}7ZtvymE -{Y$%$Y=Df0}?rW)Md7D!W#``{#@TK{gLUa;MymK9O&06Qx4DGyuWA!Kk-t@_}~ -_%cnfk6#Gr*HruWY2XsL=6*C_MEFv5QLGv)=`pFQM`F@=DhSnPl;8})(|Wk$Z!X|J%J2sdQs32~jQB}v;?0V!e_nTdlfC4XOD#>4X=RNdZ5nwl3lqBdP@8w{XZ -V#n+j?7F)CH}je^Exn^eA&(;`}(@{b37XIBPj4k8`V)G)~)u4dya)?e&Dwhz=!bV>HuFb1y5;ed?K}y -Ynk>^9uET-uxIKDe&`?Z$JUH-B=~lh_MGloo@~)780w -l3Hs1b-5f -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 -W{;h97NRLEFgk}7Y33r&UZ9k(7wt2JYaolbXr -`On8S>@-9c%S4q-uDeYHda_VuT9%IiK(E*y=aZ6Z6E3dhwy@Gx@v(0+5rf6v1MO-U5}wHeG`?{Fu{gm -`5=yw4rY%A~T^F4O;kAc3Pw&m}ctM=yPCE+SoY|I#$g!LpRrp3uf|Wf4-99pDBjSHOpOOfjQBK(ZH#= -s?Gy8Qwo9`jLJAHuI`(PllCE`CX^53EarhZKDUS#P>SK1JS*)x3ouGnG=OhEM2f^HKZOBXNAQ_$c-Kp -o+uF3Ll)MjNxyu#`oI#RwQIgyMaWz2^i?I#V$Zy!YHsp7*%@tS@#6$JGKV<7^?@8T)z6Gxc-K6B*RHuSLL2H8sI~_^vN`246{DTArO?8O -lr3g->ZW&hk|O65s_Fd5D(nH?Gw>D?gDT!!(G*vquC%vs~@llE_j;vuN;rSzm__2D*R_E|2sKzKI)H6 -eq2KinV`{);@*SzJvn$x)3ok>=y$-cW#Z_OPP>VrXdd&K>ux`nny6ui@Fd}*F~<*r3|XY;Y@G>`*QpA -nL?r8x+Yke@1N-+SztUN=_s+iI^#=L@X!pT7 -cKi1M&Xxm4YUb@2MCAl2h>laJ_dEpn4tA00}7%0fbr3oHJ -|WW+==>u=XP?W5l_BG=roE=@g8ADD^}d*gVXNfqi8@j5QAs+AUuO>~h3Z5=pp*Lnq$VP(R1XkXkk=E4 -49NWx>!c!bBYeqiN`bG5L2bkeDl-{R^+t=73T$bHcBGLt2*rR>K+(YbUN5h*C~Up`b$&c_1$ThO{_U$ -TvGO3dbui!Y6KF`>uTAs0zE>gW(yRv*@;KRa>Y!N9b||$5Bem(t&ysU9vF7l>&D5IUA^T<{Lwd+2czX -z$g3mSdyMjLLv+!9k3#pPNn+{Q`HLBbpX9?g87hPVaZu9wU{ -np62XVe8XoJrO53&oZi&AtYX!ws-LCoIk80O&Jkal<-JZj{)y=@~WrvVzH+9^iO_K}8jVre&)v$l?(; -=?cUQ@tdMA2l-|7set9{rNr@vRlzK=qi@{4LZI?TtAGuS4oPMgDI+|3%YE#1wMB;t-oa9EjC&hF7Pb& -WbrA$BNuN#E#b7>CDnEY&hS39xD%owA}%$al4{RNja}jwX85)bHQYf~7cfjo8vl0)EK+a5Y{UssQkwd -Xh7Y3>C`xM23MnPdE<8ksX6;8NuZvpr=?IwVe%AyYD)C6C^Kc+3zx;Miaka>S|)MkvI -fWXLRFc9<~{tc0UxMk7G474$W4iPOrp9M=>;5YLmz%*jci_}U~G9ze0}vEL#3bQ8%hD&SLbR>jCK6EYP5gvr87 -hBql4WT0SDzacwvZZJ8^p+~$m#Zxxa6T?P2QA9JqD7t1qE7CB;cG1l(-uL!PGY?}`3SAo(ZaAY!LQ3` -G73Yw>xBK8N9%7*$(A_+z#`cR2{d!0W7$0BKE* -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 -!iV1h^#6|f$?yZUOd-x(T-oVci`;)!$!^Sv2` -r=f*HiMoQCM&gWLav{%wH{u4SqLt)`F|cXLntE3Ry97fC+XGJx|B$>dj5@ns^(W10q?;e@`UYyt4}E0F}xWNh%kZ1OS5(bQctyneQo;xGW&po7WGkM^J3^V}9i(8aq3E)o>za -hMs3!^Bc$q3U`pMR&?=ZTZdxI>`K~+T;MDV{xLF0r8PymU40*EvbH+8QvB?*WELCd;uFO(pyMT=}=^AD)x0IOVD%Stn(wNu4At%(VWWFkj -L$u=YI;ZU-S8&!Tmtn97=+Ui4Jo3+NQ#LBgxMh&Tanww0-mWa>eB*XZZ1_M3t;RkhT!*%HXhtb+}5)2 -ew(zHoBrAUEp2ui`T_|fyeLd4oUx8!v^?ZoFe)lT8e(^-0?-I_s=3 -up13ZiR4l$M7xo)jRMwq#fe_TUF_N)YQMS6d;5)}=Q&$@v*MuFY&gT5Xp$x8a^9sFfDI(N|Yu_f-$Cw -Y|B9Ec{4YJ_WVu@KrG5dHhE5nzpCqxoq~+_es@aB(0jeLEE4;)vDcBu6@xRFI&ik|439@&r0@;t;249 -6DKx)!ea5l^$q8OReu*RjN6>;&sq%~W`1+05qmDB9NiqVYtW-_R#*(NwGon=PL;0IgK-ng!VniB#zfS -x7NzgQdC12qT>xG#vE*TDAM}w|;Vl)_B{v;>O?C&fPAx{~F}b|JD&1dT4Z>exx7*{T>~fQH*!G%BT2E -9V4)RlHKFJvmI{Vh`#(WXEx^JD7g`YKRaY_1bwaXLu^?4_cE)vTdSzEE0&f%$TPLn2LY(tT=M;m -KkOE+6-%GJ233oEAHK<7spBTDKXdXb(5F_wObqejF&4NNV#rPdRSrs4Tln!ljVSNP?d0FIXt_F$BJqk -^?fl^KTD5#Y_X6cA78-aR^f<-&J{)SY8Ig9+9t^xEaJ!rZwDOn#ET=m+5Y9G8-nvT1@HQ!RnCMOiw~I@;keI$ttexO@l -J+$8T%LmIS^3xu6qLQ9^6F$VnzV1d2q2A3l>=gXju7L^~Y1yALpq*oT-@U&SKyz-g&b?^B4PmrL?4)Qvg($n?FAsuVT|+2ivlV?LEL6G@L68<9WQyM8%n(fWOW -xZbEae(Zf^>CWWR-OhB -BE6%d_cyoB%;P4$h+$E}71o<*4#Qe;)a8Fqhv~plr%4S8f;<}B<(>X>e&g4nbAr1ISB3EgO3;Sv3J7aF`B)cb&5c -LmblCy2F~3#U@-|S2xxm-L+^bZ7E?7_XAf^o!DQ+Tq!uF%ZC*jQmM5{UH5ajiP$_8aq8rzl+wb6W<%r -){QyAuF!XFEN@GX>)LG(fc6{QPix*S})T?vEgFjjFFK9p7J=DXx~2yScE1UIm9s;h=k*B1_hVDx( -d{|f4_jeLDjfY_*7m#jCIe*bsk`tIb@t%cb!im`o7xr<8@mml`s1dmvs*~>o5+j6K -_$)}H08^1LE9F>0|-Mbp8+l6Q^X;&Fh#!eZmIf|RCzbV3yb7C*Gbi#(aLp?p}c^YxpG!JkQ3~9IqNul -m}s!1`fTGQx%w;+RQBE_m5*ap=UL@_g8K$iYN9K1bsRI)&FkFis_q(P5)e~v>_YpWXZ;L0g*sk@rd?j -Gak4W}r&QV5DOIksg$N6@8D)Zlm-YJYS1%8SMuTc`Xu=rHfupWn2~XbI2z6)D`7EO59)uV58}X>c1u2 -;C)r1%N6t%cLiAnBD0)#NwRu|cY;~#C}i7vIcGYP47idmSV?kdpJv!UCLq3xeV5yUp3n>g#+i>&tno- -`>qnea`6;UBHIYXQ()tY;Y%8ryQDZ5!GiX$Rt#wLNkNa6wtL!sh}+{w#7A5|oatO+GcZ9S|fsV-XrR7 -Gg&Lsc{%RbSv)1A-fCxZ{SThAK3}ji9oSX4?tgl?&ToH$dn;X8lWRDdJd_@*g`o@j$zhL -O0H$T93vk2Ae&0*hF48sfd8&XgH(%;kD(8`6#K#IiH}e(@Z3(M%`&qpr7GzjvfvD|?f{f@9OCiV0Cl$ -8j+z#hgNtDU@6rAFI+QkDNXGv&0I78KuqBR?tfBx4fpMTvz>2wA=zT3TPYb=Rg%)a|uoiM?p+9P&axL -_{7OK)h|3?emRf^q&5szR5kn63BdT+?ve7HvsO*o|qVy;=m9^*mk*1R`H>Uy^_e0)UUh -)w-@7hbLyHrlI+MNk+i+C#|0Ft_~9XFl9^;ZMpC8xepRb*${ed{mKUW0E7snNbw?^tF-_nLGv?-IK2u -8nx)VswY}-F*^Idz|*I#P{xF*m0+QRW#791spU$yY#eJiV%!kjKg9#4g-+&r3lFISz_sN`hhD(1-o!$ -Q53syG(=M{$_^_?!>4rPw?}vzMJIdqRaXDUCPT-keBBUU&+gOZXk`ki*tXdZmq -s0?)GSIklut3qvpYgK9R%fX}QmSo_m-&a*Y%Y1uM;B+}t3z$$M4kL=52?d)LOQgAADF@L`lArieY#z| -oUMOTVi-(oYPQSs`{2jh(N@@;8Q6qboS?pgzIU!Ua|?rQ~}T&mWjQ>btvXWt=P?c+k8^z+f}`^T{tu5 -DyB$(fkw`24spqZ#Ae{*Wlqg)OnaMVL+yy2hM!$6lhcW7VGf0Y_ZM+zzBKnR6^Dx>;VA4{r17Gnktm) -o`d;)4(2dEyx-`_x|Yg_gL4pS&i?}C1r_wb94g7NDbl0(4G^L_r{R;eb;)HveOcdtD=AQk#S2a0<^^; -${|nnmeLcS_kK1`vK6;shSyC~nT4-wL4lK@bd$ -Am7}xK*SudXpVHj0QQnSJ^I2M7r=At^fMvhD1Kzl30P -r|3Zan>fX6PSg6&)P1kwIT@VPGojXxbwHLnSH=VfP>jwmde{s9W-b#I`PB`T2g~-ELEsvMXs?--Bh3D -0M*kp*~l5s!vS@ZGG>=`_b^_Ww0z^`-Tzu1GgmeH@~I;g&4KhJ|1r1Fbxp&gdW*2hvHitx#BM)PNCSx?KMp^Z-9oBxe39k!10fD&Ax -ZMGoSL-pIl6^ohrYBL~NssZ%#t`hC<8$Ah_3y)N?XM8wP;XqUs%HnX&im9|;JA6YZQ!EM&x{`R*oqY{ -AUTq>p(zI6mM?#MB;S{-)^gTY6pA?q2a7gt+ -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 -In5fKSM>B*3X|(XK#ZU|KqNtyAg=$= -rHSi*>l!Pr_ampC*}~eVQCU5i?9jn}G+N|%_*72H5fIY}5q(}e0x73)hs%r1Vz+^B(~bx`ty4k{m`%% -cG;_L=#_|C-Q9b&B5hQn!*}&_Xmz(_TLgMt3auqrMZnEs5cPSn`rIN4^ER42)^)Fqxb5+Z_Nu_tJYbZ -U#viIGj5O=?;tFN-dI(mjI2PS>=)V+t^Za0B!2Po|{&n)?;7fF`E=4CjDH`|M(}aPC6vYD+?^>6Banh-)jr)C@)}JLc+x6i>N#`WQ(O+XH)n6*aA=@yz&L{q -PgLw!M}(fnRrX#iPkhRY7k=vVL)yVceAE2zO3+iUHsS-jv4vX^S=ee85-PY2nPkMjlk2Kn8%lTg3g!p -12uk!KflgH%BG=K3u87cu*eS_$nfBkIDze^r%ve?I -=Tue{Kh8#iHP;(pIC_UZqrrTRzQvC_nl1+ydrCcRGL-6p2#?io$u-8M$6A3XCjpmI0Dd<#`Fy#o2F`42K$l6n_k0(sTwb|)ct<)ehF1@SSpoF_++oe9rBogS&X=Pw6oCCr^R&@`nWa?OIlO~utuRwpu -waEH=_1Xw;KG7h!HUkfxl(lw+lRJ3ZaX8NM--H7=nJWWUI}HVHZ3zZDL-zjFgJs4k>Vd%tz-nt=%%a~ -`pTv6F~vnyHyhP#rA06w2#Cd@q~l~@hEMiub>injeUuCYi{dq!YMD4>;vVzi=~vq4YHl2 -NiZp?Oy7NF%UG|W-R%nVUs6v&e#)uQ8+6!^?$^|HiIic4S%_J0OxRQVycP?_vDV$Xx!xXCh=_g(^p%T -Os&iR-0qiR-P2t`@ZsWGDLlXo=X^2R%3McK|PMcEM@?71WuseI?35oK=TS@CUxYG7oHpt^k&^NM%ARZ -zYBPC`)q=Zd7D8gN(~e5$yGW%$W-Za%maIsIjN9RQ5R6fqk_mYSNlCt#q=;_%3QavBU9K1+NEgccb17 -DK91CVq!r`LwMVgdcZ+QL8O3IYRyocbJ{5QCy_f2%LE9b4J3VS%@ -p4Ik?BR;F-ePy6eUG>dhQ?;3&7hX>ZH{rWA*)DN|hch0?kGEk&IW?`OlrTqgxE+TN!59gFca?$Ip3qZ -R}{--shb3}SPkO+O#g-mWneh$kr!>rTh5lcOTZ=c`0w;^!5%@dbrpJ_iC^U_Yd$??dp|0lbMWh@a-zZ -%nriQ-hC{ -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 -s>kMhU8G~#D7b?<0zt)`yMdIj-!90 -8Pj5*xVbm2YFQu&=&|I~skM*OF)fBgXxcz+) -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 -t5`iF{IRI*Mc%3HAgj~id)4 -jWas0OYKk7NZVcSaCvK3U*1&M!Cl)LCiS -ghukx~8hHWOchTXc>L#%`-Xz8qgExsmoG`89t6 -EFEYZz29(96YcPTHdK30uXS>~Nu6-7NMcHj7<$4!P(|uC8e0UMH4=n*vd~2CP+gJFektxr$fNFZK_?A -ImM3nfB&|zfHDJQk}Szl0(^!ZA38VabmpHY=Cn!yX;Xnoc;be)f$%Pu_eW8FXBbn8_pD0cZqFi90V~EjT_FkfmUodJB@}H8_t+&Y$xO75{(^GCf0EYc$VZ7gr3)GFfDbSHvv74!3d`i)ASKh{Z&1}>_Pz5O(r|&h}?xq*QUhd%49cL^wY9XpS&iY -XJMI69$#GYc@j25)8_+yTRt_eC-Mf1yC(7f5~#>-sw&X=2QtyG4bFSlTm*%01jHl%uGWeegX9JLK*u0 -Zu|F4M`=s#C4aWz0QYIP}eBEOgrDGNV-Jo6CT7sr{_l#Yk?Pj62IfxVp0}#9!N4rV55@XBk+b?ktNr% -iGHIDpgXfDiU1PC-E1=9~6cL*~Un&{Z9Vf;}&G8w8Zj;F&{^nd+~nz&w(s~hc8xk<)&|Kou;qSC$^A6 -ds%qzfhw(>m17&~A@Ljwb!v438~adEU>{H;wYa)fw!!%p+FM|yA2!4^SKrKPA@a97+-nDN!hxFBv4T_ -dvv+b{th~wCyKFMwskGv~V0e?fYwByc8L^z(p!qr1aA3}a+F0(9+WexoE7)k(UA1~r;@gl_KE}J{exmjh!GStDX-m(;ygiP)rh)+Yu}u$!!^e?G(^1D3>IaL9 -ed5#ZLE{K_eCBHSqCV>C2U+-g3m8HRFTH~XYB!7&0z5{6_=i?SH)JFZrAkwb1U^hWyau0vPBDC=kPA>Ee+}F13!l91)oVz=TD$-~7quGMP-FH3yIYj8dj9 -VT%^%qauSU+6bJ5#KPbnB?X@>oep{+!aGWo-y=-r%EMKZ_l9*nD!!GBMigg10-%Q^yFKshLh8AwPE1dGv5- -QZg{E8V(HW>FGG(X4{jXh-P&*Q?#pf_T;DP_M(!QFG@xquWYS+zr>Zpxh3j{ZH#j5Z -5CfRM;}*`2pPU2KD?RhYlzoRwxO8V=4)IPQ;!4>rW(@w6gsC6{tcqv&^i7*zyG2)c0btLD+Nn%1gBzM -;u@;?E>Vx|b9(FBS1u=(wo|u{c&N7(FB%pS;J*LRuO({&D9weX5%gR5tGmss@K-l`7iYZxg~Rwn{3hi -Vl_bTxCi!PeNqM@7(mo#@;%T;-GTrg5MUt|edr1EzWuT9CvFS?a3LfX9eLM?IMT>E?^~gs^k}X-m`$L -CP$7rTpO%IkQI6Y)f*^@YlxpG_$zfDSV*gvDYz^+UDyQCG=i6`rN|!UmW*@2pmIpE}`-c) -AsTNhX8D`#sk<;a} -~<@ndbj`3FA>5gV=0Q3pLEbQKsI`W+l<4kcu-TO~LyinjsL)b@W=e4KhR8zM3@VxK&9DjVIZ<1WA4`_ -LTT|(`VEWs&+ojbMO0+T8WdXLZ;kCuR6O>Grk24A{Vjv%U6w6`$otq?rA7&?0>8VeWymh|7h@E% -<8W?_de|P*;xM@F3giYM49C=EVaVOdg}S?``2E^Q55;)nN$`VkS22gkBHO!xe3WTx3y-fnK2Y&@5dLl -4HUt06rsL8}S~=O-V~^WrB%%JMm-u_^nYRtr%b@J_KTBDL+m)21y49g<2q?RQL#lQCcAX?UYfE_52s4 -oNt43Cs^C1@Yt=GA+)O&mY(weD6HAv1g)S%^+1)on!nlid6jzFoZqzYMDiev-JdGMFuEKf+%&S2!4c@ -*+Vplg7%iYhNg95Jr?5V!paD(dIm2<$K6w>1&z6dbQ#96 -Fr*DHGzKWA7a)E+#6Pn@Jz{GgA>d?00=ZI*Q*R2nTNyEhFG%p8vJl<;r3HqalCP?9l@$xFdSuvQE!Z? -yFkbPxmYtFvy&F0*Ec>G2$gmr0Qf|MX!6U;SSfKBmytn4sYxP2LzR5=!jGGsn@xCRIIMuzWBi7G%ZzP -}VTiKjmJWC^6k{3B)DCUgxb)LCv{N$#Hop*O9gIt1G`cCFwn~MALFnhv_gz;PY{B8FnW!Q_n4dk+|Ly}@4yYYbIAbPJk;JDJ=xoeRnAfO=J-Bg6dxg^E(q -PB;G67n!4bm4{C37vkS{)F62MUQhrdtRtX=*<^u5_+*tLXTaM(0Pkzp=|Lr!O6_F>q95_+x$ar#>FzeVY_ -VVhb+Nm*u9~h9q?2JpB)f7rltL6HITPyOEU^(ym$r|?Y#%OOl$oh09be3->+}_Op^|*$(9gs*B -7^!I#^2#^PGvY$kCM)GtDT#vxIY@bH`Nb@4V(KvR<-X5hEECbnU%773J$D_@pZ;mIZLqUY})D>*$MMk -^?gF1o(hUN&9P%^-zIn9KoUL&Zc=9N>Kw+mRQm;gc;P4KQG5YRW|ec-g*0;C=iCF+<8b8=vX3Vh92-h -u4p$uh{P1^&k2cwFKX>7*vx-8sq!7^o{uln`aX7D87=@eRn`a86u+tk247juPcVMQhFz)vg>4{yg+$yub$1=!TVHX -Mr`@Au!bv&USMNiD@D3Er>;n -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 -@mTeS>) -FzjwW?Yk9u4~b5SDwnOxl-=Y%vx8d~vy46BpYmeVn^G!BTU -(9QB#h8S+$iWKuFpb>|BtPPuNG?n#P!lIxTVTYK_LN>=`1)s=Kl>U#auP5PExzS#%8W3Bll-XERljQ4 -M{-xiN{caGoI$XD;$V#asLog1HA84FmCSR$@R_rG95ixXNA#foDO47gDYt)z-I6$yU_GR7mo9@gKg^l>Xonrv;K*CshXwdI*@xZX_ -C~mh=E8}c{vz?h32oXdEuf``YCGX^WE^?PP8%tW|2)_7JVT>{Ed=`rJ|U7YQ0)6rvqLFM?=-F$6Y(e-J! -Hu$|x&g3AQWeifn(K`(+q1j7l&6J!$D2o@8pCU}M5U4l;tP7+iR{6^sYn-HxCx)Ssz7)&sVApJLd{Y$ -hFY$Di0aGaouz?<+k6Lcl$OE8FFBtaU1jbItU1_Dv7^7BEWX)eLq3s>@bY_LUqIm9BCC0fJ>Z(Bsb{T -7#B;ky>mxVPoXNc7C;a%ntFEW^YTyncq@Yaya?@?vIY*b?$`CRy|Ig_tVx#Uzm<@`RQCx{EZCMSp}pj -Yek@Ork#uCPs@53P%GU`m+*Fxn!CNa_HAioC1#VoZ~REfwO7smd@!;<*-wTntZaQ(r-SeGLd -*bi(JwujXd(r7L(~Mn?iu{>6}7MTJXnyOp^IDVXHK$;i*tdO4NNI6`_Rap~kSE_tL?>!oyc!q41K@XMeeEUs{l)T|!4zJ-p4FFM-@lL_q%`d>=GVuSUoMvv(w$D>^C* -^`)2OfJ=ykCsMLoUVe*ym#PSq-AGUh!riBjKCa(XFViGNQKde!pIq1>z*#%g`e(frX0PG@PXKi%4L1Z -={}p+uol_d~J5@_vPq(HXf~Pqw4_&z(ox*(%Y -P!|MTU8patdBv4^0{{W5JHtfl-X2%x7ETg$WT7lo^C4rPNoogiuqT_Z;+wAPUkXEYmeIFA -l*=x8{H|l6#!0Ki3tfj=cFe-JD~FJYG3)WqEF5A5nV8J5kl7sWM=b)^G^`-A1>DKRX^@9LwTRYa7Lth -@-TldIOXk{7Ec}sC0+~0%oy_)R4*l4|8qV>LEaDb2t3I@l{E-e#<}5Ndk$E4P50VKraxs~hxX2t5#O4Ei0Yl} -+s&Vq7)i7EU{%R%-p3=Arc0pWg7E&)O&H&f4GYtT(8CW|zw>?N6|K4|eaNxe#wz1ICKIYDRM{R`o8_P -&~s4i|KXT?$mS#-6fICT&;iB-6C0P?c9#ziQ_h5mdlQIL(MyvHM=av5&WGbFuEN~{u5bGso{AX_ue%w -D!w|7I$kKC?lJPXJyCHO%-GhvLwi7oe1lsu*OYA5AavJJ6w3G>&+R} ->oJ7XW_@2tNsQKyn=xOWe>FMb4vAeGS)T#aRSvb<@x!oL3j~SL(^oaQQ9%*SkW^pD7RP2!i(bh@UsS~ -YSxWjX%yX<#PvgN>%=^JYMvS?X#II>HlYjH;TC}|Wh8tV8Zqv5iO*gkU-_oIDK&Q@Ky54$Q;O*VI_Xz6QE4X)`zWqW% -`-g=O7#LxR926BDGkD0**tk1}#SgzTVZ_M9QKOTR$Beye-1rH1r`(e|aZ;K!ee#ry%zLw@X6NKi%geX -fr_Y!<>%RME&zV~w{;xmgEB?B={;u@9@^2{hFjxC4s^_nEh6xYhh!>r+bt+-m;Zuko)&0{*Lie?&k2TGF5K&FFW{Bx`ine!o_FYv``)!nbUQ!g=!@Ma2&+SX -i>ixwv%6(q+s4@y`bzdiaqQD<568dd*{xuU+@V`VCJ$_4G5(KKJ~_7hZg6)61{C`r76#TVH?U&9~m(w -tYug`OaOt-`TTw-~M;sJMjJo2M-IMu -_4S#Vn>!;nw^sUC5g1N@H3NJIY9~ep_v)UOMM`uHUq)q2wdUAuk@dJGV{D+#a%9ySKkD<8!^lUSz -4$p)v|QnSo4d3iZ`=8SCfuvB~Yq$yU!o5=JIPR+=&rkQOy=A2w>w%Iz~NJotOm$e{?o1@Jpw{GpA -;oQ*$X$8ij|(Pleju5GK!>nr2SV%b7}{IlAb03&xv~MD0+*bqtw8glT+^%b7^{xWvai&2G(`m61K!oX -!Yc;h%4{*$mzy-4yX|qf=c2mdvlXzs8FVvL|eP*SL= -tME|$;9YgwHtXn*=i#eXV}b>a?-5koOE-pzV_6e=hW2kaSu*2kIadjau01L2~W(-T&j3#YHF!aT2rT{ -l8s6R93r-K=}zU?1-ASt)`_X)n3a)j?Qiasm0xcfGp1zNtogaAldR^{belEL+@*&-Kd;BcjO-rP?CD+ -fh;`(lMplp$1wz)E@m6tVdYDRV{7a|o8#p*WAo@29`sf_C+RUgvf#LL>Uox*d)d19cz;@& -7j@@l#>GHg>I2KlzsJcIMKWoIZerd1155ASrk)cR#it5(=FD;K%Z-)zqZTBAn|?iO-ox?MVUF{f(SLs -Dv`Tn`>yZs|g_Wo6~eK%$6U7o98PG|Y#3h19^U9YFo4vwj@#RXWU%G*f@SwVkD -we_0)WQ+vx*~-;t9@=nzm9Z2r_UiN&-&x!LKpp?&#H+{uJn8EE_MCKe{} -(1-J^!V4)gR9|yQRT&p1SYq`80N1J)h4OUESY*0qH+j2;&LM9#Oy|aJ7BTdkE -2zlD_bZ2}Uj9rL&K;SN)|VHCR(A?CZyV?B#UYEW*ps+ -HmZ(z#6&lIt}`rSVjgCI?dPT8K9M_r|!+pnI^f --&&jez+48btsB5)RDT>Ja{EW%jS_-3bY7h{2yA5Wvw9x_O`8Dzcc~PWdxV~m;=}xZkIcfGR>tNDlhNn -(N*e5+kK+~?5w#gpx_AFb*pjkHSsGQ`CG;7q9)I9N!Mx!enjZD&zcA_n9DDe-vzPr0N_TzyDX#nop_% -SjqEswDY_ZXIwnx^rMQs@Q_8wgiZ$`gu+)>Lz=miR~!m6emPzugfta(K)zebnR>D6f&ELQH2O6G`z%7 -BOy#wmkZeC3^dF2rBA{T+AYI=&q>g4|}RxH5AoQ!Nz?ZiR%q1Mz~{zOZ>$9DB|tPGU{ -c~qjKX5}L019fC7QXJv%wIxZ2Noq_|>+HGkNTvb5uD#Zj-Vx8y*;5#)w3wNbthtCw6appjBzV`bw?}M -#v~{9=@?>jX0x=%~^`nRZHBosPHezj7B9$WJ66!I`nmXNjh5NVevDxI5nw4>1UC+quw8Y$u?5G?&rTZ -PFYvfid&ylvQ(b=SerisJE$t` -8gnsa$LF!54!RzGJA1qo!bd8cV~xN@Hv)Y&~`-uVliM#Ynu*C8x?gwYi*50t!n^fju%RL5O}R$2()ej%k->#n{_ItDkAf -l7P5&~JQK65)?BevYenFZEIv|e21ysS_G-ORTLR+cM#DRyPazh7ABNV(len$ros&1%iWNhvF6qf`gR| -`UQ;^LddwM!)@>nmYYMGmwXLXfEGEy{fEh#lG1H+VL@gZY2(y9t%q8cvk&UCfp;@oTI#So7AW>so5e3 -R?%-2e0Z|MtQ6Ft|wh<<>i*@m|(BQs}0-(xE;6^~ZI)(z%wu``?A=f1dvn59A43=XBZe&`hi;;M*%)3 -5M;4II$P={XRmS@AuWrMw;1J^J`-8yFrT!)1;;AuesrSuj@2``z@M#AMHI-dymrI6SO!ZHTOi#OxDsL -tL2@gxla{>SI_Z{555u0({T0H!WU{d&(qBLn!iKyFVfOkPJpF^$F%o#8a^Ae{GQg_w`%x(p!t8Q`M2^ ->d7<-6=ef}of6~1n|34ePA^-o$M{194nMGU+{Cgk&1|QYm+~D(XM(n@oX?Rgu^>295^nVubzw`OOE(6 -;0|F|e5c#(eP?&-UMI=Fl0&zgUW=Kj>zs=4k-)tvYEah?&f%q?W|846#%{@8kR1L@AYteO_h+C67AeOvkM+7T{E?$y9sgB`o^#chm~TWMy76m^c!ppD! -CHdV1S<$0Bv?#PL@=8mmmrN`JV64%FoK~3(F7KPaDotm-ULAew-ao}ZGzhg+7b8?ocogUAUH&@kD!d;b%IR -<&k(F7SV6FiU?D*v!Ayc&f_n&t5m*R%YvH>Rv?K5%xcG%dCo6LSa^!K4l-Napn{BX>nnZUwW>R<5H|8(!(T?`pAL?kCCi;Rp6VYl -1Wf+{Se|3a~1#R~EC(@%>x-grZlm6eH4KmAl(zI<6|Rkx4>(+h6Eq=D$t1BHIVw0FjggZz3>o~bVMtD -Z4q_G|<=BM(#`lm{-5_g;GUrI#}lm)>X0z`Jtrz<~q&jwVpxgs3yJI-UJz%&zkNo+8UV+WU-yz84WcJ --u3eFF^PM2h-CJ?$zF_Reu~`UX6E3znc8D?L7P$|M}I64pbjFk2LUpdF1$53Q(Oz>C?N{+xuJIy86IDA|W08@jd#&ycTEFF)8i}^}STwtNE} -42Pwp2u*;`;|Tbl7-+!ve_t`^a#4k)rb)0O9|^`cVfiRv#=ZWh-Sng2*aQN0nK&*?a-dUN`Wo9Zom7iy2{Evnm>6&&{{j35zcZvYPhw?d&dG!1~87S0$iq5&u%kc -GmHz=Obtz(mkqLeBj)S^pzB@7=q%h>wpKiHV8gzWeU0CF9GMEfdc@_ndg`wbxk2Kl$Vnaq845mz?Kfx -@8l!p_{h)pd{|kyKo%HPxH>_0Vn^ynSw -!+JkYX3D*B)9#dVbBdf>(q+fE(J7)puHUhFG}3!x3DPf+@`no-HVKyE`-Pa>zPh!G=1Qc{u_J9ey?Fkyn2n>JWXo;+D(W@d -`1Q>Ti&ygY6{=FFKR*4ZP(^Y;x9ug(n$1%@x4*v!JJ63D$gR`n%7^vP#7y7oi|ZzU6CvHEWJYSZ_~7ON!rt;s;Uu0Th22#UD%YCsX{Hvbk79@mE -v)7b$)j#s84vpEShpOz|xg|1OGeqxefG{<9Q+C&fQZ@sCpcuPFYv6u*MvpP~5Y4e@()rtvJP>bW#rTx -Jtu(-Im(K1+StE~_$vLVmYg$f}oxJa35KlHzxu_(2qZAjOZT_>@*v2F0I6@fT72H -5C6PieFCgKcx7l4Dm_%NT8~mOUlNwMiig);DgluRFM)R&DRMzkm8T0`1eu#M=1UlivN)zzGx;A#En#E -ZxPExAXVGG;vh+Wm53Fx`CUTx%oTFP142$)E#&f7gnW6QkcYoE#5Ys?z7&5L#h*y=XHxtn6n_K7f0g2 -Ir}%p*KIOUm3yOb=;#axi-%csqK`CTV3JWNOCn<&9l)?#0p{ltQKlha4+z2VoPm|)paw&d&S&H8eOIQ -516u%qAkD&M?DgGpiKb_()r1&c-{!?Pm1~&ouZ-!MGlJO^(8UvwyvE5I&|nf#N`_~C@wZOIyx>YA|f(6rCZl7T{;B}8 -PZWtAv%`4=|7e%(a|Y^w<5rhA!f}#Ec$&y@bm -L)Hw-zzKb{{-zfG+GDPRNqqhjJ?K>l5BDoxy|)j0Qle9wc9YErXR&w=v -+$)M*xj~oPN)+cw%vUOk6w@!8z#sC;U^Qds=`(9Hr4_@Zc6LT2KI&e@b8P6ibg7%3wJ8-#mCQqtMRfA -7%0~g)|F_iHnXWE)Q!pyo!x>%`C*G -g!b@nVMhWl-duVaF@J9O4>`m_df}_G>YkR -BmL+MA8AjZb@>0ig&kUp^{HnM*mZxL7fiLc`=Wm&;RQ(?q$QY6a$9(}H$9;jEBD-;oR^tr3+DY`y4a^ -y(XN9J0GxO9)FXATq#Gh)PwyeRS7{1~z6(Yd1EKGG?uog4b86iMHxI>SsdAAa~@v1-*Sv1ZL0v2NWuv -0=jo))iiT^;Omf-+c2;))hW3ds?g_ePa{p3cGjj79V`@0qX`Qzxi5x^UXKn=8M>gbkINB5LyE?*Np<#sVb?i17G5wTo;Azqf -JsQ<1q#HS81fkyPX)Y&e(o#OYU_!f#ENAbr|{Am<_0mWZK@n50%`zd}+KldL!umTRL{>LSdWxx9<_yBT -%&Ka9h_NH#P3joB}j!+Vtjjw{+^-qewm}dSC&P9FFv8TzJ2@lU@8FHAUii|)QDOZKH-))#YX{AOTnLE=p6sM9uF;DziSK+zM^}C_?s -Vp{PAS!ciuX4=FGo-{PD-r)c1Vz!w)~4BJ=BW=gys>ap}n~zWCzhy?ggM-Q3&)!^6XSXf(Cb(!nPw;K -QHcAIcgX^L3p&ckW0gc-8D0xmCP{P3~z^74W~g9Zf)wVdz?&W%F-LXP+^iR6dM5?8lEEm!E(BIlqG@9Z2V4lVtAPPe1*1k>u`oN> -lmmx8LN?KmUC8n98vO$wU!o*tv5j;}7{bcI+6J!HE+m_#HT+PEZ|Y^T?4S45%9@511%7)E&fm -@4fdJf0QBBe~EIqeEG68KJ@-OxmUxV>Ts_=ga7&S=OuMREdL=PA)O(|0O0@r`|r!Q-g-+?nM=}{xekN ->D3AU7_cH+Zef#!tT{(L6DBD2;+5pHK>IupSxNZ4SO6xHxBR-I__kJmRyo1XzrR-iN<;ZuXeD0K#znn -jRgz8GWKZF1F?c0|J1O)UU9exS5mnaXW0r-OF;4A1sSpav~QTAVb^%Vo`;5qmWd?5#*1$cx1DB~x-mo -oYjDJ>sL8Fo<0eg~us-bXa-mhv{Dq4PE=JG?37k}ssJuC8AFXYi-C-UYy42fZwG_$&1U^#Jmc{FRhLj -!GHzG4UTXP#N@nPs(0IL-waqp0AcN{jiil+g(c&*M$Ft3l|{Ei-UuMJCH0Cfd(pvD`){OkR#w&A06O3 -c#blH+(7=(?$Vfz5xSG`zvFY2hCv@mIgoJePc-x)xye1M;-7Iu%7C}kI&rP|Q=cLA_8;<3{gDLJEfZb -N!B><8Q -*u4ogta(6-b^2V}h=`2zppkA4C1(9Rg2q?1%fLBp3)#u5!NM8l>ZueZPG17L;Kg&ww0cF_2J*4LkE}qQy-6h{)+NI{Y8JEmj&tr-gO#)Kec< -Z?2$D2{H$>K)Qkc038G;Q(SRqF+RyPs!x+M2)Hf;(dVK~BdVNNl)N?QKeV3HCf(GKh`3cg -MfdzOEyYfGRHq0Z@a=(Ko7D9!PNwdu^Q}?O(7%wLc)Fw9Y2eU15X;!3k5)BU!4fBbH -IYa}Vx;n<)N9u2XfIrCs%Rk9=5omeffd}NqjT@PsYoSA@1#y+bugaZ8{bU)@@D|a)^_gfuo21w0wbUl -9&Wn(bkq3+oIVfZxJ}3tcqSrQ>R}T)I>(Pn1 -6@-&q_bcQ``iTUvd7#ZaF&Xf&+SX!r-o(UN;3Wl4rg10L2fsDBVKDy7%?I)J3 -ISb;EMW)Ixu(cT&BS&Uw>34Fj1%KjUYmOj^_gg>r=4l&X$?LtTehs6+D_S}OBb1vk|Ljc@=1w)IX^#N+H5wtc=2Mc -*I$15CAT}!h3caPc%yGXTLoPR`Y6T&cu=1YK4q8HztMPO{d8A-)^&`PT76zdb>WZjfBWsXO{D9L((&) -sty|yp^z`hsw6scUN4XrHc;X3p=bd-T#Kc5dT3RX}e)wVe;DZk`J{aEvU(kTQ0R4nc2gWd<0qrT|5Pc -!?*zJs#A3hEI&&zi=Os~-~rmH#z>NDD;M`y+Z{XZOkg@=ci#K*_agZz-JFQ;*TpUs;$Z=^ab$xqIlIa -5-dka)l2jyqUKc;}sWSRQ~oa77vEa;29C=!5KGoCCcMWr6yDI?eT&%E4HlpRH4$4{UuLdf=*S5uVV{& -<9tpT*>nw$Xlz+vaqla_jt;V9XrZfZ@pELZq2v=57Z4KfCe4v(gJ`?>1m^FeSghF`F62|`e#+gK>z&w -oG7{VpOfY1?`@IifBEGImG?EzKde}>LKYPjon5|sxtu?L{x5gkb(hS_%3{9LxRPZFG@xF9PT&SQ>e7N -a2=v2{d-Tz$E0~YKI1+Bqkx+J^rJje#|Me^I=+DmoATrh6wYj;uU*CWK{g;7fa&j^Q>aImvX-TIC`+c@^X_*wkP`8U)p$Un)_x_R^FeS~@%7Z)dI&z{ZY02 -+Y*ph1J=m@#A69c2I-bN~(dxEEtF=+}^Y;0Y5l1lo+U1)U1vsV(_OKtMp>*Is*VDaH_g1%JpU0J4PnX -1D<_%sJcbcCNosQBl%jvB>V-yK_0fA93o^QJ-w-bwf8HM`#ZqS7?t>cKZAV;e_S3YhAx!{E6>S0CfO$0_ma+CfqM -mIz_EowX#sTKe2oFZbhhTK0eoqKk_x|KRRCl7%u`JDqBgq1->fg`atC^N#0ln&_{vCdK`T|5$z~=iuV -7NS6-1XzW5^dfiJ!E62}9tiBAf(Z4;=EoK3jzB)0`ZUH7=w^&8gTp#Px#r~0@A@2GDufeXqTb(Z9mb& -07{r*hj$b)EY+v^9`#T@FzXP%lv?kQ0;v+GL|X1Uu428KFGTpA)^<5r=lu>l$%);ct{xqYN8m8@Pck$ -Pntnf&~jCwOQOx8z11N1LB~aL0g0e@`EuE0Jwv$#~*(jbBt@<-QE8z?k@aw{h#<<1RC^qu|7J$Z?r+w -ck?&_GKIdjo;iKSA7!KK5LnlN+(VYYbJU>~_?Z9C^Su`SM)^k@QMZq+OAAcQv5y}=UOw{3BP`!WzUzQ -+kR`}E-q8mDcYS??^x8H51>6~bL*AJ-8eA>gqJ$qeJpjgGJ$>%{Ri(@mjghyV -D8zohudB70QD8}4*XFMNFF!*jkq)Z#`#ZbCtUMCpaFd+^cS>$kOTBb=sUqm)~)Nk>qqtkAEv`I~XagX2hU_%{Rcz;_RS%TYb -DkUEzgGiFo^A9*=q#*E`aL{zCJ+5TqJLo?kpQ~XV)Tb=X``4(zs+24VGfo4k8bkj@^&GgYslV-Np%m~ -da&`hbCZZK=_+cAs3@ZkF%9}T?3Rdhwn_Yv$TIA3SHhWtKbqQne1 -?tB&Y*K&FAQ>uw7+_+S!nfbN9 -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 -X%bP4y6}I -0-etcRL+2ax_YO(W}lkPm}pNZdya&cI^E80ZRd&b-1{lPovHG1EdT0Saw$^*dkBv9>@*JOfPpiU_c32_zHHTGW -khp>=pOeg(gQkKm&~?h+8Y2kK@HnNJ8=MDoZE@+N5{lju^qn_i;_fyhsIA%B6t%iF|fVw=}1Kan?KJb -U#iodFq*vmN%ljcoV;ufS3#)1kJnO_t_AO=A5tKnYr+4cfJ)jf)`C|@H;_rn1&$5MfligxV>~=e3M{Q& -g(+k&)1Gg)4DxEs#@6Ls>o*j0g{?o5nC=YEwb?7i^LnCMso{H1(GMo*Z_uzf_Z}=?kf!b^Y?R(i_)&& -)Ii`~H=;cI!a_oTPSJL+Bb2E0jfF7(x~EK--$Rh6T+=u&+|$N1V$_kZd?3R()o2~sh>Fm*F(;}*`@zePp!ZNaI*EEv9LAuqlbj(vWCA$(Kj|U%1HJ@Yxl3LF1u<%-TBOp| -8t~ffs!IJ)wW^bDzZ6`n!T!252H<#ZE$l{V1RbQTM;={z2Kbb- -4}1jQ!tx;Q5WM3k54J?FjQHF;g$1UX-NG8eA*ylj!3a*9IWY-?1ds)i0csn6(3`VRkoKigmHf9WTf`78O5%z^%@rg^N`eI4B4D|e=#p -7ILhM)^y*Rql|dWngTh@6aWAK2mt$eR#RFt -TGU4=0RRAs0stQX003}la4%nWWo~3|axY|Qb98KJVlQ_#G%aCrZ7yYaW$e8Pc$3$aKYZU;k}b&#USu2 -0BAJ9#79zb!IS-wlbtm%+g@8ScWblZaN4%WeV*WLOWx-?F?y5Cz__kK>zcH&?P -{Ub~@7mL(ifaA!>pTyyedsf5SA5p9YQ^V1_t1LJ%1?RLZup$1{&SwY@2l~A_Mrzpb -z4S;^JcS*RiC--_k7H@b6s;RQ|vPOdBm-_{XM$4lQ#ify!2sQAJ)Z@w1fnwb+I%B*L+>n-hfth_`X0(c(G -d(&+Fr-zis`eHm=9@3js|5#I{>UrtzTI+txm?Vm&amdYdX!Y{&IW+K6-p{@tdMiESjjfr`^{ov0_L>$ -&Y=l~A_vVKd#$qtY3-tzEZv749WYfE!Vb>+NHu`_zLEA)&+(a3$<)%cyjf<749g-+$=C=d*MX%F>O9T -VxGX2vOT(S^nzKqd)E(vf#b>Jsa2A(^b0J_?`ct*7?O$(I&)QFSDHJ7IED2t-y&@bN0S@)*?E4)3i>! -3$ZNzV)b3D`RC5dpVT_9tkOFpYb>2_ud{XzJ!kss%5W`9EHC-5G9WHkY@(LeRaQ}3@?FzBfI+;(Ir{D!+Hjl!f0b5_)Jjk5zncWxDpg2VDC1K -0U?qy;btMMng?FB3H|}@y-ZRhhFzz4b{Y2iU<39br+IIc{ww9vv?SsUr1^4Khwa=j3 -qAYQI6@DI19m@A)JcII!?0AQtN8p|CsCup~U$F9;d%<}15U*!V{1VSa=XVVfzZUCA{PqC9MM6V;#!_B -y=JouJ!J_l6gRAgd=MhVJeE?VD>lxr{0&orZj}ynlHE?;Hcqjg2d5L4xMn@8%sN^AGr) -OK0Byw2vm5Vr^POj2k%4#kc^<~Qhxu;eydn?YdE`6LLZts^aTYzS9UppB1K;c5e+&A775%}6ev#5yB# -cO}MMN%w*DqPLuFDp^>n)3=>kNL@XmrPEOKw-M#f9g*uCtc>t{U{U`_b1vioW)7^tDf-ul*(Znm}J;8 -P~6yc_tX(@xY0T=JVU@K7cy!LEU!)hJ}Eo68*QL6R?HeiGOsfv2kU(D0v;_8&)xYkJ(qAnPqHW-cx?z -r{JgE>y4#aN5ki)i(95m*F?#;4H5eG8;@;wdn~W)UVm!%+cVKqR|4rJHxVocjgh-Dq5K*6;^=_N)&u9s38e6cw& -Azc>gBw<6e^wW3Yb(?@0Hv7R#~3Ht26hQQwwh-xih@=3R(3-gIqYYhqgtMWQ8;Cl^)#rXst^-{6;Ci+ -1eWR_$0~+LeRR5~xSl9wE93d*#)rY)=RBCc-aaN!=pbJUyb)h)A@S^*o5Qzd_rfj87!e4D?yBIZe@rv -@uls-emjIA>(#G%y!LI?Mk4{?uSnezmL&ng`|teq|5D!F6pBB+mIWCll6Yvs&GC`BeeG!m$9HS8Xcxl -Kj55@nUS96bl6iV9 -YiHxMfFbx2Q{646@2L|Ib_>UwSL>G$VQg=gVUsyH!*o9OPNu&?IoliA&`l{QHx%KP;7^$_$C;J}z$Z1 -Pf7c3?h(^FOq;oF--(H-og$^CPg6XmRX@4)%F+jg?7g{D1`_4D=HXQvp)%TZ6co;9!su=4#m;N6q1-K -$O6eXow61;6Ex)!$7KwVL?!-v(i#IOoIDK9(Y=AvnnC%UrIi*HH#OOg*pLwmYWb -Rfeh5*e&-_*wIec&^Ztu2Rp5-xBgAAY5fE_Th?A{xX}yjwb!+_Luss+SAV!oG5A+P7_&v|H=ic?Dk`I!X3AdyazkHHy`Y9YnB0Ril|+j(evwK{0z-8a5} -ZUP~05&1k1L3!5H-7$8@Iw|Elz!U)zzc4)_?6?r!6GFMh-6>`3PrBOUnPi{GgCf9`r9>%90i@jcU7k< -Rx2K0M>W5$pSZYQ5vuXO=T={&!o?e-%IP(C$CAoc}C6W#7@IJ$XEl_aTRCmxJevpwklHudt~1D^#8ft -6}@$8e+ft?CZFPPELfq7qS8Gq%8MblHWtC^pSmUygoKwn-lwU@aNmQAN$qLAFqG#FBX64%(srbVZQfj -VxK6=8Me1B*8I5ha+-(?J$gX;ftRHXXz99Wv36aC?FY#k`7LZW8~E)t*nnr70w?r9;6yKM!Ek|Svx`8 -ETR<8<8z>xl^kB#3G$V4c=1@nx{2wZxEmuTdE3kC_)=i&H)%BKXZ(fAGc^P$Hv1p-R4cA#9>pY9P&!F -C3*qyJT&TouS=apO|qI|ml)A4TFb>JP^HTzw)Yh?L$O>7gI9y%(-K5v;3VY`G@{%R@k-7%+9+SYbm?0 -*ivCDUpZX4(qa%}CdW?;UgWNS_e<<@;qugf=t3p|4jvpqCxUH~pX`WP7B!+KP8KX<{Pna>@dxONNtrA -xo$+nq+wUINYV+-IfIJwvKrt;bmHFkC`?R-g(3DZW{~Ue&n03{pa8vdh~_PGZVDV*9vs-j77rye_@M6 -EuQ1B`%7Qj;t8Dv%%}1GKG3t5-*G(xSnP|8hzIqt4x|qqls3#yI@EZ_=yIQx$Bb4!Z_TkTE7wHB+&%4uy_Q_xmyqr(-f#8)5=zdaLeO!g`2P -)>|U$bNL8My;-M@^s3Iht~tYX=5>7?b;`CKw1&vnO}e;1x%oKW`)^Tjtj;mQOiNgT5|%s@mfV&rkOzH -#e|ItKV%=WNp0=*idyMT}JVtwSQTJukw`Zj-1iW52y`#NwCF;6_cg-hkp-MB|Uhq$y5T#deKMn1z2QF -DY+gXJ6vA#KDl>hcb(Z;fe+k)~Jvn_glORh~EPeoa*gLRQlwxS-`k|pa>#PKFQe}H31wDYgv8|%9jG( -8P@JM`$`1X(&hKOM9Ms-qJ`gtTz6uiHdhKKN=k=qme3h7qA${mJIpAxj`otqUbrWBtCzDoRR_k2IFN* -=q~+ZV@5!KEf6N?rSWfl=l~rcE$9R&>7Hc0q|nzqWhPquP@VxoB=F}btcva{a?yF%*%Jh -$XjS@W!iAEf2ZsB%Y-=86r+{8j@>9MmHX2? -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 -_4z7NhVk)|ZIE5pcdyFb{_=&Br+S9QBs#`yF*! -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 -9W2pDu!gDUy4NdkegH3jD@6oZg#^#@HMc_c -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 -H506T35!X{}R~z(uV2eJ~U3* -wnfsPIJT3%Lr{(X4WFN5>PdNij?zaq8GFfnFd5$>-)SkKhjs9oS>I9MgXL%k(?eTv^;D#Xz5>3M@3ai -{t~WjOWxh*^=aJ_It+bT|orYZd<m -Te#C<|HP4e`+EDgIRcfC)b04JS{mJcF#xnPYthKb|-M1SJr_DZPFDeQNvmXmHPHIO&J=JDCvGO>USe| -9jui191mKsVQ`ao-Dp0T~7@xj4VN8rTKqExw;?M%7<@uVEN9@Aqqcz9le0T>!5tmQdF<)WgYS?KuwS#TgC2bFMb{^S -U9%dX6f3WtK{V6T+zm}$A}--o&xj(R~MMzJoh0DPN#fVI{N5lC(nb7(0AlG=s`SF$L#UQv#}yPr&I2E -3R#=tqO3hUN?uDEG}uB?4w-aNbW?reEc!NipX2_Vn+PHu*KhXyy@36?&3&vr*2%J=1aD=IYljE -pm|5;HHjyUEMWW*g553mQX?;8Al-m}e#?&3|tbAzNrHy~pt(o^ju8>b>S%W% -o@M$}bIo9?#^)&qW)5Kh=@tJY<&Bm{88PamyJUe|-gVsSo^;0l9E%y6FBD@Rx2eBAqhcE&at#n-OV&j -QGf{lnZ!v7?FF;`w!uIO3N77!?UBb{A0O83tPd57aT_5gtxhMr$fvAj?)@AaX;*)1A6YjO`xl8o&`xC -wPd=QyQfb2jZ?I7Pu~qW^!4FuYRZqDnULmXq;aK+?yqL%wh+d*&KG;W -)dEMMA`He4%L7w@>F=OWKgYMmmb~zyf7eoFDsdLfhTVLF{1Z_K}i3rp2JJT&fnsfBSZ#%j(4d0Ixi4b -(8{65o6%XBQufijqOCel@zY0_*eO%Q3SG`lRT^VTCfV`J-}-Y!&lGXIJVQKNi|48oQ5%8Jbp`kz1a5bMpWCL3+wQ*|*W(24YZ)cql(J)ji~@OcUD>$HzuJ$)@&d&QE`bs4`wOPZV? -J7dn}1mwP|d0qE|_AK*=va=$yVzF7=FA2DnabMiy$sv6+sT;B -Y*eSGUXunjMGH!~LadEoTnfR9L+wF4yQYn9_PmjoR81T*`I_-7^TRiUOg?PQGdgqzLBkPrWM7}3(Ke2 -9}!y21|mt{9Qd4}Y9?`1s_+b<*G%xBrgUq$6vwdZN8z7kdIHpeFSz(Rh?1#>QKC#vCf$w4tD%Dx`X>HjCbzgNx-{$+TA?6G01VfD#+c~*^FF&Rflgc1EyeQmORUZe -DYHV>^VI9r&)(%lSatDg!Zufx<6D#4j@m{AM}WPezP+2Lq5MLpI@rG`oYLQ@%d`1dcOX_$a8$Yo*KDP -`)I?7ib)|&SVMD*)P54>!#>G -4jp<3}#wPCbTW*Q#zN`AEeSXVE?(^Hfz?&x)_($*aTXw4@KF`{xkG;=ttp2YQsdHvn*RCRY4)9C%sYb -X;*yr&KygKB$54_YPvgUgMx^r?Dd~Z2aT<0XyOQJ@=&Gek|Na8EO6j_#+-y3F -4Z$)3E)f39|ModEY*acg;NbX-sA&NYXHx;nKbo9el*n>qv8_3HGz$=pwcd@)}u@IjY^TqeB#&izZcHz -wl^xgUURt4r<~xXx_rd%54THgP`VTN-r{87s!69=1lkP~%gOT~Y_kma#qgKAzuEW$q>3XAvcTsmJ@zB -SOpvujPP{3YYX=_KcaZ6@b8#+>gOr~h$Gi8jPr=zkl`{#S(d -zM{+df$+a;qU|9~xV}c2I7a__U?95V>&VZ!)wvd<(VHe@OaRNwondUG&C4@Fxp&0S@1i>lo;`!U)`Wi -cJb2Fk4`Nwov|YyOGz(?VFdy@r+BA7B;b?Zj_c$aP4rk(ePBg6KcpdogVe0lX?bS`XW6ha#eA_&!VQ& -VmU6WLr&6AdWhJM|l$qi@u{oG_T@8o5ScWBx-yO!7V6-`a4Ke1EI?{?=s+<@?zUtL%I~+k8KJ+56@Dxea?$`F^hXe(tjQ@ -_lK;*%ZDnHQ$#mBd<19mNoEPx~A1->V0L|vYGPz9Sy5&e1C`e{*GmgpJ>{AXTx4A@pGq%pF5YirG2zn -;_$GAXZiBpzGgG~E#CWe`x4HLJFclTd+Cc0sWdFZj`xfiv0wiN)2f(I$_mC`>RYOL9>SutibgU$=rzF -frc7YWg+E2b2-bf|TSgs1*->xG4Y{s*vsfnO%x1`_--u=G3tXo`Ir%DhXo|^i+~WhbcB9iFLR|ahH}? -fmXL|*znTP4C9O5|n>zF+6is!MWt)YLr3cOC9cjHP~hIFACd}ltU=N>1o=4_R6@*~4?@>R&m{K@~zpa -09BG5%z}%wuJ$zEisy@R4<7PquGDj3q4*RwNZYSWJ -@@r-^xRfcM+8UdIj#wv==w|G3dD~mV$BhTyj3@=kfHM -^r?Q97{4@nd?hh1{))xoV+_&r@MU42bm#tiMI;wjf6}?6ucEp)9;DyIeI_lRv?-q_Vv;`UCvwXj3;5ozgrQp -GPlwG!4Q+vGG&+E%LN8rTsQOGePv{mi_zZK>9vqi{L?|*)*@xirDdDhN;w#gn+@$mL-a<5Nct|r32j> ->(ieE!6A&1SlQJOeR2)_#qDk&s@P<#MDh*&nG+OlOvxc&=u$UGuhk#GVYk=W<2Zq_wZfDQ0tzi&N?>_ -VDi*qix2y%tE=(SqJA!f9K#mVaku`jGukNXe}7}Kr8(DmlkNLp-Fkh_Q{UG3Hzgu3>M!sS&m_!)5SdE -<8T3ERs!2cd_zC_SwcNMX5A?=COAKKc8$+vu9@Nel>#;Y#&OPWGyMw*WjqokuLQQs^W5A^>ocFN*(uj1xzxHz;#!L&}_=L_eYBSwaW;(w)7DZh~K -F&QTI6m~!e4d4ebnqumtY>U7a8oAFvsCAM)*TX|Tc)ToI(Or(RVtc**k_l81`ax;D|n^ZVTK_Z6ex-{^eFt30c-$r$dV?62 -&n?1$tx@_q45X}j(KzRs92hCK6*{)QJkXxFCAA^my3+RbyFoX)T5dJE@`+V%W_%iw*^?W}}t)rq|2^` -fB%_E86LU0y2WK1KDqD7$R2cX3}(x0Wx@bdkQPSPqzD@YPUSwuttP9c -gT1-#?iB%G5(D6vUbMLm~CUb?2bS+WzBNUzM6Ap=N9&qEiYDcHXYKB7G+8%rS02x_BCFd2lpSBeGiZ~C9bgYMI8Cy>H`5Y^kaSR85qlH(Z@+|7%dH~Op;`aGfnp@7l$urR{@jP| -9Ysrnyqx{d(JAIhm=?V0P?etFcPEquZ(VBGMoJM*ZjimQ!*vTEv5%g{(-R>Dd?|40=chIDFRh-_9WjC -OAWfHyXY>M7qo1%BRq<8uVdh-lCo}8(VbNWT$J$UJ^s#eR7j-8DDjjdz-N;o8DMdETt0> -nd!!3xK)KJg -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 -5u%L6v4xCG3&icmOSmwY)tyBK_j -aR|7P)PLK@vg-FSTFwDR-&SrHuF?O$M(k0KPi3{VR?lDzeq~fHVN9UIP;o}N-{Nd8KXaToj`QnzQu#g -bBH=SubI~sYUr*m*%RZftS%eGmSYGMl{;l3D8Z-0R$8slcvr(|NV#Q96r!_ag -RA#SNV#eDU=WY&Y)1Y*%zO2W0MVe&$zJZ!rRYlHVJD;mgtb0~+>e{3B3T=#gsnUGk0Dj}PDRH -BCZ4R6DW57ko|dCwS8r=(iu_G=mXrN+BmPwhyjPlf(}q597||H<+ -e1<)yt=u}Vr(SIR)ZuDRHeQv04IlMoG!rw-r{+#}T_%GGDJfzMmPrfSlYxpwJ{Px%w*Zq2M_MtTT^ul -Mfo%@7GWO2G}O?D2vM$bq_0jAWzbs{FHDU-R6!x!pC2K$+J426kK@QP!{X@B1&e5FWM@&YGNYhN+VYw -(=Rc_O_Me$KU0E#5QyNXI|v4DS6Q?TyE`0AFOVUP1 -S?&8YnitylEXDhb_Wc`*_usYeg^IU9``%yZT}|J)Pj7(#8SJdM9CcikqbTiWpWpW^)L#o{-FjLBxF5r -s$cpDOC|?gq|FSjFG?8uIL~CLh$L=t@?wS}5pCnj*2<0a@pO64_ddtQ+Zphe>zjh@ETQ}fh0ACS!-4^ -0qxaUSb*q^*kdCGmhY^hf0;$FNY@Tu_pFnok4?T-oKE^ijm3U<8MXZKJV_BFMfo{j`zOLvjSq_nucDe -T31uBCT)TN@&Ra0U+ZzN$L803N*Ob77wuNMCCI>3X$apyMj_Jje0fs9y@nsOVF@+9xMH-$G{=txu0%5 --~pUyjSg$L+#Vm%f6-rzGsKz_3e{)j4luueXXhfbX2kB#z3oRN`pZ4jT5T-v$=b~t#-$htc--)(4{>!!3Sw5@)kzyuf-Z!B -DS*WC7{0U{kLj!io0H?Z6c?Ra+yXPz%rTXJC -osXX(VkyYSzBX7eAHGu+ni=*9bHqHnf9F$du3{AaiJ7%Vl3Yl!}2$xUN-zP;>zL<9&NxKU6;&Ctk*E} -d|PU1?lmlaxURmZmeLnx-SK&96rCBUA5T(#oS_fUGuxZW=Xvq1&CvB`Mv1Z>2bPeLIhVtZl-kd+%7}; -SSBd=_vC%Viq71{E%EtLotV2fGQeO|ws*jS)$e&5l`swt%V%q*@_$W-@$KtE#bJ54b*PuvBG665EXd+ -vPJAC;Y%#1#ye{owc?(nx=@#Q|K8=iff=m}a)l+`%abUvt#Vuwzq)|Wg&ZE?Vda1wk7WBL4e^1Lk%^S -(|>=5wf(>gyV0@sK4Ph_OTTsg#7RR>hg4c_f3xSDdlsq3v&S+0dfKtTnqz-|{sro0Hw>q4Uy9kvx80O -;L4fX?4Q$%?26|lmps7Vqi|p0fUs%t=H+hf#qc1>S)F%;{CSmbXY4QS^lD}6V9~IJ43n{mkC&{tLl4- -Vh=RKC(_t%Xwg&K3`D1Ol&4OL8A5*+Ii)kmyDmoL4a|r7VFQJ{w;2{(GI6Y-vGWxL{H~++fKQAivjYo -#JWT6w_`~OPk#>iX#ShUMGg5u!l5%B{6ftz3%Yzb~*L1GAH~o7v&aU-QEFSCaBCWUD$#j-CEAEfKhxi -rDew?1Kq35>UtY#>=wHaeG_+D#sn~Yd!rtTui)Cj+QCYc+z%IlgZrTW(q?(sjJu86fq<$R<|wjb9e*@ -aC7w@v3bP~q^6!`oMD=V;kZ<=0f~Z?zhx(b}T^*j~#T!rlNsU*aG3^SVgRLx9yEan8>#_MR2|g8O)cv -03idMf0=wyubUJa%q0k`f;Q01JrlBWNEBZWB3%QaVslA;#SI$uxB^A6#9It+t-BmhiE_f1Lo{*p;tZyX#0Q3s~=n6{)UmJC$E`4a@72l^(;sS&g=6wsRKIdcABWnx{14zCPooXs4ueG=q-Moc1- -PMYEi8;unv@pII+>NLy4uyuJw76)WyolJy^D!?`DQvBghB5&v~cHPx^&jbbTnt1?>*SIT=UjGg(@S4E -m1SygPJ`Cg)zVocBOHl$VMe2(y5<2tsQmm;Wf#nyNL>y5)>& -&<96ZiSJCz2v)o^_8MD*)?b6Y-L8J}f&9`vFEk4&S!&{n?G8e6!0;Wv^MSY|{Akr*DiumOKtXh;P$qdmZ -(D8uoR>zbwc02I -WMH=*N*VQNp=xG(Vy?nSB3s?EPEbJM$1A>;&|p9q<+=WxvG!ovW;=QYJR=smoW#_c=yEj^6MhlPTOQU -T~fX!5$uqI%78pGqt;gX9qQY^5kDD_BAhQ6;u^27tsaJ-|DcM;>Bk_A=|+8?XNd38dY(>sJ@gLrl`_5 -a&?&zy^wBZa>R)=rdEvq$Dd;Te;C|wg>z-R7XC0{rbUd$%0 -%q~+&(RB?4=Ht+wWcK7J9^Z~BV;+;D}*M|6h9@oQ7Dgxcxsl{GNOY -m}7V7f8k-4F^(NKW)OF)ZL}VvFSSFpF3;M;H>AFinD-hE -Kh`1QKD`uBSA|;FRM=swbpfXib!m4Rr#=*W5PP2A5$q0(T@*#_fzPh+572S^YJMNU)|6CF5oda5Kz*o ---(l(5!tvtGjE+kkYzGGVS@tEv7Ow<)iloT;o{H52B^lmTJ-HSG;?Iqs40e*DJR2 -H>qVf)piUF9XoEzn_fQC{p>2dI6sh<9r`gm7~9nC&VYqg?_1&f+|;XV`#wj|Sw7N!lGgrW9 -xs;0@gQs=%+=-y7H{6N$r{agtWTT|dOoJTkj@7#jJ+C<&5pBXIThMG1HM-1n`^bW -R(kf%_p*-WWo309?q7ht#Q7AZ8-Ag9=^WT -GjcE8$F>{XGfuup!F}VKV6Z;`5h^h;%*st%;D~%2Z2q%(^*h@XnZ41OIo9UyoPPo;{Cu*a~7?`2Qw5m?!d37GnJmkv^;^i5p -SSP;<88$FfDLbKRw=I9m{?R_v!zQyCSt-TcmAiLxiZ)N_9XV2A?GSPC6rEFv6w|zZ1>~8+v@7K;OBI3 -V5ZY+NoHSjajOlO3}2K7VtWH{!XT3aO(S&{P`2nK7-L_kolR!-^SEnnq)wx*Vo|gHPAK%v>o}Kt}m_N -dYUyQ6&b_}6BKt9kGsbfBHj|6E1ZhN?_j+@R`0Xw)lVCfh8`5ULvHJb-+fJj$A{xrTFf<^W3u5VyiLI -!a(1~<@ji3G*HpKS|wm$!G2EiyB;xXi+W#<`*}J?dYRN}#W-Rw#HWY$oeI -iZ8OOXhYr+nO&N{W9P(MOr@!<{^egnXZO(edOL+vaI@yGar=W~A&$@w9$Ie`C^;Kvjk70-e8#fuYyv8m6BI8UR-; -%gyeQNqVUsU2%9Qq=s=b?+RbpW()0i+?QgF&5yf;HjhHBdUIQKNx3BnrKY+oDySl-8f@%^1Ns(c=Xf% -#F!`nW1_V)w0*aS8JqXRjLk*jt-*O7bp1pbuDI -gcefFL??d`I;JM+riO%~!6nb*Ky$Lum(ncG_@B@UfjyRLZ>l^H|-FVS99^&@3?EFF|yn$-AXDq|9lR~ -|dGmE;q|V(I7cXf3XLNzMX2URGmZq*41&KR!Q5lg?x1IgJqF&t;5=|B5u;;lf1q$@(TsPrd$;iMp9ID#`kdtQs%JFZr3d#viO1#e=(1f -tl6C1!U8bGE!=2+jvZXPF>vK|Eh~e=p?VXUDNS5vpXRAph`=szYi8$EcM6n!6XXzF(gp*ub=zTqudN5G_f&pPSi#S-gpQiO&b{u^lS-%z+@IYR~olHR4ZP5Er*;GevmTE_5;QnfsZeLw=J8bNIb^3P -1t|$KBMOnTja=tg((fqsj==>8c?1S{NB8DB0ZL~y{7j?n%Vg}`dJX&J}-oBC#zs9in)O*Kp&v1t6e@) -^##FJOJZKO9+=tpX_G3YZpn)zO^SlIg=TPr&?8$86EwnlG^b<}m%&QU7P@oD=9=ettUa6Ie~3`De{s5 -A9ZU(h=~XXW96@m}7u-0)IUleHQD|;yQm-^mi8R;hyb<`4!x@GSQWVI|gp+An4am^urqGu@^4-at>xF -?FQn_(}^}d*~I4>?vZtcBp1deH!hZvyRqg>slvC6-}MEQAFq{P7_Ht_i?TgS=7>Brrh0Wf(i(9-{NKJ -N?^N)gz*xS57fO0=F?Zr9n=RoSGkUXuH43+YFy4X2Ci+TPky2akf;~?jv63n=0l1`pEx`Pdse+U-AiV` -y)A@^zk#Zm-^^D{5o{Dm_MO$&HOfI7ieF6S+y~H9sYm;ws?h$xsK(X#I0%0EtH-{^2=7*?=px^k$>?u -^$J!k>%W7@v9ifIjPt%z&EW0KN*p!B*`4Q;@ -I-$!ZkN#V2drxPQwSU86GSeVmVo-~Qun(YX*g`06!vq^Z74u-<&%EnYZ1Q6IU8&k3RtB6WJ7v -Ya{j$*Y2c9o%8{&i?+m<-)UuQcU$Mw@(Us0Ihw7nl!13s<#GVNeehBHe0zAv3)qw?y2el~2B%-yX?bjGe+mPfQ+Sdql?FpldUif~rY -Tro+8ZX@NbsH{Z1%7wtPhL2{by%}HMCLwD~cpd9|@*(JD%A*u_Cao{{2Xz{Om4G#`7kqxM@E>PJmaXU -ZjU>BM`Ff7C5>d0A=ADW7pXYl@{?K-oKkz-~gip%}%4d&M^1ThbghKDf+asJl9dk#}Z`eA@X(Hd-siFDw!TA)C?=O_^--vF&p8 -1UGGg4YC<@uD#{e<2*FilxGKypvzoa-BlD<96wTnxYK$ulz-vwY0U0j5`c4&L}T&n73e0J1?L*j33XNcNMK?@9<5l#7FNy4Z2SSqUh@|Nr(p8TqQxRMOGTv2^JNC)6M0f+R!X{f -pu}~GR_Onf2$aJy4@{1)Y)bJ*)5m>Q8gN|)*Aefov(o3ytd -{m4cOCKYx+LMZ+DdH!wlObDR-l+ -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 -~b~EO+wo3lyPbx -27lW7NSjG%GyQ43VcRoWPq;2lYRo*XY{dQpTZZ#%9nCm{0ef{nm9dQbTM3(y@c5Jg{flHB{Z!5;z;A; -d>ep!4;fCBImhVy3(71h^g2=2-G$zt;`6(O-e>XoK%uws#AsZC2UQzwocZTRutRjl?=Ui@)(D&@`h -*?`=Om_Q&UE@Gx$%&JkKLp!hR7bR5o-Xu<|GgLYBka&6`tH^AH#)0J_2IQ&)vf}@%WmwZadj>uIyjzxp3ChYsB$6nxNWbtaLkisb3_a -v%%QVXF3!N>zS!*AMdb)T0kcbMXF7jov9G)=aEXcn@RE+fpFo$Y`WH{KtLJySsvjsh%VR4p+S5*TYc| -W}bJ(F|8qXs2j(34B^xn*3Uj&m%#LsHpa=Avh?;A1dC1RDalKE5~`Ze4 -Jm3b?hR4dYpaHrKwy+wpXf`Nb# -Ht%(|8-TaR=kxe@6r?R!W@EqTg6h`8IXJ}DQ@WL%b(!B2_1T-xG8VW?}uo7>D{G>_3&8mvlrB7uizP# -^IP@zuj#X5jlfz%xej1*J*{||S9j=_c(xvRS@NM4eE*5~{&T!b?>vXkjf!_BW1G>|WTM?;+ajFHi4VP -tI{+E<6F$}Ta&UW|8G=2d;{6eQI@UZe+yx%EX3=oHnuq3S&T4vwI6j-#a4hMCKf5BZY*!8mUJ|;;Z;` -kzi`%wFoknY)8UaxMj-%%ER3YY+))MD0HiHB?oap(4MlPVBYp6?O6C_?d -XdpKImT)3&&X#3&&X#3&&X#3&&X#3;%~}V&PX<6AQ;%6Tc^!G*s?s|4irxaJS}Au^n)?2K~||k_Sp9w -j2InrDw&xgalldIkM9vFgaa#w)AXxOpt%?AB|^%d!}AmW8l{o27Q}(Nb@a-=RB8j--2Em`{h0#$4SAt -f#fbX?!X-?jtum4(3Nw4A)mnf(DWl({o~CS%Q4VTu)&Ph)xy|5!hyaixSfPR~)F8CH!C9;G -{$#5`j#=&M_ahAstkB()#kIE@SOx$;!HtfrQo_2?EvB5boU24Y}>&XV$dH8l^&smbMQJ#j`(iz3dfH9 -m?yL?z?4JAdYEUul~LDyY6ZjZD9_DK3G0t?-Czr^DnK!+vu=c)L$iLNNzwa9$mc}GgmA4zAFuF!tO%b -|9m9Inp@sY~1IpkwmHSsD!_LmRHNcA?F%sT`y!pE^vcsaAEy#vz=+d-4X!@e@2- -8-NiFIVR4m1KLF=q=(Z^r^8Fa9#wSl?hl)g3iVI>kl-~pD?M>C>amh)pkIJ(aOYm4WE^i-$?H*pm&g8 -kGOa=MkdL0aJu@Qm=j}TCWOvU&`>^eEB}iUFaKELl)k{v#cDlQ@S%SqD}9Enx$jqrO2aX?X4gM -rIQf$wVXVSZ>nU;otjrQa;*S$MM;Kk8RG)%%68*>su1jGq>3Gg90+Z_xMq?_0(@2l4tE)cUsG%+L3z4 -}7st+^4%NLG@YG`pouvsxL~buh0;LJrvBxLh9S6*4OW^5B3Ou{%7iw#;b4i{ga%2%ZRQ_(DoK5>g}X@ -e{)sn;B$iNO&nPlsACJ&9j(=!ZWz&d1=jy%{+X~t74*I(c>Zn`ZBgXWPR1kqu1BnSO4FyFSEA3w*J3ZUt#7$gXrV=kukVc%m?aUhB^<^A7AQiwYy0cSO|p(7AO8cOEC!{{37sjxU?1fw6xv4@FFlcbgHzZ$EHOjs@ebF|U`+jWPcv?p;FXZos(FTsQc2qrfbS -jD!vRnZD>&tEM+Yx{cB`T82AEX?=tr4>0I(XMPWr{}}Ka5QpRejCm3t^G+IT(4NZ8#@QPBmwhC=;S)Z -cpf79@mSwO*6?8^{T)TqGd3U57v3{{HV%<_1hbl5~#vC?GLDHqADwwjv@v3qvSW%;cctmb;?(Sf;Zq&@+=EP?S@9<5dp(+hDwT5n)H-bm{=Mchuu-8?>fO`EV -Iu)j=gyGY}oPI+G$8rds%GZp)PtT=n^+}Don&>QA@?eY3X$R?SQ@By$?gT7f@|7GO&d*Fu$NPaKw_uo -TD(RW~z%qDvC`y1+03grVv3FM)FobokYfZi6dpCpbwn4PI3S)As;L0NzBZW{ZeQS3}Pt?wgx)`5O{pk -K>qDA7+lY;{O1cTbe!-9vm~Zxr)_UO1mvsE?@=75A&j%pDK?Lp-%-B6IWS>RqKFb}qNiG{zem`F&LUT -yGJ5cTHek@ORi9EsA8`1=PpO)CaW7BXgWk-u8E??P!~3SNoro2j%15!n9h)h9n`7r -{h`k>R(OVfR$TFo^izK&n))gFxAhBO+201yKjuD$`yRDDJ2~b}eM$OX1Mly}yoH``uUW(`YM^|}==aZ -Pjy^~KxGU<~R><#x7;w3cz?N;C -{-yR`S%r*xL5@3s$S4R@Foj$`o;oS&(EbpbXDqw)Ct_dC!V@96X2y}$;9$-}ex+S6iasP{TNZJZbyie -yc|`u~rB^6u#q_JhH3wnO8MN4RzoeW0Uww$DHBi~58wZ)iLk+-0tinS0AsU&r_1pR_!AX(91+j1BHKK -Ovb+;El-k<0RL>_7-|u#4GrAzC0ccWig9|L2U*r|U!rrso50%Ep-k3K)_YoZC8 -y+XC^evjW1hPRJGjM;(JoPIne3-G8>7x1V8>3Bh4ve*SZJM#wg~~!(ZTwR3@5~1A9eZV_RLH->4!v2$_M0V{?WeFt%VAruH`* -d&I2O{+ReUnEx6U|mX<8gBgCDIY_Up6XjZ)l3)>Z=hSn#Gv6Ie=Hl$-#c2j~HfY)1*cBTlE0?cv|kW9 -WS&(cZ+eVcQGbaKzy>AeKNPt4TMpGVt#9XqNpT6S1cR9$2!y_ZYt~tSLWO_{3S=CM(tr?7~3{>|@aX+ -G1D@d}wXbq-xl2qJO}*nKPm@Nw%-(nnka=)vVTTZ+Pv;@xzthYpaPCThp2i&R7d@be{_8-s@bn55#Agv{cYgEbvK8=^ -YgAZ+@ISl@Ep6Il7e|F=8vZWYh!`_Bt(0YAk7G^P#{8Js{6SCGnW6#vxN%T;n*{N6evT*p6hYFj%U=tQgT~qewA}y%00Y)dYW4@|J+fs&X|$_d- -DQmBHzCXY7_aqE|4NPjS3`v4dw!!A$SbI`P2u<*q -7W}q+`G4{@?4f`1HN|-^@Zl@-Zdg#+1~vkUfEt*;92dJW$x$oUA4RVRp`4EHM5fHt&CILMbgBXpu1g~GP5cUZ7I(M9*RLqnF% -bE&8&jIXON=#JDXXi`a6=xPT$PZ)ZY>Oc{qH~aC7d4PJsi7dFPkM{c)Wu19&R<=}62e;;G=FhQ-%CQK0PjnRJOU*l%`eMiL_SFZTi-uU|MJ(_$ETr?hM#7pPUK8~jpY46uX*gdZ?=QSjg4$|lO0ve}j -O8XGW)C&v>p!N@ -bOL6bPKIZqy@irbM|~dDsKi(f+Q@70eUN7j -3Qt?(fs7{K~D86Z?Fos;|GD`rkqQzF@!I?}e*Z+w<{$sBDqN{WbmN^ -nUKkDWY=PNS@jPd_ujzDD^Dq%ukf8GqIOozw@VEGZXydJbkYxIR){4voDBt{FvI|P}{LbW;4;2;Uz3T -aLwR3@RjnYE$iu>Vos;(;raY|OiRJXEPs}y&qdjjef$grEck5v-v5!X`+6964NIUSQ-F1x&*$Y@iO)- -crD?YJ({F8l{{9O0y>+9k72H2wZ--r=ewDI`68>-F?(-Fc#GVd(-paIA -?Jm}p*BNX7+@^DN({GgLwLSth0Af;K28bY5uoO7K5~ZCe+~H3lxj(Ed;I#imGYK1xCJA#jfGdx9lkZf -o-tb5xt3;0KtSp>xOecQIb$wu$Bo+5{TkOXEv>dh%HBU|iD@H6Qq{F?EC<;JDMCeT;a{apEr@sTeoFN -i$!8d^>hrm7kucJu@U8pf>Nr#RD|TEcc-c@PUK8ZS=?V7Je8dF?=r^y{4Z0yYTs5`}fiD13S(Oyo4`{ -9|+lPS-|`7H$>b(hZ@5^Fm7Pr{_;PG8@P+s2=@1T0_scndvLtK@$7Q?Uj{~sd$#D6D<~atSvYS=_tTp1O;hRoa$It*dR^d%_@V`d4`k0o*tO -_VT}IgW=u(CTuVZf6MzXuw4&bussU4o*Mzv4>V{6HiBx}G2d68tQ{t04dWB(&retK=ia4hVXsGSx2W; -70DOlYxtI}sBy8-8qiN#-vmnbea?`??%2s`aYzKwO{XLPDkY+_x~$OWO*<6d^xc92Db7HTJykRI)V2RJM-pCu_o^SC?!=p4rJw3!@ry8 -g1X^PY>-TigV#=gkDPOnm*sz(%0ivK?YefzY)9$b0=Y0R-eHyNl-}dVfuD>u&Yb6D5%aM;k(D97H}FL -(WhEc|Njfl)*M*qX`!a{BCyMir@9l_oJCy~w_1^e(qCD(rb47W5YI$__D_gcdw-IL=+;b!5_0YA(mPd -%+Iix8GC~GZz2Pv(J#$*n#qSnez1JS&~R$tAk+Gid*x)`Za^!yX+J;S0e5dZ27+Nxv&pQi -h)fVctVtO_(pNZ%x%dK%Ley8+x<8!eFm28nMHD>8Q6UKVh`=lWm2N)+yq+ZG@jcHtgLQ2?Tx^$rM2go -#cH*(1IvYc$90ivxoLFnL|G1e!nT;k31ueIJULJ60(60$%Ql*$p5NZyK6sttR*3(?C%si-XP)I}T``9 -aV18Ae&3g;+5%y&@4$)kyqIq=%0u2nBUVSVK}XNrIl{lPAjDQJTJhq}hj@|m&% -4LBRSV~z{5yK*#?1Pj1G|O&9(XITLLF*cRv*gBWuoD*|Z?B6BArx)~r{e|Zl -C2N3aq?ZgPPc+Vl>C;zj?d?~TiMhxen!KBdtOG<#5`*;D@un&lF`fW@f -M>d+`!DYb}65xc$^dVT -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 -fOgTk+Evx*N28vSAT?Uq&FaJw!E`9Emld -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 -dqr4~O^gUH9R!-`#_HK%Y -VQ)AwV*_VgP{biZ)=9Emp(*8s|aX)y(}`cj4}j?u5Bdk= -=WsyDxL!-c7i7M|VHw-d=X^#@u0CfBi=~4{|g1^K$x~&UXES;93;_1B@32Li;O*c3SSm{r$xJ#eH5){ -k3-X*PZ#R_Hx!2&-Jx*_SfSM`o?p8p3eUK@1QS%>vQWL1bxvks-qU_NEzAz?tOWlz?NlbGw#+M-F%(g -dZ3%1lbb)fX`I{$bE9eShVdTVKR6ZoU;17_D)hf}&%spaf9X5QER4k7;~Glm*@E!CC7!Oo16d%*Vm-* -PK)A=G_tvuif*x(*o@?h7;Kh`8@MAU@5ZsmTRRuEUHvrQUK)|Nj>p8%u(e-$FUE3N0xSvLq_M_xBX+(f{&n0x=(;(iKe;)RuV -<-IujhN!(!B|K@@G`+yIS;G$#i4%)C2Bj;cmPv% -yjQbx(1K!_vl5~zKqb`*#5=LzsKx%ytxfdP#)+p`(1i3O2?>i?0sGOEE)X$mc#VE2;;5fiQ11UROBQ2 -4#33z+M92ypzplHw=SU{uUY{o+{;(3gmXEZC2-37@4EB7J=JQy&vXO)K6;qXkx|`;;dda;mpMQ06xUn -2KTHsg4O#z$*VcQW-du7E2nv?&S=xW0_H#8oC9kDhU@Ykc{Wtvvju(WZ_sHV@zS- -Fw#`F^6g?-EY2+e>UJy6-Z3jy6Kfc@~44PNn{o%D(2`ZfEnMw9M4KwL338ngex>v~_+4?YtkDnrMsOh -+D?z8rVDvN!ob!GZlDyz|fBIckRp`BeP)2F3VwcUQ@BB6{dagv$-i-C?KAC>cw4Cd@XNb4vp8l -wdj(=hyJiV{7wTfG2fN;6tWMzrR-qAGPkozl)YVZs95!)eLQXu_Jzc&V9ycfAmX8V_88U{cbW`kRCc+ -fHuPL59l*>E2z$rJL`NUFwoK_2NhNvYm;%ZD4OJ~NyHA^ZfYL-MZ871>@)(+FvzWxpAI&!O+}1Jd@?K=TZ;R4Dfh -7+>Z>Tyr1|@v6{~&i;zgUp4U>|C3MbvPPf1qiNdPLLTNq+9$y|0KyK2u!(>PfOOw&+K04~$o9a1!>j3 -MsSbcW@ -%y$?+w2`wlf=Dj`>`5N4QM?_4$fhtIdt@o6a?ri1JIx(Zf6$Jr9M1@(&1Id)^|v1*D>b*)RhLI<@2mD8!zmBEb+ -(#*Q{rtHU-t(Y#UOCI}))iCK+&WwA{JetZoBGMHG!) -$cL`6wEs*mM@^btxaiE{;C3my4Ki89Zx4b}FqwgZ8-|iZ7x4eLS#NF}&ZJ-#x3o+ns{=lZOFcvTGq}{ -oN)(`qU0f~Q4AP2vNP4^q~k-sP46nf&kZZ#x3%E(vgG5oaRKLwp?ZMa -0dBA0VDYyn?72z_C4I5aMXW1jJd0vk?~}mLRT0tU%n2cp7nL2*-HD(TE{}59BxvF&A+O;u^$v5Ni;>L -;M}lJs9(Ys6iZrI2thq@gc-{h^r8{BJM^!jCcmI0r3W6+d&-rB8DQyB2Gh0M>HbZ5T8LTL##mDhj;|B -7O@`jPsH|vF`tOBh$)CR#Ags!BbFn+gLnY(B;xmoO^7Y9yt*KY;}(|lJy;IC2fE6OM-Z*##B&C_a&eR -n;_k^PuYQ+f3Jk_5rgf0+7_NK`>RZI+4cqUmUwQb(tC+tE#KJMI;Y^#kJ~eV1a!-*jg}fBGmq-_KZ{( -EYyAw|>RMkyk+(;ltdOT6Z;QMT -c{k*x$h#x2RLE-;@+O7cZyV>mkZX~55%D9}ATLJlkGvdtU*t84@Quh@3wyokYHxSs9>RWay873=>6*T -%H(mRmU&UX45uc@y#=p}& -IjuE=96T>Ik`9Xr> -3UsEc#4gKHwRd`ixvalg^T%%XPS0teL=bWp2$eSR4+isaZOU)tS%JXIS&i!Ugo^3X+T(U)MtvsZLP|5DG3w`wbZav8xq7RfjDg=_VK6=%O-KOz9s>>m -a6a%L_m^+dn{|+0`a3v`*Pj7UDT9OIB)oq1WwIbLnDF{Lnx3G8E*1&{90(ZLpO8lYAA)ls4FiD|!jB- -34}dcm&cSf@q2VCVXi(4>elLXcAvn|KWm$FUx%z&X{?k?d`zj -JVdmD{|ZOZ;~f%W|Il=Cn|M2D7yQERkujm~wR)`aHeSs>#!tL_YO7n!ibk(%&7ER*WlEfDxyrS|mET!0t^cB4WBnJDq~+%s4H{a9ECyOD(i}$OK9%WH${%>p -#YbA6&Zx`QXa1d0mHwJ7XXo-!Sk6V??e@QmZ(4tgjJiB%8U~}mYJi?&f!?gi0&gOpLawvw3k_E1^f~! -*uE%$c?zZ}Rci(riQ{Ow+Tc#)W6(r|RbG4A_mz?xV+q@%l0a`V~!--jZQ9n4A -)*iJs+-|=DobbdB#yj$I-R5$P88vrOQSOX -N`HUn{=diHAA!V33P>*e_2=AJkel1zA*b_FEuH+seOr~g6Ms>`p9St*9?KPcg@TtT_(}zTPQjm7@a-y -BzC*#qeTryj;=U}8SV#uRCuX9DzdR%j_Yc&zo@jG5q8Fk!%33Npy3zZ?ws=za0CXRLJREr>@?;D%3Ed|nPDlMSFz#G*& -m;Vv*hKifr5V#|!?Z3&T!QY8p`OPvJk(RxV%8#GkLlQi@%#(j-^X-)hVoM=pW)8$k^DTlEi&K_svDU~ -rjQu8V+zG`5_||5NfJmj@FWrkxt>6xfc@{Gx$Q4d{<~dv`(G^7xV!y5euqC-AB&y-q<{G59Da$@pXwj -}iLjb~xB36=hi&na#~y#;$)}2+Ub^g=XP2)iS^3=at6q5VrP7yIzq01lwd=~(Z`io$wb%bu{>J7lTer -RWR>j-zyt{qJd+%57{NTe~AMM^#wRhkC0|!6;r25d|Pmdh^>{!k56Q6(a<;hdEU!DH?%s1bjtvmPK`T -7grUu?Ma!{sYK{`7O>)n9(S_S^4&G+qDmuNyaS*@>ibbN6WBsrK@2>C@_-*1m1pwrk&^V<*4PUAlJb- -osxL(6d+XK7IQI_755`FnG}5kRd~d4IeS`-q8E*9|iXeW3{^Uj7)u2c8+28oZLKPzG<%6Vl9|Azp!Y* -1G64{C^fBl`NInrJ@V+kJOBUg`2W-P508k9ijEl<8y7!5Au(w}^2AA#r%atTJ!QsBZvW=x|0C@GKcWY -e9Do1xaOnXh$DisS{yBO;=Y0Q@{3`#{cgS;FrawgH?cqhUsah5`VCSoq4714%Rf>?@Jj<_AM60r)g2C?Cw>wHHOa`G|Gdm#EE`XOo%0}(?IwTQ`xX^4f0C5UB+ -m55b{)rd8SwTN|y4Tz11O^Dn -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 -6^n(&GKYm){(;a(?N@_B -D;&XbfW5ZhUFC*RC!V$}x|(+O -)Zvc@oHew#_o>R>Kel*2>#{MfbN<)QK3}lJ`q0SL#)CuNFBy3-BCvXS(VM-$WeeXk!}qQ({_^F4kzaM -WXxlJo%mk5ikT{V11Yo`3gD(rZR_`TDidE3DO1mfx8EvGvFo`zs -@i8DC95^1zM}mu|Jo)lEFmiYGs8YR^41fkvHq`qo)_HU- -M^r6(5S3>iqEZJFH}weO<`qWVykm*G_ax%p@Qg@7$ygJU1ygHdAFTY~R+pkRW?z~g-?((_h-SsD_Wmhj%%Wi#CExXfxgbv -&q;L7FO5AOe;l!!K>HL<-|3fIx!Jm`0F=m>pCFZu+Z;zargA8;~aTv8Nqde7$~e?B1Ap_L;S>uf5Ji? -uY-Sj$s~T&&+|KrY_fYD6y9Dm5V&>wicC=dF;ck&E{-e36UyJp7Q0alZz+c -yA#Pxp+?@1bI8;p~%I1G+N~1y@6Qd9g!y^_d}k7T)gLyhFrYYk%L^kH(^39-jgUqF5a`TA@6~_7`b?F -rUbbLc`5P$3f?Q -tLjyxWD4)XEHO~}P_$wK54klT=puB#aNG~^}7rz0;#J`;Hva`7a%9QmWjpJ~J27nQIBs6e?2c@=Uua;T$nO#1k+(*khTIpq33(eK$N1VJw -;^vQIQnld@{7C!@-pNdh5eBG3Hu@MEbNE8i?A2+uEJi(y9s+;#{NOr3waM=ALRbRKFBq~KF9-veUSGQ -`A6PM*avxUk$>cUME;TY75PWrPvjqYf02LWK_dUi2Z;P5A1LyVJQ%t93ic1k{g4ku9*BH6@=)X>kjEk -)i97}Qy~uNrhaxXTejoB;lMLr67Ir1>%mB_~;uSTv#UW+^&c?0rDd5BL -B!2i~J*(Z~&r3?vC7q?b#D~GRnP>ry=(i{*n72w;}gMUV^+G@-pP@kyjw^h`b7U7vwd_yCZKvJ`i~m@ -=?frf5!TST!TCkc?j}3$YYU9IG{>F?v6YMxhL{MdSuMz1%UMJFn1GGkw -9x-5R#PlHdMec=MgWMZ=2y!3fTI5}jCnFz)JPo;o13VLQcjPt^Kk^b0Kk_mWKk^C@Kk_OOKk^z8Kk^0 - -lPcpP9hiSWqPzhHRee#l254@55Eo4TRMy^zO>@W@kye;i=u2>-|ng@5G5!aq)*lnVdI%Y}dBmBRmU%) -juDyjJ)}-XQ$r0JTZTk*j}2`Dn~Pa&P2;LaxR93pw&w!Er#EB6uw3U+@IXzu-84EJp6W5c7|`3-WU0I --J8#XR8(nn=7S{HCXO`qI1{92s~`xQ2c<_AF(7jNIq09S`XC^+Ao6tyvr#XducwC* -^%{|zkekrI8QsmOF9+kX;GDjN%tf5X(26T4tmv-*(LgdW|9bQ<^26iF#qhbvvoM|WFn>9?wm}CL0E&6wqpLdo@|WAirk9npM&L)C)-1Z+=$_F(O*7`koTv>bmfOHYD)pP9?#39|jNSb4_D>6ypMYnpr?o5Ni}BlWCzd&!S%_aH%(WEZvE6zN~E -Cc&40>LS`2SU%{4DiSe}k_=E2y(QDQv)D7pUd_!H&w6!AOyL!(Q5)8qBbaeXqoq&FSQEBv$)aZP@H -3q9%gfodoklvPi}yUP;U~-Wh=-3*lvA>-pZkxO^>P1Goa(=Y)&EH@{u7+i!|gWR#ecM19^C(UC%KWuA -M4^jQohb}|KW=Ao+{@@hvncJK3O53s3^w>r~Fx%eWtnaSf_emVYMVqq2F~KENxd)(Efb9D4r7wyNKuV -;<y`THV&ih0 -38l#6;on -qe759MNBra>`}8Hn-*1NjbN59E7=J&=n{mUEDA681p8P1pnZSz!<4&kK7XuM_q_ -{-Ll3^3Q}lke?IwK>oGJKk{!y{#$VVmB>Hxk41dg9>qLrAj-wOTBBkfG!*4xJ#39)UNsiw;{A;##XNZ -m%Efg<%ySMB_C&e-1_bIC^Xi2tUxmCF`D@5ak-v$&9J#oTS0aB8c{TD~$V(LSthFdF7U@^a+cuzF%sY -yCeKGIfgmQ7667#rXURdqP;r&ql7V<#kVq75B4TyCGp(q#EO)*a`HVBMG`E2B6ih1}Hl< -P$NiggM(C>Qg#VqJn*w@`@k7mybt7xyz_-GX>bUW#&Yofh-pVjjI5<>HMTG0!dDiKs+*A#ySAd_VGPl -rKRp)=9|wKPu+YYtjGH$QzK0`vx(uE>?&)p|EcpjLBvHu5UPIt4$JXNmkFUoP}3)*XbRJP&y+ -^2d;idGs;JQ&9dSaxo7-3V9C7A4e|M6^K>%g(!bj=tnL#Xe>oufV>>JxPK7qP{cZwN|e7S?1fy6TWgW -Ogj}p!5$jqSQ2s1(u}(p(t7t;`Bf>t4buel#E*IlSvF=5zi}6Fb821JuFBSPg{*2I%d>!%>jVbv2!k7ouE@i^Vz#e2h+tQT_z-Qsn3*3F1@Hnk`hi|!kc?? -&E)d_QuvH|G_?p2#bOJ&}JZ?1}snk$=THqga&B68XdD_~((QpnRXmAM%fcy^z0-ybSrv$i=!OdH+|Gu -NL+}F7{s(>u81v`=ES*(63mB(}41&$i=!DvCgLnGE9gSG`qi)INi$#7E>#Y1x{t)s&$JqWt{jx7As6e6a2kmeqI|7LAM%Hhi*;6FT~;~Dal2Vki98> -9HFB};uNL`t$QzKCAa6px9=Y0w^LK*e -bM=LY$Hlk?efemS2b_rILy%H<(=kqggLq%U8-esDflA-CMdUG-Vz`o(#XQ+^k){L}w4oSwHhyXQJR-{ -H^i(`Eg9{v}`6Q8%qlJ&WPg=Z_cK~5j%v*mKu<9lv%~TU>ayQ~D{-k?RfTdGa`sbE8~-oSU5 -T$GKj?%?e)R!Y%T3m-7OJe4dj&D@(tF7r5ky&qEpH`1rhGmYiNb-;*WVCky)-y$g5LAHj26c$SlXvrG -NW!hS>8o6mzd(#Pj%9PN_NTRPs;=krg_ax=DDXPzU+&*wRF+ssk3;x8s6kE-pXYGok8 -^`uANYIov*mu9&!1(>_TuwhbL93hN73(yc^~1z-?N`1r=QP{&5`@9Tzn(gD)V`ai~7OmeH`tP&kyFw{ -SlwXa>UP{7Uwx}vqH|>n=8+g>t7zeO)ApE=Y@@O{o(UMj{3v7Q7%s-_8$)a7MK3Og!R=Z^IXcga*N -d1~EV4ET65A8=X=l<`W!zF022!a(trwxL#MC-JP$87QAko?&zPpyZWD}u+Kb&J?69e?chZ&BLl0#-d5 -}n96X0zUvgybdOfk?xWX)Rt4n$EdH(|0KVK(clFNtJj)L399nS8~`Q`Ka^W^mL`FBTp_; -T)A|2cAcIM0*o7v~nae^`L_m@o4Jm-z2=lvk}_k10x*tYesB{TIU!@3$D93V)a3iN^OB<~{cTL(hJ@8 -9s8VilS}O3;P%!ka&QHw;yEq^p;PUeCDJ>j34^-Fynu$ID&dUW9YZ@7{#jI8*7++R`zkm!~36L{JRE* -+Jx6XXYR{%UoiJ=UA|=a(Q$^P`pJ__e(Jtcj5qzj&^By!E#s@keZ}Oxer0I8vgS0?`$yE*j3-`ZSoOz ->GtB+g&~I3{*B0`+hA$ZE6E|4;k_u)Bj -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 -L3jW}TD+*a-5F&mjNM-)oN1aA_*rFILWt{ -Sx{Uz!JbZK;M&aoubzlz$(C+lXUO5I#uZ@8Xn5$6iru}ss?QX_dM+Rxw@~_RQgA01;m!rH32=xiQx*FI?XT- -d*$=cIKd-V~rtPL)(pI!WiS*q9{>0U5tD)mo<4219@jnFSaIaFSS{sDN(uW(%jEcp%kdqB+}gyd -5GCzKmt8OX~{mkdDS~61zSY_K-*&xHkaOekjc! -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 -8a+g9CnP`AdU#^W -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 -Xxh#ZBV1-4|2u9aS;(}(#uFT3tKtNGJz-US- -!1srtY|^#uV6b)vQI|U+P>^`o#B27yi&$yzoaqdO~qN@|d$U_#@}{f8_K|o9X=F6OTSNAu}`06|19di -2UM<%dT#U|E1(@Y#Ao|GmCdm-=-9#?T2{Mh*vn1tASk|0>|N9cW0BurY^oCI$LfB3JmMXcNm&j9<#ADU?|3KeD*g{C@Z-Y=J*kZpn>Gwab&C -P8*@kwX=(TpQ-xwjO8OA1=(2!vR;WH$2&J8*<3&a%#6+1x3P{o#z3YIP#8Q`F5z{&qYv0WnJ)kx$=;5 -w;Xm{IFCuKh9d*>gshdZ4Cxg4)5C(Gj|qgWUtzZJELW~nYZpv{yQRtN8dsAp2qmGFE~n8}iwkZzZiD> -W8@B(~u~=v&8(B&a%El;?AQ76?6-r!9azE -V1pCb&&QVWD#Gi*H-ixhRh&O*y~Di@6n>8aB_)3v6_b40#7F+2u -ZM2xM;{ATM!^;}A?4htgY=_L+v@fXkb{y&`=*bamWe_Gf7}Q>D_;W7E=5vvs!c1-eG-P3|TGIgDE;I^ -=;#?(^@91s!fmpj<*5GIRVcxWNvM~QntQ5xr*2s8}l>!9gy*O`4Mr=9y9h2ru4@>i;x$_#fZ|5*H`#uEJT-ZG9RQUc02u%yLU!)=6jQ^EHJ3<*R -)#7f0F3W76y9M)%lS7LE`W+RA02kGX6%{a@n{X%Ss{ru++|BpQBHWyFv|}T2Z7|3n$lA5efl%sD@Lme -*E?#|rU0(fIsy;Z(jeEhh0OsW(s9L*w2u)6~QMXc~RFC#vCce#eP47A^53iHQY=ym&JOAO{u_pFqfy@ -rdtlMyWeh`YNV+7dKxk!CCnuJ(Knggs=X8WPUIQ3DN6L$EG&==MYt!5YwE4-`GWYPNxZq7M;Z%&8y3U -F_#C&pE_1P8Fp`(zVfYQ^T>V>s48Q#I6Cb<4O0?2jEsnl)PvzwIjP0?;f!QI<;B?wVmSY*)F2POq__rGNQK*L2JlOd$lNC&9#q9b=;~d(WLUjPjxmA-8XJ -@oEf5?E%6X!yqeb*5*TI_a0owC4giXX@%1Sp|;a744ASdiwN~~P=VPvY1)gp(}7NOQQ^^0xb_TWHXN- -yQ!{3If@g~IOc%N#(`m}|XP&7IGu_QIeakbocVj5?_Yk=r$J69{8;~(H{=xwc>~jJzqcYCO5+A~l*Hs -|H!sQA0n=8Q2I1a%334=R1*8aGi;LB$?+}Jc_1r)J_L!_70x%YTO0R+c@%UO$zm*Lpmjx81~fzB=KLT -G7nw`hx_hQYiD;j>(GU>ZZ#2k$aL=2zM|bS^L)-Lcm>>NXha(s&5>$#lb^w$rSyPKA!CWa$?5Vz@P(nebhLI4!JV)ay834mcviP}9{87r}U+V2O_xQ9a7o3Lzn -s?BgrKNv3)+>S&t-ERi)?uzUZsB1$Zqg0bY74Oo1o)O1XS9M`sFbzmYa-+CT%d^Dle%NGxZ0u%ejDQd --HuEyK@QKq*m|bm5ie=Bh>{i}4O=h2zc=NAThKkkc7=LmA#iz+?LlPv634R$%*h>kDt;8D|gNaBEoaf ->+AgPOR1+)ef1aWB?_F9R0=jhfbC}xu2igCW<7}RJnc(|l;vkCVp) -Egk|rDVvZuN9E8PtH0LoV)?hrd^F|^-8;ex8$2wmo!3$O+lNp0i;Ztz{m%~7o!eGYzTi!;;3>4=P -~s{o@l~wE@zZ@uoDnZCfk*?uV;3TW0`2g**qnWFCI;(Au-9)uEdjRDRls%@(R_5B;q$>DVFQ73IP;;? -I1Ru>?bY71(FR)y1U!5jvW(#cFfr?Kb~G2VS3JUS+^`P>=$Qk7|Cr?(8NAL9#K9SU@GI@1B%C~GXydo -kk0Jn6MiV&nG)~}ca&dzkehDjqy2=39iozDJ;rQ}H#HfG52zQ-dZ87}p0&vLT=}9HuyyItv+_gIpmLR -r;!sj6@8!Z8IeLSY;k{Ag3!*gvZ04LfPR>EGMGcjs#!R1Hcz=k -J0}aDxZxkn2fFY*by!*_}nKYwp9k%=%qSXlp)%0#sbw71|!>g%~+hMNV$M081n3)JjzE90K|A55AD?c -c@@X!0NK-VKWA$9Q5{*x4{U;*6N00)!bG(&Nf|L-%BgLK=(TIzb^0g}QGB?L#AUA{PA-b`noO@vh^Xc -uf?9yw7V?(!k>4tqiPXu@02a5&>;Q}-=+O4NfWOD4LRnfdyt4-2{c6gnQ4)zrP6(NiYFVJ|6zxx#3pp -m|Lk-*Y)Dq8TrN6A!!#*(KKKL3Uf)<(8H@NE>Vr@cW;mo4F2&eDtrZrcP%;oIGR+k;d48?vI;s_BSxctZR_zy8@#k09^5-Y9XX!PZO~V!=KCDpoY -+E{>2>bn|29P_8cxfd70ct*^!r_0#M5kn2UijONVKOlDp$2BYjJ5?^9M*C*3q1rJMr_ -$j_PAz`OQ?8#ook!4JlWmnINQ{9+H`iDGzYt6_4WsRto^C!}h}$75RdZIoqF9z67ZR~mpisdXAOhr(a -xW_Z&sOrXX@;Ki+2Mj#y6L!$cx@k?cpCPoWlR8J61d~`{f~K0RpJD -(Ky;WoP!)xUsv2rg#TIRTE$eI0S7X~_TcjThzR`ExknnbB9cX2>Rst{nE*o$!ireQcwR=>89)Gd2@N| -$K;Jp;ePm>lneKGBN56h+6Bb{7ez~>?|1rOkw3`!K<6ByRYJt!V~Tvg9v1o)^Hw~ -rCq>(??AAGC0K$xdIiceafuPu0~c#gq@)LtAH2h|nIdy~P&{zAfNWrjOr@ml#GpeI+Z@Tqn8e=N4

oQke5T~y^Zn)SUt?ok{bXbKyr%iAi6X}`<0WX&Tu@tgSg~7Aq?2!;ynyJe1U+{Vu5 -DPO)#{>^C3cUXtSJr_or)j!yBNwf{+bWD;L)R+p1B@f{c5DPiqHok;ApBLjmjCv?U%)2WG@tngygZztE-(msz2mRZ+IgxtJ|gd-5i#+lEPbz -@XWaz7?M~_3dMV|62qUWJwnAfKF)^Z3_2iet;LE9}?SAgiyT=I||r$80k!61mp|A-8Hb6pN_mm$$ONe -DB%IV-mRVOL9SsgkfQwJLBTuaoG!6?#2qE-IjJ-aeji+JDN!4o$cU&>ii%QHos*blV~0{u0Nf*pkGzUg+kZRo9@QcR#Oijf>2{R28k6Fs(y){jOKk_HN33qhsrTlA{04jvtWvJ}dz_OCT+9ZxI7V -`SyIDj~Iu|AeX|o#$&Yl1}kZ*l(Jk0QNm;>b}vGI@W9e;Lg{9G2t@dt7a7K+sw13^ZjQFYU(>Yc#h4k -%I2QruaJ+wAnKo0I%TGxoU!`@BurB35q}t6OrK0dTQO5J9!sNX+80ejlA6;%v41$nuq3by3+Oi@t6E_ -wwsav{Fv(o$FzTj>L}ofT++u(H=t1o@LI3Oek@}wuo%w2n=&OS>x(8OOY&YK##KoV$#?{)!ElYZz|uw7;vJ6f5;WYiA=aCRIv!F_a6ukuGDkZp*JFd@vD2~aZ$P*V_4OAu#}4CxLY0X_=&KEcnn`#q65orTPM-td1eEczPg(kahMozZU2->y3505WQ0k+QAL}!3u-}3>*PGM_9YCfY^5ttxYDNOx-r1}k+%)RFdw0gA1clBhYQE~LxmlFwO9ioU`xiOZT0q*k-3t9kWPg+lp9s5i-o*$e~%TB?zy>iU3?3MU|bwYqUkf5*z-^&^U;%AUQft)E -=G(lklF4SWsA@ez4AJv*GVL9q=?SyH-|v480%WsrTm_+yyE0DKDIO9X%T;Vob-_($n3@$ -U8F6(3#z+zxlLk?0obdHlrC{0;FRGaW<_i+nIGP8f#g__B0~ZT7Kmq^?t -Xs?4@}*%^OVhd1XNJq=$y$9>|IZ=iaF=qGShcl&@`$C~rAeL#2r=CEoYg@;BFhW}qg`6YL>@ -hdx`kBz8mWpV1Q5O7Xaq@(W(4=}B3w$yb24(6&?QWGW@5sTW@vxN!d0Z*cnf4vr)4?ioR4AtL1^td9T -~^t?D}y(Yl;j*PT~^%NFcPp$t5g5J4JZtY=9j_U|E2`@(@A=u1;{Yte!QLv;f=e-lIK)ZC{<0fS`X5f -t`B;ZmuA#=vso!00`C~f1&DryLL}(PeTsfPNI>X#6hkAv8s%;YI=Y@3Z#n!CDIdv@CkdV2(`_dWp$gO -6&OS`5d627K$?yCks3VW7po7Jc$$MK82M@t1wd#ia3$6`gH4dwB34^u{c2LQ1z?93(4m!=)fyq-EBLO>q9Vs%o959&GrXm%JXFA_rA4v^RMoJ-4@u2e2a<=|~-aF>qyy`dNe$n=NO{Gzvk$m^B)p% -{5`#z;<<_Tb>ekiK*PfHoVE2a)J}GZ_L;*^rK`vZ%#2U^ykW9r;=9Ti9%@qq1(r9yT+pYqJfq2h(zv+ -E`;`6iQE?K-l_1u1R2D=R9uj+-z1RA{l|+)JLuA(NTV8sgoaXnbYaD*~%0L@_2e$SzcN&$=E6zb+X4? -dI}KLaJDr9!lqEMXT>S)T}Ql^kp}@3IyZwn^NzrK7))`WjQjxtIZxS(*oh)`+WeHwU*BVu)XzsvWtNV -j$4}0YI=xnCZDo8EN!oZAg{Lt%hS<+tvbj0li6-hqed(#L!Yni%&`5_2Bs*SHQOkdS{5zPZ2#5k8;ev2Qlkb;HIMW-W_9|0PIBqLnZ?gi3=sHPkEm -o{zDtwaR?Rwnc|yJ+6pz<3NC;V-U6$k(P1!V?hSAMniJ>ycROvfZ4LOI|sx`jGnIDG$66|oBGq*Un+F;)pH>Z$386Jy`iH*?N>bsB!209lO!x4g51U3cYOIyP}BgXPCM)iV%=VhGl@6}EAZcd4{V~4vh+My|lj%z -R4dGE)A6qlx+GbvW}TvG63O-_aV>IJrSA94m?;ihfJx8_t-|M5+GY1@5dC3VsdCJ{%K{jDACBTk7GDu -LXgpj*z*-U*RPP@6J~3^;84O@d~giJ0rjRPCmFNo#OFl&!|H(6Onw_3lDFX7`Mk9+OmNZY_dgPt&rY5 -L2Q~d~#Ay08i|~ySq_b24S;#LR(Bt=8v98^*HCwYM{c7k(fs(8)%o6ktZ9;O4lHFp&DcQdvO5fLqlEvCVL#WqDLk~ig;jnE0>0v|BgQRr* -wgpjMezp!z#o|7s-zXPbhi!wNo14n>(0np@yf!@v&*J2);9>V6wFcSte#=txUycfR9@G$e#r03@=E@N -S3!~LZTQg093B<&!A*L417`69?#uX8JG{exc2gkGiCYJRaNSd{1n5aFU$odz|`U$X-Yqg2D(-f!G4#$ -M!I-d6Dt*32Uo)6ph(Ff>#64a(Gm=nuOO3ICpS1Xzwi$W^}?JbBRK3BUog7UeN0*m-d%=cGV*iNr&u} -Q23fl#VNt3|Kq+ZMo+e8f8wzre{xVgV=wi(P}oZigRz7>la!(M$+I8>9?{$nDw<@Vgcl{}mVM{K~iBO -2l#hIPG^2aEp$}y`;n>kbvc?;-_n4Gf~C+^IAHNwf8g8G26; -he#k>%UnT?`bTa@bgUD2iP}gMLlFk*zP~iD4-cJy`t>M`F62tKun3jg$X8mz=v%9(BoPD&{8Mb23~XqP{f|J6|ozcWbv~n^N -Mo1irCQrJ1(=xn9LSVldJ-3N!Ig%4I%>Ue`3|1Hocsj&qVA2Fucpl%?SBTlR2 -x7%A@D1dWp@{QgFe^L_GgO+-XKm??zRHt|)$7SoZp0VQ{-(O+tEoT4{V&?}|`4*F~VGt)+DI}HXR!@T%XpoDW*CVb7S -;s;aO}?K%x!c9ruv3tlH2ybQX0h_7aiH(JvC`C^8*zj=-*Z4wT*jlk_@nMo;_ln@|l`^VZ+!-*s}%Zn^8YTz}Rqvu$ -#HG2KuBEwc$OGzIy{v~+C#E2w))1@WuPLybRm)jr&ntN*Z)$Wxev -299Qcl}w5CNa-EAZBr@~Rd*&~eG+Tq<~w3V1H__8bQ-B^b+g(%^XVs*A4oa7#ERY6R3u%QVzelCsF5t -?8a0xp3{|@=!RLG^U!j!gV{Pg#d*n}8eEruc5yVIZolON|C1M?QS3zVA@;YpLh}xYjRt^JL>}nF~03Y89t@P9LeSiUSe5_eq-`P*k!B -94ZzUrrE$8u&sPEi-d^cow^-FDZt)nLWu4V+OOvEaU6gwhSiz|NFGRJD3}%Ujqrwowm>Kd4Q+* -o9sb5~Wh01Oi4)e`i*epX@=vb&Pw`(Lr_X&X6fiG$Wn -u|y9%-dke-8q@RSQ`)V|yvo@Up|tXysTPa3=90NT{ELEivZlzbiqitMIQh|MhDVjZZ$Z-b=AAxXE&2y -I4F~U%>qgxyNDYf+wwxznx3Azg^D -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 -uaChM)|xTs?|{`nh0;AZJF`Tt1Y32 -Grakax}4&(&ruGq>*~C@uB(;td2>w4IB{K`xK1{Cx|ch0c0rxDWkZL`(ky=SUVqN^;2G#xQeIZ@137% -x$XdjKWj34NyoJ)(J(lvw8$bo{n9HIJS0kBrqdxWUZFZn)gmTD-=V}-Cj9=t#Y1P|KFJZx -gG%Q|%>Es6t+JF9>0q^-tM;OfNr+ps{KoWLe`KI%0BFg~@(9WjKn*YUKv*@>PT7;$3Shh9b?x98M49X -wpBybD<;HYa0$J^YtU~A>G}CL(2X+C7Pkn*5ihu=Kw<%yc*_V%14;-BL&S5w2_ck`|!)SK2W#1Ve-C4 -|Ly-~Plr5_I%ySh$iOta_7attc!!WeS=|Xj#QIOJIS# -Hh9l=s?@-cZW=H!y$dM$gE24z^rrUa>>YV5o20&Pjjx8NV8M^j!eaWw&E_7My@S0Nl^vnWbTc57Q!&# -Svb1w?_?cLS!-etskhgs5i_cG(%i^cK&ZHxEMYpSBV2S5_L%qOpW9Hktyt1C01X0!xXxnJuV+&S0 -jKMfNS{V`7X|QKnz&*kiC6(Bs950l@vuC^5`TIgkuSWzc%kBB1e$Wdg%$s^)l_r^UU7WMUrSdj2wsET<+EY|)8rN|=!^@Q -3h5DI1KtF;%7(nVN{?(ORiv8b;*L~mstsI1XD$`JZ`L)^>j4Z1b?D4uXQ^=m8Zo*dVhB5-Gjn~vg|#h -f9=9!)IZ{~VzO{0^_4Ts(@sk}t&XdVL{cw&6Jbg8|kesmG#Hr(g-N-O50T71)Z3r(3$(1LoawadTkIZ -hvvZ-E5GZ+?HgUY@TZCl+2=%0j!x>J<=h+956Qr+}r%Dh2NN3rKB|CA?Q8tS -^?pDVXW1!VWy@;V3T;TL@$OXgrli}v7Qo!b?f^rMdcT)7qdxWVp9#WcRwn#j1XhQLZ9s`P-vmSol`!b -Dwz>4HZ!bNd?{hw{p0Ec(gIs7lVk>eV0{+#N7078T5+{G~Sb&Wfi>B2@o)4A|E|JPV6*`K-zXf`0NR4 -KU4R!<=lw~+Jb&EX#(*Vzl0G^jPLvs9p2#Dc9ttTD8aB7xZ+|gP8OmK*H$6;J%w5?XBW;v0mz8NO1v% -Wyd)J7jpz{96*qqE+m3=KVH>#Vnn6(K^Gg?9C5j~p_Gre?9iEJ#z2e62Pog$lF&N{87uXl^_l00~C+v -;+Pf%R>tw6imlEWgaCB5iv>G@ZMFJg;p(MMHunX60TtFQ<$x9Z-*^D9M4GdO2j5&CNxu*{>eC(_bG+4(9Vf3Ixnb|B{lDwQG@Zs`WOsD?N>_t! -Y*LV_y564dSQ*0W-mWe|a}vOT07}^M_`@PMBebas}{ghrc6G9v&L}b--UV*si}Avj-6y`okZTfFE82U -fS`83c*vupAP(q2HQHyGF(Dr7PrB>;8ite(B^t(;xj?3xc- -T(%<`wn2`e}pTC*t*=vCJga-JlwaM4>I!hVXzG@H1NRC6IwAB1Ns>JhtIXHT7yrGC#A_dvV&ZU#g~c4 -9eZ_HmFW;ygz#u9sMQlkW%~wuR<>hIoCb_LD;(QyQT>ZN{PaW{+$IR?lq!*{XU2-aOXLctM!@axxuA1 -z0o?&V1olhT*R_tm+q|rn2X{oe!G?`SH#L8^OJG1zC?@A -JCboJ#mZaBz-f=N+q-7*jd@ZH3g*6~lTzCSN!+-@TlK>QMvF2C;5eN%bhW){Qr=XL^#s^A`hr;a<=wj -EX_v&w?SL>o8i#a{Y$Jwi%MZqtxY~2{N*6XA0d@p94fh`M={07fh?@D{FVPCIeJZPI$W4`Q2Zruv$W+2`o<;els$aZ}q;4}L^Fe?3K9euJpvzn3%}w=HoKzl(XMhUwp# -o(_ZGmy3CMl-<#@5Jo!hk+aHktz --ZRrqy*k?kY$Rnlj2;H6~v38sD5M?~SRt$M5+hn2yOoYoj?$&jEmuVvNH!L)$h}5HJyHRiRcz#2s&40k2ROj4 -9yxp(Z|)0oy?{NyV3!W!PqRWGf&QYPgo$C1m!bvUHW8kV}%Jre-}~{nwb|bAqwn8}rq(=HO+qw_{B??fO8NpRR+k87khR{T2CI-)8~#!|77aC%ZK-Yb|1p$OgL -i&`pa0exL7uRKy2(VT;3X)T!isXM#>+ceg=ho(2$+K+Ma2!7%QQb+$l!smW*lEVMFLj{KQy>(}GvR5D -AX{K5YfAQuooKGqA6bX%nw1(=+GLY3s%qS#}%!64*C`0X9O1;C#4AYcvyz)UL1w+B-p*%JA?KqgUzYmImt?@wh)c-+TWl!1V(08Dwts -(N~1)3_hiN3OQ)Q0wNd`dCn*{Kk#Q)zYEKIu16BRAAz*~fZ=E8l ->_ll1A~D+$FT@0~F%4wht0Y!25yC_;9`;tfiuzUZ>5Z^!MU*;2%QD5WEGrU3X}&eUKRKzo`z`FOXiif -ZTM4Q94w-yf>TWgM_q}@LG5FF?-tT0A{#nD_Iyy9Nw;e5)5eX55x4T|RnFsq6Ug#gWzV^#8QS|;o@9)TWM6%`Z*`w@(q229@)P8 -?h-W~CCF)KAuDe$w$z1%qpM^v?G+c6GNW1r9N5RoGPn#cNry3lMN -xw8!9!!+YLfmuyNg$YhL&4dCaMQO=u=#eh!r;zCP8OOB*1u(6DW3WK! -zj`USugUf1aNu2C_V@V)&;tf=M$^#Cngm0?#_R4sa23+<>ixtEib!iuuEkBbz#0zf_XL(|L!>9=W0Hq -E6?g_33cR<(+L|j{Q0vui88xtgOp{4-xbOxrB!B41Q0223j@MUnw&eSWlYALQ6Hs%%y2a)>x3a> -=z5om^vq>K6-$ajr7k*Ck^eAa%0gtULQi*sx>~$H9UXQK3tGx#AA|KwO)}KL2>TNb03s+%JG%|}Q!da -BbAZib_s(8e8b#mx>j~Y!vB<8Ud=xJ)y9JJ!Q4M53bk6A@`uj1X@j9PcECqvJq>~fO@^(yEsG)FSF$m -QoqT76DNH}1@`<5=|J)u%lp -&_NLM)?!-!K{|9 -;Z&nQdNo+499HQ$Y^ugL|JA3ipPHLDVz{w900vxWr%44-)Z^opZeK26cjBWnIaXoS8=^crRlTK-k!9#^+BteZsUj$5 -k8ObsvjU>MY1AAZIbC-OI-KnZ~{eu!xf3S65JXP5t#kbqsJrtMh>f7YWq#hsI7V8{JHS2Z6SO4%KsAa -8T`?M71zoGtlzB^XcvAs%u6vf0-MNv)8V<_e`7LFaEa6d7cbHj_}X2tgek4gtVFY1#1V;3U!4Xnq3Cl -eXXCh6|qxz$JKOn76;A`-QUUo9(9E1g0dL?yjtbkhN0DL2XuhePQDt{wO_COWqX1&-ArkRym|B9M2)r -Y_1O74qV>6HI67Y$5}a0_SUpZ+dwlGyk8KNtX8#ie<$@Al#PD*UPin^&jqG{W~h;0-@~72Y%v2>H -rTFp8!UgSfRs%4vYbU+w_Rw(KPyC$0il>tByh7>4V#R0^^5lzM-c93C@0mZ#wOFj22U5D! -xurm?xWS5DSpS(#R68yFt2cAwZUTXbgA7YRZfmsRb#8diw308gIX1Ke$)y^xaQf20;wHfxeq!&hy#Va -qoXxwVUKZ7#_lleHeC)UvR>_OrB-#I_TP<5PHV(=6o&bwR=A(;K*B1PUQPAv5KL!+5_Op?6fv4z=Cff -E@91|;M((&0LSbtgcWfoVHw -vpjqm)PqSOQSHfJPE;S-H8&>v|L*bFH^(mg})VaajggH5RQ!h2f)kq@ktX8*A;!bS%4Ax0Hl@ZE|>Cn -~z2znMOOCustNNJuTppCCLZ157O|{?W08^1h -%lsn|4RVFrCt|UGAedO9EL5XQ#frDEWH6S5!cFZ89;iG6m4~HwbQz|Uj~r)pNDDR&gU9o?3}Y8CJ$A* -0rXZC@utUyikZE-zZvo{*TX6(|6C(NCN_0mEB7==MG++4pPG7}mPp&Lc>t^81O|H38c*Zw4y$|b;OLP -MWrq7_<-t;d(#$k6PR$Tl2^w^Aw6=g6bc$I=L{Gb!p1B9OxJt>sZgx7%}eOhbgQ^{`yp$ethu -tD{~G+wLO$LcDLCsX(*b5cQo=<@W4(|XVcW(m6NgkRl-fZWwSyk*Qbz$pQ?%h*-f^0XC^9A4ze00Fhg -U8e%9E<8C`jpna5OXfyYF|_I*1PGOfE3pm)5?B$`JVd>fUjY1{C$rg9Z(fdAVMcszv;yRNt8yY|&KX9 -qe~H%<3EnAD-y*+)HS|QKO{k9%r$w!s*@qw3rXiH+}%fBo`$$z*H;A}8d`Uzbw(z -cWg@z#=44ebCL!LV4Se^I8l`eq`$RFLNh+Gt_WToO=DmWzN1@sgF}Ld1#@bX%<#c&-x}ZcxZ~)!Th*v%~;%7nrjGz8tzM9ybjRDtONQj@{ecVOO -*p%V$YY93iQ*ADhb1J>43mxuH27$sEs~5`boP-{ZzO@|j;3bjCo-gPoaVjNYuB{v)t`06o}KAmBi*F&i%sxh09;Y&xBXbu`A`sz&|FbTzt2DXd*gm`lkbFL -gD#T&(P$#1T+>BqfPHnq{LjkYxa2VM0a(?b`u0*dw3A3n2SQyD5gWN+dP!*5F`xtVp_5p{B-S9whSq>wufgE=6Tzo2_(URz*r^7wZ&*IYW9FlEz2xM9driF4Vg(-ZBcY^Y$rZF_ -F!&vS;JjoR;5tK#5ZQ^h;7RI*>w>@u5wfHK)6T>Fi&O-Suvrhp$aJU@&e(+W7+xv25mCV}MoR1~wkAh -fsjFDu^%6&ped+^#4%RO8wm>sz>1xNA2&zo*UZQYD8P)5BdNIC?YvHy^o(AO(H9AJQsWyi%*qGY!{AC -bvzGgNiV?pPXY9-ZcozP-q7qS?B!zonS(z8AN!P#j58H>1SNnWI?zQnkt#+uk+ -{OjD9&1Ci;@gnTS{<}H*Yp^m_@J0Y$DiZnT3(PMN7P~uW(DeTr_bg-$Hi;^tnq|J5!(FSf8oCGRMjq^ -?zI@`!OjCRTT$Z=7~wcJG&%00ab2g`8iM8VPlWi0%#D+X4>&0zh-SPUg_Bs6j@OlKKK=pHw>gdV8_v|EuY^fW1Bj7DhQr4L6mI2f`dDRylrPn1NjL9VCFp?`#R4EhwReHjf-8X -`-K_$^}7Hlu~n1(WFQ;qvfwnIrC%vD+T!lf+J$BWZolH2cbd)A0(;gtbT=InCmycKPk|hc+c3V-dXa+ -xKI}BFa%eR}?O9kKHu2TU+`&ehq)F1gi-(Op$ZWiPe@-m)^RMUzob&%6j^8m9Z1Ar?127>Fbq7x*oVL -r(U1pWjjF9!lodd>8aF9-^yF5?-> -?=QnVlj+Ox1?aj|@+5tOy1)q=Bjfwe~voSUwy|?iv<|B}^2k?buz~#eEyL{M*Tlr(d-Wnfv>j8hV2iho7HzIOjEJxnwABkhw+P9zzssD>AVZUE<0`#?m)2~`bKFtwnf25qW?X`b$2c4xr?lQKlhoD%b>#c$9ESQZHGN -T}!ZUBqJi|p&rS0e(Z>Rk&_!AscNel(htcezzT1jM4szJ>&WBhiV;kB#Om6fJwFw~HXTXx&wSgZP -Bwh639?~1d^@lOdCOTzgX`So2nr`R%gx0W}YYiXsWc#3%rStHL3-+x{hbv=mg(&sQQS#%)96z(17B>d -+NNac-wEY_NbS=hqdh={1B^|#ROLs!+{j?t?>Ia$w3B#t)1)RgRV5|d0i45Ao+Jv5vYrp#H2!Y@Lom5 -7{i%l6JGrwKC@7IP=UYyl+j!g7JX8be^e!(T?FO26Z%v7F77X#*~E3z!Xv8=#339nC49FwlXN54s<@s -Tn~DEPH0n5+$aA7HQ7uQPV1#lOhY&;7lGjrSFO#n?BjTl=3nYF(vHsGIG9P#`HPm!HLESAJQZ|1&|TV;k^xg`n7^;uA0SB?rm!{I!r6k46BA{EVWX$LI_mjb>E -(Ps;OO=o5!$1A0k$ez;HEGZ=SodH&D)#G&nmen)x!FZ#scoof1Z<@x_f>W#xQT77VN{<{zdG!r`^dN+ -qCjlF=sq@a+Dwh)a(Ps52~ehb9NA)1%S-X8ACaV3Ad-fwvs`ICP08GV@}y$uOe_TtaT27AzQ(q_q`x4 -q2cT%KCy5fnFmN~Fv)I+*IN16KaUIu;ezrE4PtL`1mnUxVH@5wgR}8goS*`ujCp%mZHO9wv2?<1>Le@ -$4?@#5>h!9)v_ioIs4P<^%#JAbTOaWB;dP>9+@ApqD(DV&vgD -Yl1v1?IjPT^hulG)Hr!C!)yq_YN#}$niGe*m+(Yp?D=&BZr2D+)(OYHi#%}ZdFiE46aK>~!%x=XT@$> -zA7XV8fq0<*@{NH2)V-jHo$|A&q&|i_=u^NNQj^IPksR8vS8h4SUOlBrhzPD_~` -zHlr`q66xnDJ*SRrer{=$#7IomI86dTwqEi_TEbre7%6gniGW@0ne~)=m8>bk8j*1|6Crp!A0TTgp?N -qE~p46Zff#Vd#qg;p9*fxJ?q6@nx8Obp(N*%Kq^<|#9N+)*t){1Sx?8;81pByeUmF0~N(v5KGICYhs2 -!b8EBYJs58XFdj4O36Y -J5nG4<7f_mZ-0Q?;;9`WmOV1aK!<_P6suo#xgclq|Mu|#qEXxxo`e3dqa5@ZdMSn{UEd%izGVv3yU{u -l^{0m8?MD#avUeOoFrD?OSicifKaN8U%ZSTXMWTKaO#Kvi}J0y1Dn}dWEw?^Z$#jdx -7C-Sx9UlZ&s}ME9V}74Rb1k65rhIOuU^yhAL0aX~$=-y!#w+!QSvxle>jJPDBis;do{tA_WabG6Oewj -w!(zNmK(B$71wZE8vS4C-~wi3u)V@qf6Z3=kc0^)5JRs!74$HPNz`n!uTftjxU4JTF0oRwH2Mo`FEZ- --SmgUzg>X)e=dv*7{(g -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 -BT6-1qW*@ -wb`W6mp|N$WX|wk=%}vTLb9gB}BzHEsESZWukznn4^EJd|u#9SjaT=P>LV%eO&zhVe+UKD(1w!^07~m -o;azOK5iP#Y9#`RHFwYKtzYr)nK@PCJTi9f3{USvX&ca7B(Wh3S -Hwfd~USFPucWQ;nTG2l%X+C+8>;!UeO>zZt>`#cKfu`nk}b3d573u9R2S@%wBjajGeq2EjV_Xqxak^g -?re=qRg^Zd7EPuh%Gey6sv;0r!p!!$KiR+9((r(?v*rYA_^#X7e4jHmcclc7qeQHuPgkhZyErfR!1hR -XZ6_K=uY3+3cC`UW|M->#D`@7%ycY~~>_)C6DJMo1K!^xrQ0bA0}va{qeG|2M+l=<|Pr`)}3!m)$@W> -KHs;*(N4M{So}##XLC1OD)_~!c!f*8G*cXG49>lgfh|KJH>?(zuYGFdKPA-e&o8DN7=;VMlRXL^Q*>G -+Jq9iU{$d#-M{2 -Xx`5g?H$>&}%WkY{i~bq}vXH1>i?`QwAFB#!nY5kq5^XLm-$dDY#^mT=reHwLFy2X)QB2u(yK(wNoc9 -g>xBINh*4sAqwxI*^p>kstM#u3#GSPI3t*_{Gt;uBu{wXdWoT3##W&yJV_K%l2+0Dh6m-YI3VmzeLr6 -GGoR0fK`zd=L6kWd4fsbq_>|Zcq3qS@5p!#OKZmYF=CAHU -U)>C_Z1#3~g=jNPIs?GD2#nVzf3v-=S*O)zTW_apM>Z@rfDO|S%UG~#V!pE2yChS)0Q}@)&)vXBd95Z -|DWu9}GkGn&x9-Y~f6LjtY>am4nW*prPOeYX3PUwZK;%+r2Hj@tIDDvpaS(h&oGEi|bmC -%|Tx{=`dvKSBATTgsA6YnwI+Fo2d8UThALZNphS~nyVIRxZ6bx{~7NDvQvUp>ldqIcw}u+6LIoSI -93pXgl~Y^^@5d9+G5Iwh^{Uer=-Jvn9TkiLLT%GvcL@Rz!HxXH$N`AZQ9trJGC~(JnqG#REwv{M)FM;s?NGQp}BY* -LFaCEfcH%jTk8#QWOCh>ogA_zS)&l0?P+l&^ZceuuA -m82qXS|Gb2N>ujFA{L7LpuvU5QMcU(gU28L*H~5zqS!1k|gdXE8+KWW1B&~gi)R>)gt9i?16=7|e*uA -weD)+R~f27=VeC3`duJ2KHl+kCYExbHYj+RU(={o|v%*Ygc$KZfOg)*hMVRSKqOwt#o5@@$%q@JZ5@yd}j-o0#!pxP{%@k% -*ai>^?nH1tFiYsZt+( -_nRVOGdIM3|e&+)J2S$Q&iiw3V*}3A3KeEiS=lGM^UaSTZ*XGgrcSRG5d5xn7t_Q?~MwFsG1tn=q3KZ -{<;8wvu_XFq0~8WrHx!C3Cqj=aAVY%!|pqSeQwzu`*wn%gCH9%;jV@3-dZMrwH>#G7lBz&15zT^Fw6r -Cd^yOtQY2OWWHD`%7VcHxSSC#i{Ua`xV$G^9B|1JE(e558C)g`m+ekp2L#8_!toKOuP1`z5aC$k^mR#aj1!J!&Q`w5bK ->BBB+b$Fd}eN5r&A2~wa(1%zt?|ee!zxR!g`jz4r>IgkL3#C3{ChuuZvdRzE@ixyn^Zft3R^;WlcDo< -$0C1RN~g_Ca&)mdfjgKY^z!alyj`->S!76{&ZvOayzr8TWGQ7ndRN?a;wMPmn6Cj!aqXpxP4^>>bP1$yb*x{T-PE$)>MR;XZ4s+-OXWocNW -)r1!I(ZJdCkqjCBvE4LDlhyiWT#+J4ggTKqU|%kK-?QSw&sYi_mI6A^7S)LRV)Fkum?cUV3|v|AKXYa -vix`-c{n;UsmfVnr}VYp6vHPH -B(;}Y4rrG(_7U5!}E1sz2W5p$}`*erww;dXrP}c_)p4$r>&Q=;A!hg7QE`;E(?CWkOfaRt@jfJ@62x; -euX5#yO1P!wD5Uqg7U2p13&yT#K7B?15cv;wl$|4#Qh;l?~r>;RWxAwnhf|uLI(T~clp-QNq1=yHJiCa%|<^N@b^MU{^$+$Y_oP;zPJV9tm_UrapGRd3}GA=WCK&O5?&;7LZUqY -n!F=B&NJvFWb#KOfNF7FKRbuZ`E1Dx)(bMr5KIl<}T}!VR(7(M8oqN^tD*)ULmwzYYnWQWO%+Zv=P3h -Iwgn-A;*Ni%1}9pzS()|Ewp;h6B6i^8>B>fvV`AA_YWo9B4K4G@i{Y|NC5>EE%}DKOX-tmWpRL%R!`B -S)oHJhOREpkq}6*1X?2&2ORB%E6O!uPxukleCaF$BW=#n}Byk>+K}SJpw?`8;A7t1?B73L8OS}AbKk5 -^$he*u4>1?17o158mI*=dU%ibWW#gNZcwdpOA3O@G=m-7xr{4?Php1!8kOP>8Zg?xD(rGyYsddf?v5H -HsfAZh4Ofv)KUy8=^dOxIwGVf$39x@PfvF|0Pv9wlXvCVbul_~vs_V1sG>Y!TO -@?yi5GDFoKAlfmQXv$XW`L-Yp5&@V4G+Ps1vmQS0HHwqTy_ttIz#FF4n^U6cyk;FjeVy3zBhnpisyKDdl={8NF!g(MsQE&sdsoLY-F@4Y=?>6Dx{vaaqNZw>kmxpZiEi4`w^VrzmECoGr -5{?Jnj|FtNmm>^_XQ?3(UlnPC{y{xMecM>RMb0Gl18Z3s3hCHYEY|0_b=&UIw8@G(j;kaub!e(I5a)0 -C(^c(K1+gmhLu1ON1rk(p9xX!hkT;k*?ywjeO -h>p)w^Qx8+3WpB<0C-Lgtw>W2S3SmAWd=>iNcO*xuP%^Q&6^3C6ZnD8W|`Su6GhDChgh*5FR`%-V{%V~%%70%~|Xp87*VKpT8~(mTZDIcvrYBhB02N39@KJ6PZzfb1%2# -zBuC{UxO85^x@IrUK>SbTp}u6e#^ZOfYdC(3NN?rEW$ul(O~t}sY_h -|!K7e;s;pCo=@r)q4+dc%*$zDYey(&$_#+4Z=I}yYHM` -vPaOv<5pY%)B5kPlGGEe5)A7u-l7t~rE=_xgj%r0*!t#K-mhwQ-+M3A*!LZ2zEf-}y?VX$_qQD!+J*+ -h`I!C^dH)G3aoH$#aP+fw{>E3zz?Xd9RfJHaGo(2jKGG=B8;HnmCi_I5nYf(l`RH1)V$QHTa~xfkl({ -jujD&E)#wSpy`!gOmOwb?=V+a{3aQy&T`%Uuj`x62t!cQNny}6y+;NxJ*4Tda0+Ou|3PrAjDF_(OMHK ->Jvq9nr;VK8jkqsH8yB#^Bnt-J5JLenbG8Y*e~O^qpo7dS9C!}9?xnk<<4SK0#TsOwonRV8|Y_2 -Hvx%g=`yHr3L?3JILOp9{S=nkrUw&t@*VdaQfax~%wIp{aBopZVv*iwMi{X3&CncsQ@h$rd_ZR2OTgyn*+0S+q1I!7XI2$Nl_NdOZzEb9n}ci5AaZHRi72f{FAA(DS`1;fFv}9J=Ldu3VbuNJ+o{g*niJseW7r;O8Fj -Ltq9u&PnhkC@dGB$UP;bi)B2$J40Z3gFn9R6tAvg%eh*>Y -x=v(Vb6)MrnDY&5zxgyftTJ+);rq-9wbVyuTWcZ{f6Gg{juZiz>1J_WAXGe7OpYi#+Rac=EQuIl14(> -CAhISjTk|jtw5IMeY>1$_f@kjwoSU3D!&(~Ao4x@*s0<8M$Fxk*ZaELwQMN{sUIG%E3!nma8w#qgGNH -g|Y9`JL)&&$)cjDGH=PywX_jH^?+vcCnA!$mX9NI2*oWpB$SLQI4au}_X`Eu~D;*s*3oMY6H)*K~Xo} -&oVHl23#kzRSe(7NWrR;26P=ij2KH9HdO`Spy{j%OhSPaRkN{M&@xqmGb5uzHPR(YOl}hzl)zHAn6Lt -YC&{ig|BXYc@qvXp3h*7&P=Zf}7ym$wt+>26fxPyk+pjv--5DElh3-x0$E0*OoFZ19I8ejN*G8k{;xkhKKecv$cpb3G}xhvPjja4GQS* -wWxjzwR(>Uuie5wA|wmtAG`7)Zyy&guycH{kv;duj=q>Tz47@# -igH%!Pdm_5~Wa>`V3UbiaAl={_*1X6Q=3{<^m_*E@w6bcm^d)ncX*9M4)n5uWN=B?p6=u(@VgUl(5bS -|TZI}j0u$}#-hzV8mQCR#%)PcGGNGPr_Ln$-vJo$9?}<|t?|)zBI6{qo>bBPYXS4N_xF1B8%ZC|%F+I -KQLmKvOve(aY|TRRU&eRzw7?9N6Y(j+_J~qr;`@>28$TW2 -=qS>tqp*r`m6s#W@vAlMyYJs}DxwK!1XCGIS2$wT%QUQk -nEjX`+Z?eW$sA+~8kB&~$oy+v}YUalzzhWYlBrH^#HFfpJ3>Bm&?QFVUXQ=u!apw*&W1Pq`o#olc9r{ -^orfAH$-Y66Ym}?g6_#sknpqeh3w9q9qX693PH73v~P@_e1`}ciOA*_@m3N&FKysXuuvTB0=w1KfH2_ -SUGA3VcfP#*CNx1RH2T7_Z^%0NliDHE~+8$J8$8iUP&ja;(SOL`OL5-w!?7Q}Dr%T>vLYN(=nB-H&)U -UWm^&VtN(Wg`jC^TUv&{luC__uD2TRz`JDLq+gFn2eZzHjtPFXG~pb&wXh$kxAN@78+KO6M<$?JC|ed -zFqfOFdbX8<{grPR8oQ}!xL1y4>J}WM&)@wP&paQm&~5eDDcj3Vvwnn_pNOP7e1T`)`aG0ROWM=<)bTx#d35WA=X0~?W4b1=vz1O^*DFyQ`8FA?zB -rF^s%qsGf`^oE?*?n%a~V;Ry=X|{M;-Th-oheY9%z2G4=P&sgkxNSv&MSKV6EdqC};=fx*ySKCcnqi{ -a^n3Xru_>jORj@=PS<#;!cjjlGgptVyr~yqU9`4KN|_Cl1i8t#`DiP2bD)NX_BCDnGQP9nw8opw1yXq -T^wNbj4msoh1w4$!>0Y#)X`4Z+$E$H#}wM&Q?T>1(W^%2zi -2g@j8q#90%*r0)eIq((UUpz8@rq_E1UGuG-vWV*~+J^Fkx4e@8S&-uac!j4`pgK(IYDNj?Q3620&O2| -`GpO-IJ#%$w?EFFrySmOaNZr>H!N&agl3$W2$CLdAK18S^#<<6Hl(vo5!dx08{ImKv)AjECtpC^@E5$ -Glv=s^)^3Ryj0kO0XBtEZDK;k4CrDdX@DbODmXmUo7NT3#mB+ej~50~NqKf5q^20EzdYS?0vC$%_q@4 --@|4v!1&kX|G=dh^kzme9ig5{A{lb04AK#Y%P^iJNAPUBV2NYk2{j!xZB?raYcS{hjJ6$QoPp -9dq?YkNzD{>D2xF%uh89|LQ*O=~mF`Xi7^+LrsIB;$F`5NY@hDHsJaGj`EFM+~IYCl6kF^aG|lR>iqA -Rt$Mh3zJ-?8^!2eyT^?}mqngP9srwBZT$f0awYD{ttkhaV1$}Q2^@jAAhy)CkR@#ToJCs!&Uk~=x>wy -*wZz>`!^_XG1+{C=$VBX7oaaR-3$@8|7`J$+Gpl}xXcZR=>y0b&pnr@wxc{fWRXs?-28v)!zxzS^_TW1^o -kq$+`Ppf#(a&RDljV6$jk!9ON!c-X$=Kj -`5KgH4I^*DtGQnE$edE%QFh;=UE1eUljggNM0xBM-o>gho4Rv3XdCR+f!?>24VuRt!Xue`kW! -!*+vbTh)gV_S%jD`!q8yVOt>?bUNUbe3C>FoOz2h?se>+CYE4}j6e%W=sNfq)P{yl{2EV_#BD~h%ze1 -zH-LK)tpjMzZF``Wojr30N|lGC4$Qb}LZWS!?LrSO#Kf@$@f4YvTgjZ~R&ie{5=8gE6Xo^XujNO#roU -Fc`8qFVrHlcQ!1T64X{vtNyQhwGQ5j@&&$xuwu|8RCLM(zDT_N-Uw>_61s;Akh!`o+lQ3doofnT)$;8 -*!IIY{Zp~FkFahQl&p&LzNZW`vKIG@P9=; -|5ynbZSfvDbZt)+gLAOFgb$GLRIFQa -mw)U;uL&2?jB@bz*szPG63B>}wOt^I6Io*jb9so7BdAEq4#_#=@r4ul7c1{t@J_ODVThz2^?$Cs@1&) -4AhuwF6l#W5GlO6{udWZK%n1SC%en{r&|bze>v+<_Ug&8Tg~~-;dH4kU&0ti4!{xIb4O&*J8WDEw&q| -DFpP32G`c}c7DJWSP`m9)x1k%6#b&sW5AmV_$({m>Y0_s?>m2HlGu4L(S_%RD*8lX*ltPLsWN`I&eBX -8A-j;!S0wK`sGWVb4*kK?1zLxzJOLx!QSNDbOSx|x&*6KXz?4K6j$f^~5u;{7e-IQ{K%ab9wiqhyZT1 -(iI#<_q;(H`aoqiiynBC%ygzT3=cAE@@xooW;t4;bb8J4$6Z@UL& -KZD;Uy?d>@htizyf19+}v7@7ZXLr+m(ns=r_enR7QqS&|_et-LGF0+vM%%L|_!kU|J*#pZPo6H7ewmI>7`E^Ab=>}ImCJR231#lCEXDX3CQ<^gRf -jQ4`D~(ZIyBr`Gh4@%YSt?=XL2S5TRr=|u~+EA?7vVbq`mTfb0lUJjex7b7W!w9?I(xQyy5`u1gDwB_ -nn>5G_Mc-dLeJAR!ui<gGM`gx=n -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 -yLYyt2a~CEv3+4K9E%Ez?bjIx7L|60 -XW8OHm%Y@Hn`bxX7P#zADZl13X5}j;iAY -8W7uu(5|`bGvTuW?J)*~fx0l!b1$?jH$*yPfS?e>?FBySTtlnX-9Jng>9%$c&pzNAEMZSZdc=g&qVEuW7ukp{lf`K5(R97Xm8MY7+VpI=z&n!jvJ@^DA~LiRHr -!X4dJ&${MEu&&ABtm||`Lu75Z<7A51yPnvGF+1MoMiKrNm{QT$USX_ -PzJc{h?#y~QW1FvP?AFkwHj3k<^tz(I?bn~Nu#Y8<9$~D9E0Xm94LxRtvmVK?J7a2l42vAycdvoPxjM -7%`H`S6f*D4IIXg85H3VRAjm^UtNb$phSQz5K$Ds??v2f&E*DZ{7OE$1>Bcqx-H%2ss)#@D~Ii0ejDX -qOo!v&ZhinR2s$Ml#6jkfXxQq+KcP0|+4^A*DLrG@zcLrO>@V^)p+*f16g+G9a`Ea-@xF7m4j(y<`W6 -5Xv6>z1zvJt0Jo56@plGWOEQRy;pO8Fsq4l%VSc2A``Z}wL)S94@?!yY5dkbh7tM6W2xiQWX2zLVccN^K&R)$u8)F-;s_o{`^@J`-h -*a_Tj`wcMi(s29kO0r@dZZ>8t!35<2YeBypdPmkfDtatui);oC*>#cj4b*_zYgv)Yn43>Ee31A_*`+0 -od8>442F1-06EC%_H85vz0eWjbPj8i8uwjA&C8Xu^fCbs7PJp6u`B+D8wx_cPvAL?LoBuqMWVmlklYcL|Y(S%1w-In%9a -eTJ}Sl|BMBU72>Ut{<*y0|d(DN)EP`WDpVxO;gn`E;GP*)D+H^(gaD)_o=*^3i={SBK -VL>UxE;UXR2ycWsPrFw{mmB67l$!Vo)}@S-1xLq8ffvU{^A4TCTL4J9a7VA-nC)GM6za>b&}bVr-Hiu -KZUVImH2QT+uT)LU8G!g4e`hh11Fxm)q{ND60tlOJb&bz9iwy`3nRh9Ir3pnuJm{p)nlFY5}`wa?e0& -#OQmBXK?j`()U^(rE35{tIoO+ssJxV-c*|z3AVlUC#_dyAI{;TJ+<tJ+en*_lB5So<8zp0B)W>uXB1{=L9XF0sU@|nIWL -16Sa?8(O2UIu2_VP9r@#Z)K#0%jyARSQM&jr7N6XM#edziIkxethHkZ89ML&Z*}UFQ*Xq3(i!>ZhZ9( -6FH$vu9t4A8_(FbYX8{Qb&5KK@8J46A3m`rw>Dr-J7Yn|Dud(23Ecgl@I -z%1JM;*MW-8xtw@Z)tl19g67tzVrU6V3)E-xfGfR}k19K4aXEY*)P>zJ7YvWu^grKxfv)$w!tP -9}lSA%6Uc{<{5FQ-{=!WnL07I;q?+P^4kshr7>L8{gBo<5AUx~-!SI%$oJK^Mva~?4l!nE!2078|Gv9 -3!eZWo^f2FP^ly)mhZ#%8>yY7}rMO2J+o;9wpiO;(FoWOpYg1I;Q714bY3|Y()gW>nmVH^o2t}N}5v;E(n)S_(LS605` -nJwZvwK_{$p*PDvO)RZvq8xh*dX1vtkqZG*cc+m2aKUR`m}C4Tj?xsGh;dZTHV&cZR^*p!{IG5v3(q~+v&toT}w?LJSVr%HL1ioqp)~UkzHb1f{?{_Ct|0U6qlA1+0$LlA`6!Gm^EF+Or@ -oTi;5+NnI)|RuzcMlPL|x+M4F#cnv}itl6<@G1boi`xY|nc-9`3Eg+=xoa+grpCjzI@&S1NFD-5)`i( -G{hm%HpUOXd{j+tU~4I@zye9)01k61tUe&v4~i!9UuY)Cb6YY`#H?(hHFL;imk2C#UB6z^szoe91RpN -F@&YWn5r#veV2gw-D_T`E-_ER8p$hrdX!mV9C;Uem#uJYr1_&$x@!l84i2i9A?qYa5?dxf&5;_L!hV^ -@?vH)tXXqR*%>@~pH>bD?C^h$PIM3(OA3o!py!9Uy$#2oP2pKFnN8^!i?62`Q!O?!N1&`gWnn&VqiDA*F)%CLhGcjhLeBOCP6u6_0JehY7j;swi(Me -B)#qNpYSa3`zfvu*@ -vXyH!%U|H>W^%r56`VR@;`zJxx+m`ZtGU}CjFGm7kX2fJJLGvqOcy)ViDwFqOHXj!7K056~Rvx9tu*h -KvB>yK_m$+}Ay6RL^sf&vs8olr#MbmThiK91WdmLF$k=Q;~%qjU~C&QqUm7nLJdgv*~=AWCOiK1`Y`PDG&JB=^I-MKPOBgC)nDh(x^SesMC1{X%qof<1v+p8ba_=YIM@zd -&+S8>yTgI6#-DgNRN8-Oxrd=f6moPfH#`qa!I%DsYw04r=%O%ZgBwQ=~E2Mv=#6$Y?=tRPU(*Cfd=TV -v76ViRVr0)&s|Bm!0y~PoJJoD#!+j97^=U*NEAC3R-#{ZAT|Nr$NRc8M7?1zP%#=i>wd!MKdAK`E8@c -Fls_TTb!e7I2fcYOHmf0pil<@5i09cc6XAFmS4*UI^RKoB3^9>oMAXYKN}{iOfn9|`#IV*;-EeIpk+O -IuaW;Z}sN`@``+7(2kf=8}ME5%MoFk=+PYcNsfzLUn1 -uBb?vPN!zg5~dOSn!#hlDv2S|v=9FkZr566z&vyd=|;@V{%`6+iy+eS`mP{y)Ww#)%!o4-)Ph^|HLfW -WA8-HBXT3w$qQN|9|EADS4EDHcy~@d0u_?iw{7o%2>pkx-mAikB*rEhs9v -LfO`_){diMg-a>A89SB44i|#-Tb2H$@9_X(ThG1E5^j&Z#xVE)VET1kL2@iqfz4qzGH7RoQ+8oU=_-UxUPUUn6D1sH2V`NB-__ZGq19e -@u`f*;%;0z5fM);++G$*9|K*8>imENGYuxIP1M;r=M#I~jtWlYpU_vfKgd@NPnwy@1v!0w=+uDIz?<- -)Et(qI3cK*@SyM;38>W4CuT8c|jN#pyvjGe*@s&8w5^*7p0lt$f?3y3#iLRKhPb#0<4+NSQgA10pFZ1 -@EiraW`-~i1>847#H|OMIaBb5pm{dJATGg6csJ7xv~w`7;+>B$1cR{zdI|1&z)vx?KL)b`7&TwuPrI4 -1oxc$E_9eis3uIpbSdCYYwy*&(HwW`InDcW;K3=quEWk|*8QXw*Lh$K@qMkep*ga3+p))I^^F&%HfWN -}K8EG{D_Q}Uw5qOM%f3c&T!dwgZrvikB`ANV_i)8<~n6UvRjJe=W_mNI;AYGW#01F%f|6;%|9U^=);O -0i|IO~^!Z*u`Z_@yY*M!+rWL|GGDP>ptqFgbv~sTOH%27I?#;CvtO -TWKb^*CX%{9KS)}Ndt7$h_*~{!rh|ZO9LFUk;WC23*gXuM4wFXBfJsFy8>8nFWMl?ivhjTd>Zh!`-Is -Ac=0~LD}s;Hy=*Yk{cPD=kT%Q&`#*qs33EK)><2_WsRZ2nfS|t-u-n6!%Oeay+rz@$0T}xT#znXjG(L -(rFcW<2QNfd~fXf~e<}$z?kI8-vaP3y)7vU=bd;Ssi8Rgpx@b*6eFU%_dFW^ms`6A%we+Atzp9Y-!H! -*e)T)0iR=L5d7O{CQbxbZ2B9dLgLaM9B!cbLloM?V7^U`_#C^^6>60S`YT>ex}h!e<5FiU3o0p+3QV9 -N^(yqU??WZm-1{3HKd9_{+jv(z-}Lkx{pHl|-4Bo9UKMVMFqrgdWBqRs25q%@U;nGZy-ICK!1_nC -T4tFli>3F3ofn-YLxlYowXrR%s?!C(Q(#q?yi2(-~hf6Qr}AL?=OqbSLPMW;)CHXK5z5SDOFx{Ns<1X -%6Jy2QfV@@67nOwQ~AKxK4ad^9vWk#GVQ(r*+p3K#GTERZjj2AQv}5w?T^*3|Hm!?J!B#7LSfgK3dyx -7kfxoy3pF1X+=2Vn4%~Ni;9lQ>`>_t(m3G`|pQ{D#`g<`(fq5sTFnRMP%vW`-_Sj#eo#<@hgWn>TM}PdxDi+qrWmtF5hN@4WL4yL9Oilb^QAd0<$Y$ri!xK3E>X4EvWYJH -)Mr)GsfVM_gRCY{d!+@TGe2;vx0mIe71fT@TBbhzt9&Wn@H4ME6KEm -(4^<c+tG38(^n{oX*lx^gBunhsu_4C -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 -MJw*=jg{-k>PEUEi6#CD|n=7*~#u -(Xw7z2IACN0Ui5Aqw#zoMd|X5V+;ebbO1mG$|W_jla5abub4u;_b|lanK=6TjA57{HnA+CcL=mR=!^jZ1f*5|cB`uz6h6~uu{8zVfOJ9l2Nc=2LW2VreTmnAPRPvYa_<@)Qd -mz!_CSv0mbv_J>C;htcyGZ+gKnX=c$wjNkMOt$?sR{e7z#=t+Xo6mzsgeav}w~mKu`Ph=_8XTPd0o21N3+A-dz$B63pxH01S45!REc>V#L?TJ#=zJhQQ{QE -n+G@ueRjr)~#E2+OT26Tylsjp&!{~B1_bpc@4U#IcH^M8U5|iqld)C#!9<(?F=7ypL2q7gpw_*8}^7C -VGoci>@mEv^$n$oc3Wfn7lwYtUC^mhr*PfFeDrzy_U#4(`$OGO^}}FDOiYx%eft{OTDfwip`X2G&v^{ -p=mGYJ*NIJ$A7X9v03ARlxGpwWX}_S?$#2=RWvueOWY3;G)t*4lr+H)PXI*#vWA~Lwz6d?aThZ8pyNZ -oID8EJJ&By>giaoaH*m@#%ls(1%uV24jd_JG?fquW=oX1}4K2@u2dsKboWTpL8eQl;E(D!I;|Hi~`#6 -Q@7)yG1fqi-Cc1%9KmDyJrvNKH*OwpDfA_&00~@@?f1JwPwv3337tu*vRth{s$P9>EX%x#Hy)MzEXB8 -cBN%{cc%x%dlIvp$%Nf5V|mP=1ftWW&E^zK%1SMgPp+^F_0f}A||whYsHEc)EJAdx#pV7rM-rJ8~^Km -=Yzr8#ZWldZ)}kIZj&boiRZ|S?H{PkEN$Af=|4hy4gGGv=#F9Bu}ytEVHOn?ne)NL=fMWO$QHH}ES7eSPgh@|yk9~;w5 -Wa=`B(p)Z}YGEu-KZX?cc#0UV{&|CkM~4Mf4KrL4gI@b&V^rGxgH%uK(Cvsy?0&-xL4voW2|r+2Z*6> -#rNT%O0Sw$UF3-2P%&%uOjV+es}#-?La_$RXR2ay1r}OJm -F;WnKGFERL}S}A8UynkhTC+8YrJt{)K`H%8MRRQd|jPd?gu}Ky!(Sl%pSA;_w}s*Uuwg>y5?DTKi%GI -V(aHl%zr`WPik`ys@;5r3)3^D-$Jad=a* -qSe7`2_jt^Br21mNkRA{HPHNipcc{tJV{2%AasbUi(==ZgWAl6RT-GIyW>0L+L)_#Z^vec)6X^HRE8B -1~ROzDDI1wZdW_|a)55f05uqXJh<6nOwr8DA99kbA>0S-GZGUQmGZ`%K8pzmw%A8^Yf{UQ2{QF{Eawo -JCdr)d6~sBvz*`#uc#PYet#_@AwdQ%wzwdN4J4yFT+T#{zv%`fWxYs{*|?dOXM@Jt}&g^a%&PWlr5^* -T;`*zMnQ_%9Jtgez%QhITBm@a(kFsBK2JAi0<|2z0yA(5G0SWPLGCOA3Y}I@z(7D`(W26{&Dw8gZuB$ -!l-ci6W;7VJ@(XzV>0cB+B6`K@j>z!5j9O}<5nK2i_>4BH(`2Q&iYeE+-=sn$0d*5-mcAmVz -Sw@XD2ROwk#f9B;O}K2RF2^2OQt`CY!z?{VM9J>zvy53a7t-PS78rSD2U7vt|z%ZW~SS)-C_&)?<%7m -ZtW9jO`DQ7m(YrC)ghL;#Bp=^5!%BOrL=IbcIu2U+K~b`b=}2KG{@<28NMGU3aj~&wj40|298E4~hBR -eHM4`j(uvVXLRsYzv;2itIJm!a-CkIYg}UgSbxr(If>6d|9m{XA>wi9uz9t;ZxA}z3+xf^G3ej=9=oU -$q2$peYa#7FG4|xilhbC-oSDi#%$+;e*gfnnz7PA4EU+KQB5S*QPTwE>K#m_hq&g8Kk126Ay|Vh&$EQ -%|pr*?nlV7PW*3{C2dtsJ0@M7=$esc1w59Gw(4qcST2^ZyYY3@Ll59$B)`q$X?<*Hvo{=8g5^_TEapJoi1<{{?^Df7aIc;~2d}>c+i$_crxcXc#|!yxDi`f$fo48L%~ED-Y@?&Znf -Lj8a+7)EJ2`c9ZirlvA~0uXOL{jUGMvb$UG1=pTOgVSyL)*VHf9^b6Q`D~I+T5qnf$^{ejZ<9e+Xnx7 -x{1irG-&}e0A{9J?Bj{T;Viwsgrr>}v2xcdn30K33>(869|x3QVfxpU{vxt_-LD2)CW6ci*D7Z=CdoE -ttv!>CcC*kB4gSW|K(RB -}KKNWwQGxs&*S(nS3Di%nbnso17FRgYuZHYfo#T9~yqF_RYJK;X+0b`(-QV4eRgAZmcbj{{O?pQGKcpwMf{Np8V__WSV*e3J{TJ85cDz~ka`Qrky;czE+B7Y=*C08I< -iO^hWvC>7am7E;fk$=`*o7>yOI6r9)w)9ls8sy^SUgUn{edL{yns*Yz5K}=nc8fs{L%jIXv*XP*+~d1 -*ftn6=1ada^4?f|8>^-PkpSa7!Tf}?Bdc>NI^f^HfdfsS8ym(x9NAMWoASDue3I -CmxShO#+-XYEJ!XymUx=*HeObD6sfh=p@+<#VyEHd;a5v!{q%dx{h!No?v*!RDdqf)_WT_?c9>Yr- -j9RbCgy@4*i$zxJjR#1<)6=6{@Z(!xE6kr_3=mK30xbytiFG$`+V#)baNf{f?g4r@Y&RpIF~#d-@Rwg -o?7QJa+{aMwb1El>P$~P(|)G8Gq2x({uht=Oo}QVRG(0OKdB -P;;xxSK-G(JmLNgB~FBRM-cC23SrX3d-Kx9`f)Q~8&YeUq}2Qo>UB+bujXvv*qdxUoqY;bB?Hal`r7g -5APLCZ=R2g-1uVujAy4i?^?P^{Do>XpL%L16rR)wYU2j-7l_RT+g1p@4qJ@!Flle^Q-rq|GQ05`$$Q3 -$;pzlB`v%yy|cXYylcv~l$P06Us-I=akPXUsS%Pd`tPh@~ZOc^0Vdquw$i -OBdjQ*sHk*JX+`Ol(p{zdN-ImNN{^IQmxh%^=-IJ)em}j!P`yUFUL{YjGf%Hnq}Qs@tL@V3Rq7Rw=rv -DrRo}I~7QU9gFkc&AgfG$;>+9v~=S%Pn^^NwW`*M7FzD2$w-x^ksVcEM=J8(ZW!J*IZM>1*SZ^@6aWAK2mt$eR#UY87O)E`006(I000~S003}la4%nWWo~3|axY|Qb98KJVlQ`SWo2w -GaCz;0Yj@i?lIVB;3U+&PNhLC4yVH;L8E4l?oNnLE!^cj#XU6d`lmyv~C6b4vtSGzt-)}tt5FkNHcDm -=zJ$HDzV~GL^K%r2m7Yf_n&chvVoK2GS>cCr6)19C2&DQqTw)Z^CS4DDlU3tOtfw#ZA_ubC^?*6X#Ym -&w*?>zpL7&yU}nABWZF%8S#9_x$afcV{Qh-oJZ$c8)Kh+}5Nm)TRk3Ww^juR -luj*W`I@+1F#k&RuzXvbMZ*w3HA -iW{dAz-VDa4Lr0^YdTNL<|&4AAc*5h~Q&9z822L$N0-6%8LvDR;!_RC8~&CMu?dq%xXM|#Aj@|9MS|% -lEMiJYv!}Knr6jZ^;^!P>iQe_DV`-?#N=20Vd%Y2%SDbq>UYOQkri88NO-W&4iyE40RI)e3!*5E=dff -3TU*psDD);v1$JWF8$*u)n76zZsh0{d5ffNKQzZAes)_(eW$5|&Dqm?TOVVlP4-T+)Uc^`P_`pjuKrH --D13h_t8l9dUzdZR1wB+Zxza?e>5g>(Xz#&i$U}%|C*Ma{jzKu7B_i5#T=N-Z?<5}Ww3MHXlmEyqh+Z -cY^`t|rMI)DF?U+MJ{(tOj$=r%3_-#|;6O|r@#Ao9|z@@fDFUT3GQR{RI$gVOgN(8x?=UmWaTZk_%W3 -KK$CzW2zexdpxFpNwHx1-6dPp8o>Bn|w&1c^_MEdt;RgfKM5f2oTw)pU& -HbQQfge)j%n-+-y%hH;*YbQ1Uz80$vhemi;hOLY3z*GI2jy?x#fbG|7|=;He~C(qx$IF8Ow{&wsSknW -%KfnFG(3jtea>iK^7DNE9T3J#1JvG6LfJu}~KZ7D_@ogG7kLeRbf{52?i-`@)U{q)c2{OQHtcP<|dws -!FAZTj@XoW;sU>LAuRDBTwc%qv;~bL>kUtSe -)IP1`1#TKvG?Ej9cbX$+jH0!wy4wa_59@Ro8PbVo3woX%ki_LH$S%;9TonE^5OP37k~dyd`K@J!bGW} -FJNCgdF86{_kijI-(LLv+sjAa{`ZITM}xtK@=*{z9Q?_EaQfGGzr1||)kpsJH@vp|#*e>Qzb<{^`dJy -4A}4P8*k`(`vWR3C+*;!KXcBwbftMLe(p^BWZ3a0+fPjsy@hmP&?==XBu+;|H=#v;%17mFi!%M0t3Q9 -4X8jA+MhOjN7><3#Fv<}Vv*ZkbrR4&1)c2czA(60+G=C&E0-16@tm0uJuo482<~Neu`#BY^3=!QXX14_aafu5bPG#m!~i?P-!uq -Iou1%v9f}i`k6vhut|u3Bni#G^Pslv{{I9F{|W2YZPcwYL&xQn*57@th+kP;*PHN^bdAeuF~1Vy2OR5 -dD+utvjN=x`w?Hn3CPhWB<7I-2b?;MM>ByM72>|0Rhny-3-q|$T?WDl2^o_WluE`yJ!SPI>#?|igi(Y -5L;?C$W0LSuJFJ2c7IK?Laba)J#K%miO~!lb?KzO3+RWqe4eDIjC{@5czhl;;>nyv0a*Bi0JmoiBFg8 -c|_S_G>DJw0Yr4Wk>JHQ<}&b- -e%m4}MLb7E2?QN+9xR1;NZDSLxwPP+Z|N08geX{$|jw8U;Ql04f2U0H`NUc@XJXc#9H5e$cXmTlj4<( -2aynAVdw=FitZ3Y5Z`VEoPI5I|C*7gI$L3cuV>QY@OCm`HJ6PkidFr{|G_^NoXmI-EaO@jIZg(I9)Y= -10{-9)*#6m3!#ziVNH-!mwff$}7UsyrY}q56$JO{ -cP?cNDX5L;Ys009@d3aT1u-PPISabfMRm>bF=?#85H!!K5ZSPDF+#Qb+eT#I+mW_()=@ml~4|Zd+2XaN`~yoG(Mj?VT%{8CLcjbA}JI`KC!G@J -~`&ryg|<*t_qn-SPOUHwWH>g0_Xe-brM*``2Dugrs;>;@?32(lMLn6gg*ydhokaZYKqPHN7dW#pOW7z -6|x=*3n(&WfE|*Nd;zbTq7>*8dT*!lwrw`!x*tmdDvv<&-A7y_sY1YeA?b{k%Ru2WQ)>6WditP3Ob?_ -80w!U?rg#_`Th=?A*`}Je9*mND69hpSSPLLqQ)WP*)9U_i~@|;MhSRX7uohCi}H3nG)j1p7ngvib*!( -ZHIww(mXnr_{h-`IYtD}P5A{@BtAVEUp#nbC$mz6qwjaXL5OFi~c&Hr&Bt8T8DVyYj%iCD+HQF@qw3u -#de5ztnVi8hMI(tyXSFQF@rsc_3ax#6hX$A}7K1 -ht~<7yba($(N~(P!@%?iC-=bG*;Qa1Yf2$D_q_}VY|$QPr~<(v}T)3v87WWA`J<=TIA5(izKarWI%*K -w6eTy=r-F*K-6{rET#80oostk(!?*iToMlVMc1m`?(|b!Hu~q5WA4!x8}Pd7n|05vj1zo7`4qJfY^mM -6N4+tZfVm6}{RtFZDbsV~WBJy-tvc -XC9(!#hwz`7B>^c(@GSoeW*bj-2v3PD+u7EB&UUknuk;=%V2P^vv;{ck9B -q5HFHRE_K+S?4XdmYcVv}Ej@jE6^qGQT -+`tgJuGx%5iZ3ARl@Am*@e){y}~B?o+o=G~4KIr%};&;kMzydc+kN(=En^@2vd;EwRRWAJ(Q>hr^;a+ -d7Sgrd5}?k$$0zwIm!YLVFW*hQcRg)R57UdOqbge^hbsjBsrBT?Qm041^$i@$4!qfYr{GQOMDr=B?aI -=61lquS8h|j$#}^6)uWd8Bawt`r(O4agz$T#2_p%>?a2@J@f$%MGpmjT#l2Ztr6?~3|stqiD5TU0rd; -ztm7}G`XHlqutUZa%0lRO1Ox^0>i}5kJiZYWbC$1$xXa8JXzZ-#El2G^x-A2>rxjvGRGG?598c03xbL(?s?CGu|BLVNLWy^#N8%K-i#xN%rYB;sB^1l){BPC;E4{rK3C(uu{3nv -0@*#MKSK2|wx*-IQ*x729VS1Hy6~rq!E2_rXk8pjs2LZnx(x%8C9bs3StuWR -*z4ad@)5FD2Z=^yy+;jDF8>rjSUIHKu(GxGfQHs(dwTaKYx4n!dNHNJ!>iGMUbmmG;Cf@5^jp7su9~( -rtxkKA|Dv!L>8&&3oP!Y^Sq0Uy5zhiEDssC82;_#6!!m@ua4dwzi>)_o014V9PkEgXSBEq{NIYK5}!^sNwB&bg1d{boEL4K5eF;+Q6AYwuDiK@CGn$^`gN_xYfE2d^c)X26WRqVN`$@W{uj)ENO&y1tNqC -o?xTr_teu#pf>b$`07;AH`3?bT#t7;{oA(pGAW3ASBoiZq}K@=lS>`M0X$+y1}N04k>YQFprJtEQzwD -^uSsn$@nbm#!Kf;mVSgBs1GOaR4#tL&@=3a|O>NR(&&VcBH{jM5dB6%A`PipAcC7xcwx}lH2RnU{w7vA -|aa+vevJ`l(r3WaZd4+lD}mETg&CP(SwebIWJpOUMfCUUb#pr3f!9soL1 -Pc$C`XxwibZ07k25Zr-b=k9<{OWDxewpFy~)qfSYZP#=2$HG(|7)7w6^<0GBQCR{>(q -XZY@TNfNpjL(=#O|VmiQ70Vd68y|tLu9GG71lkQd)nqmCDZfLR_b6w+)mw1Q+D2-cyXJaD4WE`P_QHZ>J68|92b1u@gd{OLoDs0d`ltDJV0$+GpGC^2s19gy -fw4+Qk9h#iu5t!6xN}nOoU)Q|AS+1Gx-NWaO=#0auYLO{oc6h*^|6O{@fEhUQ%jz$fQcIAeQLVXxl`| -c&dpD4bZC8LfOanjcU9wMC+?)hLszMOzfFEw4%@iLpzsyd}nx^4`5nN^{#D0Rwnnas(o5StF2XG8**5u%F990H7%C -#9X!&@a4N02r9XUQT~c$m~22GxDU5NNkmW7YG-m|848xNF9FIL6rHfDSa6zXH24w@9*KdXEmhy -e3lQ^%!B97AALFRdlbByXSnN>@e -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 -0=rbjGr{4N`~ge9Q8GW3Juns;u~WRF~Q1u62 -+yhogjF^Bw~3k`U-V!B4qODZ2j6)a1%mYd1vs~U9Qc`fg|ZVXz_a{J~v`_ehqBJ~C1@^WU{)p;Z5*Z4 -(k^~PKH=9*F0L8JPjOIt_$KTt~p1QY-O00;p4c~(;+?`QnO0000I0RR9g0001RX>c!Jc4cm4Z*nhWX> -)XJX<{#5Vqs%zaBp&SFJE72ZfSI1UoLQYC6B>Q12GIl@A-;R&JYB>@drI1@dqfIc)NzIQ^gL|{yi?e& -5S&w=NRKhud|V&^ea=vI{J>!!?rFsK`l$oqoVOL@?g>@tbKsRXh?3DO6by#6vA05|8kw4mX=k0)5}<= -6yq-L26=gU#)A5mzLs2mu6f%9rO0N%LTQ=u)iph_Xe82$iIO9KQH000080Q-4XQ?H6IWzGQr0Luda03`qb0B~ -t=FJE?LZe(wAFJx(RbZlv2FJEF|V{344a&#|kX>(&PaCv=G!EW0y487|shy;b5Eity9h5|XPK?VdE(x -FLD!yw2qooG`eL!!6%?!#Rr5l!+vJwC}(SiD<+w3RZ4J7}q1e2N)1Wm8z$rgQ3WB*<4Yxc%_)7 -WPMkZyg=2ft{`Ck8lWIY-=h(%9w?Y%!c?$&*zO-U_fPwW$6ZW@J~o+5?uGo-SVtae ->p+=G{Z>^gG)OJHN8e-X*2u{1i-2HEogxCPPm%9DW1I`EIfo^D&!mt?FaCftQ(LO)^EG>rsK8JI1lBrJuErzcg|-6C@u{4EQfkOY9= -!XMr1a7ZgEJhGjJh;_YpHzo#qNWDNH)I;)ElW{e04Djf0(O&Q*eqW*IWMEq{*GUZg0mj3;4aU!OnYXR -pk>SR7@$&5WlMNvV(QD|tR~1Gov!Nf$*ExuWk={oeIuo*>BVpTFBVVk{~X0dS$J*50V$?KN -OY%XM$RXAZo)kb>f@aU0dp{x;Konj<`wpOQDAv-sNg*A;a#!6P)h>@6aWAK -2mt$eR#V0~2Gda-003)b001Wd003}la4%nWWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mb7*yRX>2ZVdDT2 -?ZyU*x-}Ngx6bueG^u*R37I$9g4vyDGj3jHrkL+Fqff|xcYNFu`GY`x3-v0NiXFq0!lGX`uh(6dPr@O -kUy53zqr{?iDkIkxD=jHa9>DtZX|G-~PPEStFi)w$U^X+wOk{2^`_T=fA`EYH`+e3R@mF9i5X>YUInp -agIK45Os -s>#42SA1ef_^ergW>W1F=no4VS;<8+_3*G3*>%Lc#dxJ|js+ST<*TOXd -C82F<(tEwn0h3rygVC?Dq6wkX>igTZF0-}m{iB?-vu42INGw{M%%1+*kd)tJdx_?y`Q?|Ia3KT0{QmHZ*#DSXm -OqOrm08}3j}V2%*(;3Z?e2-2m`P6Kf?Z0w+$hon*IZDH_-ENh|~48-IzeflTA~v=4P{Q+PNugxoxk{p -MLwz(O$Wqr?11eW1;;`mssxYQ -AzrJI?{?)ea>QdcYI$YO{mOXm&KG>li6mb%a=<}iw2fQz`7EMO;%&e~M>V|>FeJ -NG)>!w0cwa;8NHO>W1TCmHL_E1A^T -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 -)(cC;svCloH&@3au0B>m$4{5W);Wl -4EC!IA~;ZXKvAC5hVwnGGI2wClA0g#But<0RiooRi?58_%>jo^MPbh81=1xf7LJC;!(% -ARE^n^EfwZ?4! -qf>>60m9vVzCaUJH}7ZL2BJ-Lhb|!pwkGup7=all0bKZX99Xd?HUyuCu?XL#uS+OJ?nXjiQM3^In}lF -HXc|r#DK0}U@npmvD^)Gvmlf#uaIubWO-^Q@Ss{zVz*T^c2d<-W>OIk?qw-jsIgBxbxXaD -HEAE=4C_wo-caJYkhX>lsCV;imQi1?JHM;s7`4Uq1){=F -8^mrfXr;vmY;@ks;!X!#zdUXhO@x0~;oKDa(ZBXmGn!Hi*#COJ_o%{yfNff4z5cnmO9U^tJoLML4u6> -$Uq%rK`zUp6ZAlvEs9n2kk8?{<$(H{cFC*-j5^(Ik~-Kb^jLx5a$ly>$i*VU%vX&bI4}`jPOrRHTaxE -w4#~2e2aQ+LGo_1;>K;pND(kRy>oNNdxEIW=`|5Ano*4`uC*#kq?^MQC}F5CVv-pjIpghdS?&ljY=3@ -r1pIyC)BlUaDG;8?*jk9qSkx8!s>SnnFaGfQFE1Cb-n{$s^AGPaZ{^a$_6sIM&rA~4VCLo`33D~hac7 -rvE?i8p=#bTq$w$oQQuTNkBu-=r6n`9p#gNQBu*iE}(#>aIrEz5k2aMBm2Ta5nfZC@h_o|DZ15bFU-eJBtyPP?V_djd%dhplhdWXGuocanX{DnBnp<*8Yj+ycPHE}C -6w$t!6Am6+shl{RhUb1q>6fH$69$b#Z$4eWK%l+@VkjfMRlZz0e67HB3M{U?%7Ev3U)!>=yMG?;#w9) -xZ*SC5MZVrDq?4^=5#R&URqqot;nk!c@@s4{nhs$}S<|ZNT&q-TncdZ&wQd+yF?Vt;2y17%|_@a87mj -MIfp6MP&JfV*c2KL}N^zqF6=KO5z_;DOfnZ@l-;11x&MJRY$4I0=}97M)+Oc{G7hE)3K`!>Skm9;I7E -G118!5cur3{IuCuBsNp6|n{yy~tOe-3?89knVleAw*zR__Y_?ltMgAyRur#++kWbW^3{=f1aCNUUq1) -4np8#x){EZi%YJo{+*ffa|blGaL){#JZ+3rA|_nKpO8yr6~zpjqKk=Qs;r#$TSOuoL@uYo7}vLuZ2AW -n-UIcfn<1PuJgyns>5=YCScVQc!xtn$V$Q9Yh(`b-qykeQD#q -JQ{uznDOy0d(iKR1W7nCbq0cM^II{NXeEGvk9C4J@utBMY5f}WY9nQ@&(?O?Z6QIs&dM#4z@ox|wXWfVYhMWPS%x0O2l;-_x -E{;7NjMX~iqfWAI&@*~2`vw7gwH(f=2jfr&r?|r!dv+S!6s7+Kk)}Sk%}^K)?x1bJs(;}eta2b<|9YJ -gQK3VyeVOudzOMZjP8-X>T5Ny;rTp73H^aJ#fx1c;Q81giD^#*g{0{%gt0fQY+W!>MLOX%O9B2s= -isK@Jbmi$Y17BIp -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 -9N=CpGG7c#%COGN9*Be{_-e+Xg -5jXI){N?*Tw>9Lps7-^EwZN^>)PAa -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 -_@WJ9{p{HV>rM^W`3T< -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 -15i3S5>pnio--%;y -Xv#m?|37})lC7_hkoj(Zi!AeP+Ao2&d9YXFS8gE%Xf%OV2=VBDzO6#gr7fCf1HBbikEl$yHC=kxpqdQ -Es_(-eDK?$KOdK!JFie@>{EOh!?bSI`T_3ulZmV~UV>YF7pAMt*)0Y?T4$cmvqx0y^+3DYoUL3xNItS+wJm~bJcSjdLoxZ(@pvc+5$;B_x>C5Qg -Y&eP%hH)n_E=h5j|boBbo@zLRnespy5{P^vQqmx%r^bG2roL)r7N3V}A0P@9YM9m7kqr-E -60WhQ2hiA`!f+q*hj*gEmet|K4d314tkl`Buj}D^f&B58l(et;*2kG>hNIC%juPEJpbPF| -itqld2#Pc8<4|49T-qv-H&@H;yH>EQU7+l~(2LZ{9EGQfR)`sSCjqgOv&0F0kbk6#?(^Jj;E@WHd=Lv -EWo{QUUf=ygAOaq#-!)gjkC1!#Z@7GXz|q?Bk;q*(<|Gg`YF8=X$)EQzNP6-UavDbgxIA_tBNBwk#{RTLMKtMqeH4zT#f#%S~zmJ(D4u=`Q -xKhBcRDgOKqcqeZn{J^`N!S{om4%CR3z!wUrfKN3a{dKvslRQFR+Vyy8535aqgJC7A_8%|c*B-Fq;c$ -CrcYF77H2iMw(eB>PFnX{93_9w*EYf}i&tE2ED%Oks1EEYlm+6$iAe5u&p0Qm-PoF-1`oseIn1TL1x` -vK#!=S)wnkU!eWib+pMM`tN=^=WjG-vA%WrUMwPO>@6Fq6sR^h -caj7ZK=NI`WrM_sqaZqWV!lLrPoPas8~*PF=R#Rdy#BxRK@My29B*fn-vRSz&F#4nze}@eep4cu0iOaYdNSCFia0HSah>E9K!kxzaK9t48&Y+|Eh6F -UN8N);I6sx?hbs1J^dGIZn83V4=($C%{LO0+>3W0~|NH%I8zs1sKqI^1MiZ#?E -iA$2sgc$AH?KBK-`MGE1;e{EbTcm_Gp2O;q|lHIOIqbz%(T0ybwHI$Co=iE0k+<64=|iL^d+F5==Pya -Xsu#Vi~CAnL>pRJ0GJTqKDQ*k4EEVIKR0Cj3OWb8)mBy1QDK?%R -pFaQj{OIq8Q1$U}H&AJwWV7lDciu&jPZHS4K?EAl^U0@jDddZUW2+1oW`qNB$K?DmK(25-i~1 -JIeJdWB3~(@$iR6MIMOVgM6+y3V}lYBh##;?9=&%f{`zGHJ{_~#A}`Y~X2p%;!;@DRKaEZfUL%&nogJ -@>*YTJ1dUaA%8Q{HfV3rTigH!zY*snlPhsk -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 -Ioz3iitpm5a=DmYdNR}zCj2rRmTH -NiZzU@bG`f;2;VPJ5n3Gt1C^MvK -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 -3tw<-1q4Q+y)QlVDRg^3_{zfafmg<~yry -+I8xo7eIMG=TiaAtiKa+ja8GR(V@%;q#q?I&KqCnnA89~#_z|tuyVN5Jy;B7WDC!8_qD`wZ6xP%#AbL -1xxb|~>me0=8);k$!6H~_%z!Kc7H)4D>j@T8y5ADU~E@|m3Erkv5?zU}y-OYB*tthUd>%6RjM;b&s&c -T&Avl6TYPjWd109&EwbKc;VS(VQ5yh=RG@D}%1I7@jUQjY$5YB*}a|7Fg_*`Q^^GVFer?hW+Ty|xBt) -DQDki%`tB&5Y23>T333;AREW^KE5M8hQ7sL$+Wg5gC*@!F8;0xOBV8fhExk;Fd?H??k;^mK14-f&fg1 -`a%;^5j}&kAiJ_f_wzJMOxoWb1}GnW`6N~amTw_@p>uOc*dIIFR<2+5Ij-`;B*jdvE>jPt(V=aeEu!f -CHBMOnMO(x?sTiaG$+cprc1%?|)>=lcxQ<*G^z*&*tD<@ -BMuMImy+G*2S_4*^>v=SbbyNr;JhH31)Oad41Y4Av<^_IUA7q13d%aOGKj^OyL2nzT%MxO(r4(}Tp*I -)YPx-FYEL_BjSD_L2XQ;IxBaIsOvCOxrS&K>uMCQ_t{*{_Y2#7BaLVSKX}&?dMwrPgC9uh(4UPtuD29 -PV%zT^re2l1-BRyd0b#9{xN!KfL&M)%(xs7eHlXap4;H28=a^WQgnMI{wn7&hGRK?b+NzgRq~!J@cdv -HWUTehSx5%>8!0ta9Bgo2=yWeZT;RHp1fEwQh>2;hnSMrhJ}5Tzn3+);sByf0z0nI5fs3DwKrtuSnu9 -|89^XOthOTJI2Di5Dk=QCUlg9CA}N3za?zmrIVp65TV)%Q`-2#ozb|_X?v?`7VQHmZo0@d+ID!r$#Al -c8$tS^Rxpf2>3t5I8CZJotKzrZi!>5Ft_#fXa?Ui4zr0-T2#7T9E4zGy&v+q9sJsB^s`9d=gZF46q5m3hw2wyRJeQPPU3I|RLA{v5&RgN{+ -hgS;R1P7BWzh3N1+iC9ognrh0Mq32b^MRKz*6`<&G37mAR892m|eK7&!E$^@x)DFnu;!J9hF -Xj~?mLEDq>|iKSfy2q1D&n{1*!RYd93E;$R8r0C>cQL&)dq0KLq71+TUY0WOY$+;~tf-G(jl$Xt_qr) -if7;63O=PP^2dDX6s!l(FFOmB(L4K6@0us9wp -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 -L|uN8dn=TSm23--6inv1%fh_= -<*5JdZPaI1WIowBHTwy0V^rOJhRAZhi?X9e+4R01wAf9Nr`_Y$_@Ph}JwAom-sD@OvIEMO&I#OHO?;> -nT=XLlV6*8#2KAweTFflVEssz3Rt-uOWf8xil-om0hI -$0FrfQ7zCk&N4!noaYtRh@4raAsmsl2VzckjuQ;UK~c1m2|%v0&>fn>T2S;;VG{nw1weiSz{&FF -`U|;TSL&98|_a*urMC%S|d`5$b7fkX8tf?uiGU&6#R#S=&W|GM3bNjf6WfD&h4$*Mrlr(6 -8Rb}q0G9f1I*p^2jBjiv>XHnX$1J$>f(EY{t8gy;r;o!vLm5N=PWlQ9m^%_$}$yeF9 -G}XTA`+!A=SOpimp;>9411)(D(O$*zGM!hLHNavq^~|gHEmQx7XBsvkDJ)E -(;FMIKviwFK(ULb2LW0HRU&-xFQ*>PwS -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 -5!OO6KQB$Yvyvu`sFuhR2kI -D#rHK%h{;ohH3}s~i7OwBJRZs4s8R3P1;5o5%%NsrKL# -;_hA02u4AfpzqdnlMJs{=WOB$iLjyr~S5*f-k`uP7c5C4xqlnD_ml*ezlF&w=s^caE?A6x`g=a%GKkq -X{JV4#SZEtUP>+n-yq@wJan4bw`{*Yiwy?dNl?XLEsbx -mwyzDl7c4pVL>TUvooc;@-q3`N~rh-5KdCIU=%g$3*jzE|S=85dG-q{{23Oc4wC`w9g -W&)#dPmBf;VxhqR&bLX(OOR`G7}((>oB3@{oa{=9MnvE3y&Jh5`LalC8j6x76>%3Vopo)U4URF^ -7Rk7~l6nwEISBT7Tl?S;vn59#;t`6j-(NunD}M}Ri!F@}q)G541R)rM_NKAWch4}~aVPZ071a_n=V&t -_C2bz`?~uJU=HbLI^ZX~&u*iY)Z)Qv@$k&VmWkb40UYuaD5gP-RAtL!lW&(+kg!bmxO|YtW*vrfun~3 -F@QOV=3vownN8^U#6_?Y?I0hJHUj*D_o!B2@A^WN9M3d#oEQW%V*?yIk?2+u^0~t83=*mSd)tigtS5Q -OTI+DW^;{cdU4KL8`;Y{rR=P^NogA&}$owJ4UjzQLWjfcP;i#;PyKELsJ%*a`1V! -?x&S64{ALwF8#k&HGqFKNU2zf9r^ZByksFGY&mJkIone$xGORKvwe_{5oUUH^d{c2i#?i=rvt1%h0rv -dG=`?=XsYKhf|*P9eHF -?$OeC-#k2Y=K|r_`PrE@PIL>5G$ji#6~M`uklt{8I@NtN9hOcA)kLXHS<+=7sCfVhn=KD7<792z}0n! -4%y1%svwU~)d5F>z)$#a#;O{q8So%BYY?wf$}~xiS#Elu8R5UpbLFzSrkHOG3fdi6ZuB^?@M>^AIyyf -)eAUI`ZrFj0k6yY440zTzVNR3J!N`c7e>pi`*Yz@rgpYO;eBozCtlL*xmUz)(akVJ^vTwfnKz}|410h -j#Ku&&14fCBMajp$i5T?XdwEx>HAcsxng;wiBKYqt*9%WFrNoFsO&O&77?pf6kY1C*^B_5_Rt3d;keL2n}%*t?PU -T25=RFWx&&9BD8Cc;FS9xrJs%A=J|30=W>bGhPBj1^us~SmA8_@pCHf`2CVRlqNgHNB%y@Q)Z>CRqqU -%!%G>+ypj2U(MaH@U}mqZ%}w)V#bzG?LpMgr(%h%L#Z^c-sIDclvoyWH#>Dc&yUYbg6v(3A=4G@!- -`Ylto~6kxtHqLvE{c4*oHQM(*!BheIH19L6E3|9P*K5wxY$*qYci@HUZ$SzZ$(>yL=aT1bFcjCy^`W) -+v~2z2-jM3Bv_9qW^Q#{8ZCI#hN%I^^XQqCIZ{V}fXyxpcen?gd(-~CX{SNjaz93w)hg(Fq}^*nJuKY -Vn32uQZqY(Ckc|HM+9KLle#@_p|_vE9|@`m56wf{YcQ2(;$hLULVebP ->+*95GMQY~;8ET(;4w}0&Py{%CC+Yjc1YL6)YaLFu}Mti4&y$~@w^Co3s_Z?LD8(EmyRr{n9)rlP$pnq|mQ -Zi+!y%?vCapLkvG;9LPS!_F@yLVAyPu6X$@NxIkxZ`=m`h9N?F3XIZMtl{i!Nh^JXufx6QW-TVcn`ks -)Q@Q&g2-2=cH&!%V!5@dZm>+bgH5E|)dPxXB&&Z)k61wZbIr(`=sWh%c;#`zQ$JFOs8@x>NzPUXN)wx -j0Bx+ESB8?e7$R|y>J`F%p<7A;+JyHkT1$_by9iKof7t_Zf(cxPDmErx6pZtF3ags-C7H5&5ognh3o|#PnbOzeIM%LBq39eRlt5?@ -jGGcTzgOoL0uOdP$J<&TN845kN5%ard -Sebbv(nOEbd#Skrl@E>bPg7F@_Auz=teIHy4Q*9?`O2ly%`EOFymv@}lL?&_~wOIBI4!_MVLn;_k%3C -dq98I(cA30f5v6h^(XSu{`Fd92I%4rt8OS36nQ&skUP{V87S?o|&%lf(r*{j?rFk8i#V@09x6QxCwa{ -ew!Nu?z&?DL&>@S2O@g>Sy~ZBH$JbPQ=W{p6UG(ugk7w*#4|pHA@qRky~QZi*Yc`Q=6vN_R_Icy@m+Y-{PfHdnVaVIxd7zywSjDH^m;+MVmr6=;codP$yM9pX*T7 -)zyAy0E(5=4X}Viu4KhOaivln98iLn+~mb4=cHO|Hxl=zit?KbV}H|Wcsh{`b*WrETwo|Nau6+@)aX- -FHMs&P35UecLQRkv=*{ka+u_=-W;xOW`(jC{c2IbSFjzu1^F$tXVx6V_LFNSGa=aPa -1;(5^5ciJ%U4V;^uj#wjW0*{51JQT(9n&*Ng^$d>x3j4zXh_+m!sm2pH`xmrVEWzyftzuU1RPj5yqay -jg)Mrc$mO=SA$$AaEJsAx@dNpZC-$O8nnfXrWZLKwAx!s#G2_RLvqIh77b7t{^f!nz;&l9|UU0^A_w)A?c@2q8EY#iD#%GmS)FK{Rf7Xy-Scz8_9)EZaF -*v0S)kgdmA;D>^ML*b=qFk4l4a%HgjhQcb84NrGlEE%Pu8KJCxjD`*ObiI2O)K;SrvO$+&b8q5vaADu -Pz!e+snB?gj<~1KzPA_XR@tSv9f-8$x74HML;axzm=PhFpk^8mbc-7_QV}hfVB-aN>Ud3%-nawd*$ZvYs>w}Cnq`bm0)1 -)QZJK}7Tbk)R`mFws4>(Syv%75BCIqIrh)=q(zEL0dAwzkH_ED?9z_)|hB&b9}6stN76xCnNRx(ciqQ7>C*Cw8%3wP&KiOAgc(7jy{_Rvj{X}4fG0VG!oQ9)Wk^! -c+Hk>kNgqLNvsat8f@Rdv*(XoJCj|P!LB=McV()EW-N>8j{KASY>E9TzU1&Ri{GtaX{bo6+GM#dk{Jb -R!fJgxAt*lD=Bo>=pHoaEmPG@Q_&6c1tC_HB2s+4;X@LKN5$T#Qa|34~k8}C-F`kj{gNLdzCHD^d=YOG|_T{Z#}S#Yi -nn*+ca-nAXSi4L>Wn_2NjaKA_EQY{Z4(8s0V*o2V_gKPp1Q?nDz|XCIyL^5Y&ziqVk&jKbIf_%rH$ah -)Qum_f*sg+axX}#2X$4(8&)9(*bCP8X8AkK(j|Xu;wP)re!_GHMWhUt7Dtl&_W@Ld`(TVr&3wDM#2e; -H7XV&22*paxlLY!2Q`2#t3DD0N{!4x>~{e?4JvTi^k9UU`Ai8zE0Aq#$}&-9Ia{gXvdkwbQK`OI5Ez38)&&il00bCb<*Z2_BBo<--LU8A2*Iy@EHTAsNF@>~ptn%(~oES7`_uuM2qbwFD>AqC@Nn# -eM{Yu^`=>Lzc5XgHg#T39wTajB>IME03Xgo>sR;<{h%P%wKo-F))~(&y)&gd=H*gDmi0X&RxN;~;qbg)#wT`QY&I{n7bFlcH}8c*Ph71gsP*N8Mt>%5obVKfCV1c -EUEHxu?Us6Nx;JyAMQ3U0tJdp}9i}*2Z)tfKAwFSk_re1)8zr%vYZ?|Y9>S%Z1!_mI@hx6GsFL -fB(`l8das58mN#=4mJ;^MExh{02los{ -5~feRb^Rv*Vvz=E^Jn{HL=T5t{joWfsjTDVB{-=t{+?CC}D-0rr%WKzX-0?*=Ze^_wKoCPUJBlGHMtZy*AE91S>h^wi+NM}B?=mS&Z9nR(S|Nx*i~Vy;@_WJ-O -4Y>}le8S|0O%h}@i(J*sZKXkeuuuk0RT&+glIqNguhR4)E$*tbPYnt5+{+y|4r0P`{3qx#tnW=;x5?z -3~4lMa}s%2M%zM6#2rSJDuOX7jhaqmakw^xsf*|z5DAyM3N -Q2hFZWX{$WVY1~OABN_hhVR;umP{e;Paj4zqBV{p8<6fWioW0HHw>dIHE$6*?obwX${cTGVN|spOpmI -4J<}Lvx~=&&Hjs74*B+kEpJa){$)jv~t)a!RgbuA22rqWw+rLRWc(vGn3;vwu5 -*P{B&H{bzH1s+t4CrojRLfqp9V(HUjC=Rsj!GAc3Kr>s{M^q4BgHQ746<=CUQAiuiR4rkbOf)g8UUx;n+4wLmqcKsah0sQw8nEdMY*8$79E(4aA6?t`6W$kP_^=!yr -8H)Vz$DjNhr&R@v>MEJz(YSKhGCUtb;bry4E4T79^e>)cf<5xLwKp!xC589jtrWIs0pnZnXzm^7RfSG -%=>M>=6pkZn^yS(byD}|HHmp*Z%nGw7f_$<76}f8iia5;5Wg(tG;S|C=7j=F6c83)o)7(go(TZd`s+0 -+Kmv5qA$m4+2v<_X>&C9nBdy2~N0@D{wWNyZLnGYM96OIr|)pwZfn=5rJM!DWF6%RSrm!Y|}8vjvK!~ -8mMLm{l-*wBp1UMvkRvTxQ1@B)zt3~xT7cnUTf62M4UDDvg(N@7@7X<;XWCZ%tl=byAh<$8vx=?hY5q -tIZ>2a<2u>9avhVV7h(hi@bAp)p{YO&oA@3BTb4bj2drupv&C72fJWp|Y>SG*+B@wIJbntMsmG(k+>C -k!Mpn@fBcF@GByp6N8rs{TQc*j-II?ZuH#i5gjm(M(_Ge^Q*})1CxjeWNrKSu~ -V)a+p=ZOr%jn)n7cB0B3dM_HLlem%{At3NORwE+{95wX$4$(6kw`v*oPbk5zC-^py!ntTKX!Ucu2)VK -wVQJYdy4-_%)z1t7_(wL}h_jp(G-@E@yTR-r+4{C_G-yW)!oQW17_g){>%VNfm=PD}LBlaZv$?jUES2 -f<`$oztN&@g=PUL_gw8be}OPO&@d0PLgpv`DB3BVfPxtfw(ygLJRXCi}VS -JiuFGopf#GV`H~3+1vP=C7O4Xz#rtt+vXR3(y7-O3sJ8RVZN1)$pclSaMEVYH7J;)oqG=aisd(!h+T`$5F#d1*-`R@z=x7A;(?-EDa(eUxQ^0tr0kDI?45cQQPe -l3Zjt8A@wMo>OC4nksPjfo6h*L;Bh@S5#(05_q9aN_jT^gatyq&pS65X`BQsZ)%ZiQ2bQ{L>0F677V> -Z08q1^s4iWK;;><<0yJyMom -1*%0N{qcL;9nCpsFYE8$U=-F#-GP#o6?PAmaM*y+;CqD4>&wUW>sLrfH)1VYSqm?XX779NM^~&?Q!o6 -~<;Hfa-Vf@-rp#eYXCpCMkPaGjveU%s4O9rV8Ajr$6v8BZ&u}N5j~0?jYl3b-&ia#74T!iNMuh`v?_f -fu?i*}nWJGI?1&g)WBANH(g^{8XwnMeQmV&`FXmvF7=FLb{6;#$tBy3&YTdSXj6IV^Z8WJ#) -HP-liV~6YK-MhY<1XsERG-!;Wu{m=Ee3RC`5hE(&~>r-lhhyQF3j|s|Ni=HMHgKsRIP+_*Ra^1AL;Xg -DMqORbt+b|L%*i=r+NJkZW8M44ECCq7bs$vLdoBxcl_KiPUFf_E6TDBK3FWvr4yoZMg@=Myw0GJTj^* -QR`_^=bI{gnzDFR6mC&vWDcP>6TN*H^Kn1d$fbkqrhw0Mm9 -4Bo{EGm(lCce-!%$H>Db8UhMdA@6aWAK2mt$eR#VESE#WN`003A)001EX003}la4%nWW -o~3|axZ9fZEQ7cX<{#5X=q_|Wq56DE^vA6TWfRVwz2)LU%|}AOeQCnFCJYt@Xpjvm|il9=c1SAdxql>DjM) -!h5>+ymVoY=}Fq$l?cCojOMT*IkIa}^pr74*!#Sj)^q=$WU9;ba*&IV_YVKZ(Zfegb#=BsER>*vn5!G -^NHbKlQ=MOFg<@U@V^yLn+mTTo=l*P`X-AIY+O`*LngM_ylgQ#|m7G9s~yGAS9X4%7i47ct#I{JwYBA -nLH*=VmTRctRC_rC^Ms{B+bB?WN6-lifK;h!=5nIzK0!H?fhlcBUS};=rGtI2c84ZzHn -==|B({q|;#aK|Q5_&R1EVFAcBg8V#XwC?+%p#T%VwrDH%?Po~B9{3ParEe!uN}-nOl}sM$<0C<)W%pw -_+&|HIar>Vjj@c;pv-KH8DqrJJ6pzbBRqCDdrL+;Tb7zh$f1WoA#vvY0q#-gCk*2ZkraL9g6 -mn0qiaw;1MxVQvx2DaLXjMtUMdcy3|I36|V0y_{glEi5^~lG|v>DOz$H8#%>BZek-Zbgbqj4O*HFwjc -)!8zcoeSXc)OafPT=Z7 -t_!*;G-iYW^n{_-qmNNps?Zd9x+(H?A=HIf7mTS_QDRC;j^ZT?ed~dGpNps9j5zDPWJz$(HTCsk!; -rF+ThMqa3P(ahns&tHOUp{N-QRg(qoKgQ~eOE8kvx|Ca;f;J0+In>H=>fyHo1IU)r*C)9Ai)6^Y-OB` -a?sT5-60)vp*^lbdN2sHLZ`4NO8oIotl$rcOrHp=LkfrVx)7R@PzHD?K(rMw616Ny*HlWHc#RV6#B7ISXv=NH%A|R`N%gVrUMzETP0Mv -3khVEH`LmIrYj78d*+RshNVzOhHaI=k8XrMQKxD<}?o?;@A|89@ldVZyr+G6q*&6lc{-PLMtazb9;*C -WNL1sB_~sJi)vnw=E6K+6fR{{*5-n2E-btS*<4tg3$nQ&cvUHkb!z3A)Qa*R-=w)EO;Z`ze8tmQS4P5 -i11V_(DKoR)%RHgP#kfP~mi%F(8I@0`uPns6z;vP11s(hqJ@HnFnVl+$h+}fBiXQtbHtw{t6q_bWl2Q -|6RYnT4SPN%k>M*BlN&e_V#VtLvW2dxZSLB5}G1$1PvlM%HyLhu0w%<7U^ft*p6-KmeN0H^wb8BSZDz -J#2e-bMDdw&{>7{;ac0#wp446#E_+#nRh*NUcYv{ -!?9>QC84aR!e`F>KyC-DZ#EC&K%(9V{&&NbgDVGZx80$6-;2!v`^Yn8-N}x~77FW4OJcBXW@mb}-rVu -!f>s@3Y4dJu7Q@44C8froJCtn3!dBp^s?^%AH-l)cz|m+@E!L;eA0==p5&W;$RCEucav_T@O7Cyw`jP -eD1BN|w)Zbf2fpUn47QI|-&AQh1Qg++Z=kK_*copnV5@EbW57e@fTbWgYCl<1k`U2g@#sfh)#dbkrCJ_tK47izs7)vMz}f_=IRs<$`F -wx#1VNNcyiII4fY3&z>%bbWkr$aOJ%v;GQum)x13L-6@!AgvzbUM=X++x@W8){4I2_ku>1LvE&H*V)W -QwUE2=&+jk&mWQYF^5||Sl^*TrnSn=mCYPV|*AwRxnQ+kQsrD8E%xufDa3jwr4HR`;*oN&W-5IvW0XX -*S??toV%iJ!5*Upr3fbSLgtswXC!NN|qs* -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 -V17Ogl^Jd!S;0SGP>lSRVkLovo+CE -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 -8rR~t0WtQG -7~3IKjVY~5OP_fy>sZibS9S8P?~XpjZG`WVYqI&UGS8&Gsn -aY{&^$>{)(g=o*4}ccGDSkm&(9Q!`;EnNDsnnzgwFjaaPX#14~Ly!j6%P^&qk2G59nfKb) -=+zr*Sz@xvcyL3EqV+I(>Bv^IJy^QLNucs+^v`VH6(acgLVj;ac1ii^|uF2O2~F>hk<3NGa)D(SA6cw -L;RKNIp))?~CdP%4U`u_e7aj!M`vL+W9P;S%PeN*vUPN}0nzf=9Z3uFj0ebb_|l`Ieg~wL*FR$)FzI! -B8(hz}9g9^h4{oC=Lol!*~S|F2}`So1NY7Q&n|ThroUY{I4Si)m^r4L-XJH1xHLLLn5ZipxVL%*NfJ9QDtJ-J{*=~k+p8#IubQi -^Bc4SY_mj~?FfTK3t^bF*1fps!alj~G~E~w^;jHln^+*D)N!_{z*^)x+6)9?J+x1P6`d+?0!Dl*Bjmf -Xg2qgF)6)O9W>uW$8_-@?z4}|AtGpw&OfW*Iy4Z0HbzMoiOKS;EuN@$&OJ9k`npE?X$w5N~OMUOc)C3 -l3X=Eu?nRNJfwA=zYhjr@5y36KR)zYVM-_-L9n2X8*K}xsbfI!h8MFXs);JWfY=k6DDmFDzFgs1U-A5 -fgiRDotVyWynO;ssni_cpA&7HB&Qf%gO&9*NELrc>mb0khs4TwqVhZ$0M&N|`v4T#S9vRE0M5x)K^zUve+~jz8PIK=oG%nD(dH{5)C)`u||}Cjk=Z_Ai@t3s3$Tg$YjI -?ux!D*+J54&I>%GV$Zx=W5I-<1~rzV1QBwsatU^_7$%UO^HNZV?lr$3s*6j2xV^tV5M})lLWXVuFoA; -l&P9a@A|?-Hth$%*Z6pX!8@}w;J*Nzo89=&-9dwiIk}_mhi%kL{9IZcmydtJnJ3PL8B@Tw(A#^Z;@Y)cdWk{8jA -DQlk4lhBWX#kcif4^!oLoiKSs5)?k+?x2EBt{SIBQX|-(@+W@O&;!$1U?2cIia<2kTk8PHm`x`b)j?lcZz9)XZZHv`s?N*zgu&8i9zPuD@ILg -MgmDYr09P0`@i;Ll1Fop5wLe!DVaqz9!9WzlKYS%<+mLXn>M1!xL&3y-rsZqO>srN%gu1b-9HE9xZ(d -0pjwEQWP7iu4 -N|$*FL|=KJiZaJ{R;d*88`?LkR~I>ac2lp)!lGv1gu%~-)`Bovl)=y~iZ%cje1nX+d29IEwn;6E)S4} -n+9W6u6C$)+HeAK4KQss@cR=ip|)~a`ZEU;2uW3k#%($@1 -oZ*8z&Zw#qKOm`&px|oK>NWX99WYz>Z#YP_)nt}Ht*H|#?SgaXxm6|bl~>PYyL)lz>l8 -f0jAKiJ`c>=W#jt5onOt7lI(3SC~Y4)q2I9FO_sTi??XKAuY28k6$i8jy6ecUIhU67w(YcU6{Ks=*dq -k%CWq+Hm#i+eRT9H@m#sX;!^sD~;zqp%(*Fq}GjGH+9-D -+6talk}W(U(9KvQQF65^k@fPe8|;uYvJ=v%`=|66gzetdwpT8j0X>iRh(u9UKVZ|8!5+`S`=DF|M$g1 -(V1$oPm(7>u3uFSEdBD2miq -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 -O!n>d;+<+k~GakOTm)?{)CV_UN4<&_fuS$ -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 -YWcJ7@)opTb`4i#UHW2?`pW>jPsGKRvqs6cV^ -MEz2v3BTIpec&M4OCj9V>U7=kW>=4=y%^8skiMc4Fbp_?|%1wXsOj2(sWtbO7k=Yu`ZRiz!pXomF1~_ -k0BSg!U!c}sHDhL&eK9zu32G%U`ZHuQ*zU=zg2lv@Jl=j0;@OY0pOn_%T!VD%p#IW*dl8gSXl~r9q{{ -%mlj^T(^pz6y_FI%MlKXX$aH}~_UKM=JrM<-8zzp -Pk$*RJp?lRzo;hwLxY9~ncAPTYUGxW!G3W79cMS-!K!aN+@g)xM0xm~IJr7$J4*@|mA6Xtz|BCna{@0 -h@&Ry6*Frhj*-E-=`o64Daq+68vTGOP5aer&Oqq`pZDC3)I{neYl#J%D!wvrDDd2Bfl36JrR0$e|;L= -u`Fbmy?qte9UIE37D0tiX0XkK{8fduPiJ!U{@TE3U!B3Ha3CR60W(V#KbFM;I3u@CO1&lvJs&&B=f`N -?CA7o@~N-}xWvS!W3_Cu9E{a?Vq(-RTOlE`zS*EhWdfIkkN*pf>KNXDArdc^?t&SfgOW~3qCi}NL5$A -M7&Xg@LUZDg3f4SZF)0kzl!9fY=?=KbSjii9|7Mb|xYBzJKfh%~^fdhBJr_Qm031NMDKL2FxiD-|@F~ -$8p)$Eo|D_89mbNsO$&5$oecTnyq)W;J_{c$Wp4WaAb_G=!w$`Izb7)D;z6R-hIzuB<2*nXNq*hT%(m^KLo=Q -}Rq;N|sucsJ-CkJaBaXNDxw3!K~}8H+*BzzYH5Nrb8i&rOe})n@WSN$^x~Zn@q`D#Gzm)hqK0-7kKRv -i~^Qr+_)U4#HZ5208t)d*T&itJ}H$J*Fv(wVUW0^Lfqi-;tTd!&>V+?G>&pjHWC~LgnZzH=ID|wFMNx -_rBc)^7EFheb9{UazfWq8xx2iA=@i_BNBX1bX&xT#RsyRnW0EQt?MD@~!jgBYG^EU8Z7y&~C5_8H7~b -PVBe$k$M70=uqo>iNm(M9h>y<JG=ll%#LNzfhITw=%lpbyl_wU^jKWl5~yWqh=+R -?N+74un^MC8yY^f1}+$eI{)WJ>}SgCeTm(wmbbV)vz%)h8uWSda}<-76-m06#|H=J8Ez)j{tfp3TxBL -HyZ-;|So0;0H*BySnxqa@4-bbX!`UY@-xq5F8jv&$$UWq{9t|Ba*&0YAoxy?vg!7 -dAJ`VNB@;O>`mGpC_^;<8eMw!xRL5^f^2`%Qj0+~R+XM^!KvTnI3PvFXn(;k7f`s%xXF(Dd4|*6`(xLqMzr -+L+8v(cf?-~vN4|aUa&*8P(IW0mh~bcFTIUZBZ|>;Vuy%VBz59g4u0Vq_O=I_w8-?L`meVyV5T;K|T5 -A}>6X?XY5Dyf34YGI)O3UbtNZrPP%gOAo;sC#~#lZ2}|0bI@d=)E507wo_IX7t@!h -lG~G)_HEn7%nK-^l9T-J|Z|{X!HPW0BM?3C8jTr2x~}%Q4C3D=s#=*#%-@fTbBDg8UvCty8u1UBVfHl -cY}Ld_rrVMM++H7ZCm7H+14uGlPf_meD5l)DP6u+;wz7+vp1beXt$y$yP)h>@6aWAK2mt$eR#RIt113O7000O^0RSNY003}la4% -nWWo~3|axZ9fZEQ7cX<{#9Z*FsRVQzGDE^v9xeff7AIg;k@{wp}1owdDVxp;__Se))zl_lHimXERJy1 -Hs>lbJ~}OUXSd -7j2)5&l;He%=eBn<$UccpV!q!$&D9B!GY# -!lSWCwHJ{hWouRY2Kh3hxFXH=yx}5{n_b4cHuIW<^>OKADS&vHe8qQR;g}3b1dEjRUNq8GaNq7 -(@MO55N_8PL@%a6T=tXGrSy}o#b%Hud(dhx++lmzK*elQr0hmOk6DoC%)+bWuJy`)^vm6)G$dDYq?%) --PEpP6=iFH!ub$xWCQ2dYq*MZTk*cQvNrTZy`igXBrkn}@xouwtM{X@DQ2@KcP;=YRc|r<&#@KTnEh{ -o}i5ogN$FgGHJ>oFyg6EX>u9yyqoG45qSgnV}9Bh6&l+ffi|676*B_R`VfFlN}5MQ6-TuX-4A7?1`y1 -yxS-RP)cXjKUZ}x)8ZgZ7dvWhOTXy7PU{BJMQ;(sq0-2_2)$rmI}fP4r&#GFYC888X_=tL#9OQC9wZJ -;`E|mey0_LOJFhu36?ug+MvCt+KZp`lPjH~7r<@&up0Qd*3s1>PnKjd26mIK#l85o)z>m{hS-68m>@5 -#cQ|*%nR?BttbRFGptD;rc&FE**a#bA6)x^09g=ITyYt^mkBDD~n{Q1fDk?+)gU50s~jQYXa%dQ?$`@ -f_1-qSaiZ!da58s>U9X$s}#rdkg+S?Y&*)mvRCD}CKo{^HqI#vZ^zp02}UrRxmi+-P~H#&)Q5%@3DlJ -2={ucf{E{UnZ$u;d)R0?VCS4bk^0THGADHeTI(E41QPx+E#s43oye^{^e=l72dP_i8Zkm(|XppM#ar) -wfwC@3{{79Go_>gTH@Ivh~8OeW>(E~HN9qBHuf;~WzAjhqLte2`i;NA&Wx-%r=;P;MKk3;)!x -esG|9Z)v;r=Nq+G`y&lh1$Q+peYq=8#ti{~4^4O=%k1;)*&O`&!S%-9$Z<+`z*I@By`bRYl!S*CVHFs*viI`p=|o(pFaOcWMvH>=_)&*7 -%maA6_R3B-yH%w9N_w_SNlC_6NqXxZ@+?b}FfH@#Dr9q`2Vk53q6JYaj~S+X2V+~? -sD!h_!7#$>J^h!bdiFfapZ$?`904LrWgp%~T~%WxqqW+;sRnI@*jamHKediLZ7O)y(>3(MIFyTrjF&w -i0m)7$&yo>TeFA6I}Rzj!W_VpvPqcWpT2Rc --#Y{dG$>d;qcml_pMIa^+$XR+!@-GR61&7Ky&MuFRU)=?EbbIIUsjoLL@R_T04@!VD3DYsf3^F6~NdR -6^+*GP}TgiTf41@-57XP$cjJ1w_McuYmn@ZE_WGN7H-cxgPCC}L0lSUC6sXX4PU2SQ5eA=dqG(8BtEVhoII;JfT -L$w7=(_TePcBkUFZE<_`wWa&Ip1Ew>gZtwes|WPU9xpFQH&v+JTZV~$e=vA4*i++8l*UR4bEOX9rZ;% -b1GG_#>N4{-tKOgr(y5iAZJ<{-(DUM@Qv71I-oe7~AHu@$?_pu|4`E^S$5}Xharjj%9ByIZ@Zl_Um$& -^Eja2t*?OBq#%=P6e%3FVed%IG#Bxz?N63?wq)AC-0fYk1K3+D~F%g-^#2C8&#qgr-`wzjo*Fi(T~zq -3(Y#rBr#ok+9f)otFSX^^I0;r8nGNMY(`_3>bMr~QcPc)qT*w@w2m8>=V?LcN>Wl*L|YSD6#l!K(Ifd -v)15?nW)UnpvdTdSCUL=0TNpU4{q9&*^ae{qi%4^Keewdh>Vk;fHbjAnk1`SLAMyuov|e+N_&*SOUGT -rxqPur}l1FI@FMb*JYGJPI*u52zrgjxpN2}(EG2Lv%8%At!5)jZ~soKp=#JJ=C5e);qC0))hH|OSB9b -fYun_ikfcyeQw_XoM`$=!m4;;3n$X&&oh-CzT{}3~V@vk+(NtZeSIX=e!8)7b-tOOm?rnVgXv$W1G*v -51vnsp#z0W?^debiJ+w3XvWNi6X=}VqE!uUFl_R45KKh>gKM^|z!%Mj87Eag9_-8S&L$W%4?)}VV=plyLx!(S4`L!vSJ+x#jS+wAO^a4jjb;0_ya7kgXIwJu9mdQVN@* -%{%W$+brwUorAuAq|>Zd)nVMy@-ce>%M{md)hr%Egqo!U%`f)fe##u|CcQ2iThx^#9VLmO%>@T-RU!u -ZCPXw59RMnSmP%CNFBgJ;Bxx-S-L*3HB0}KRtUv;6LF68#>^#R90k~X7mXHoCv-> -SgwMlQ$)T(?W;e^3uQ3cM{aRSm|pM` -b=pN-Gza+*8lbBkrryjQI2ag_3-L{i_N#+d~OXHU -+A{gg(p_NK@Atpa*oN>XNxp-X3jfVxEvMX1H)_r{+Ng!K$kH!iqO91 -{zg4w8g(&}4i}2Uy%01a^Vr3nlJ>Nm&dsQ@dH&OM+PKvdcU6*0Fn<$|aO(lkYqg~8At|!xYEMTMO`7ZxSFxw7<>P17fahveh -`jjgM>`0zFyGVPhnOWV3-@S_a;GDIpQ-TjXrF$})lOwU#eOZBFLRwyxQ8fC{i{0ndN-X8itMJ?qjPGL -Q0z1753>EfQh;zzV}80ve-*(pOUunZs`c92C&FMKJ}i`ryGI3sXitxV=w=VWo*oB#)FoWc!#zD&L}47 -{`)0}_T9(-!by}q9-Z9iReh-D-IX!@6FUzaJ9{g|*ezXUFxCcMpgP-id>zt~+6#E3e+K1okbVXuB{#o -A;2D+?2gR@PmgdpxeZ^r*Vv=p{`h?GDf+g6WM(xv6et>9U3AUp-gb$6i#J+m -YHi2(#ypck!w@+S8r&p4F>5U)e*FM`|l_x0@*5v!Yj?{T_-czwaO56DwJl`zVS%OJU;eF;+?X0CSV1_ -3Yh8)oe2VZcl&Tnz3iG(5WeV4Av(0qGYR!4YGaa^C3OXLx2BT+C;$ixN-M;H$V^ABklJf_HLt;;P$&= -gUO)&qqSlx&cl^w1FUZ!5a3b+8mekHp%YAW_ME2DfL8`URGT -c-(ZH7GL-Vr@{duvkGjTLp|NU0~-QwbCvr+vllc=a-Od9Y(0X|oNmZ}c%)z+=$xdDEvfXsJGt<957tY -J(WM2khKEMl4{cR`1Q4d`VHYAC~C8MeL9e{(O?;DlM?#cD^HmYEv7HZ7+jZjc{a=G_4h~hcZfeIK$gut?Bv^xPqo5W{{qu+Fr=iHN8l9zz@~f -9M%&INF5<~)eu_`SA*BH4`ZZ%$r9+S5(SY|2 -Qdrw6XV${mI6DVeHQat6w9^ZbC2Kth|b%0i09-n@aCSifTa-v=#Vc1vVur{kaLzuNESDrY3FGDfRD<1 -iNowKLNH0y~0n++CtSoUsgW_MNnqIpQ;}QTU#7$iW2>JSYDIW&@(_S`h}{dYz$Kid)2}kTHX6H_A)l1 -EzGu|Ej+y2sEH+M&svzFRUTcS- -XTCwt)ME|_3ezp{4w$N*`TD>as(ER>X{cdUQ^CDcw%7m19|M+bSxo9B`UF8y2uLo4ERX@M4e;SHx8P? -if|M{cD+yi4YMEeJr#rl_=#WzuSJ5Q@R^`GCRcZ{hkYmt}ruLghTuar}#-}uoxg0xbwyO%ZWQwwWo?; -W)FTWG(esScrSylPzZuahMBpnmCdq}pWH=V8*#d0ZXeMSA?otUAg9;Pc0e$ENVB+P!o+N!5Yax1L{=) -y?kS)8+_G`>ze~d5=A@8vX0D=c0V(XK8GgZ-5Ud-z?cwxPm;OWVhLewwbSV3M`kbAP*>6yV#;=4SGn~ -+6m*Ll?%4B)lSRI5&uX8 -VB1R~#vU)C*$qt2nn&i)WajFc_vu}IXx(c0vQUCS4_f*-;MReCts3xi`&x)U||J%~oi%8kB{OL1&$g7 -#8%);(=>hUkeeEed}>@Q`%-+$Tu>KSnMxZ2iKhVWcfk`>yu-mzJ0q6)3$tKtXkk7|$K{#en2WJS}Am- -|t4k=}-xuk!%8_WF5*7Zt^CaZB>7F7kP^*;k}&Ntz-T)t%gYM;kT4vqwR+j0z`Vjd}KH72ch0B~r+ij -+h-*z0^vQX2DZm=|}bNi>hZ&pFOJ-#c$r>bo4@v^gs9BC=u3gyfwq2@6I<@=T&u~atI!+Y8o1`r?t#i -gI4;hq4{fM{yH>&jm=+^$B$Gebf(ZQ1*i={yW6XeV%n|PO_i`+?X=+HBmbd_QMRp_#b|E+(xKh9swxs -`9912}YxmDO*ksx_Ssr!NxAg}E3x$5?q1HfOzuVyM-IL0sK6&1I(uTC2=n~nEB31QIs=e-$=gp65N8J -9pTW{Jw>nMEts|mH@XB||<@1~xL>3OPF7|*$yDr-jkJdV^(Ry#P_lT#wmR>Mz~wKs;a4jals6-fXZJq -lK}`P$onz$=5w3uh9qEYeNv-9LU_@AuWBNcQ(83c{}|(dHOU2qo~jr1tTL+#HNa|XOaoH|_`3qi%emURfXp2B -S%M)xw5OgX@jVLFH!&Yl!~~yN2&#B}D*H8UHV4bwi|F1hQop1zucz?C*fc>NQI#A9OKsc4(g)Qq>NH9 -MnUuu?c}4H)V>oN>h2A>WsSuj1GWyl4u2Go(P`^uDJxi=_qt<5sx5n=OR^`i?w^UZ%O<29^t53CF^v( --ak-kY@r#1Rs^AfR#wqHDQHPo&i$D_&N^PWEE>)##sd+NW#{>jT;@9gu(_q|W&uisTvlks>odfreC;K -h7?cY5~f{N34UUDt4MZ0Z`;bq!8>s+xDNFV1=w?|(R$_WtAZ`^%Hd^Y`z1Z%;n{wJLYmAL?Rtxsfi{A -52yG3q|*?_v!7)#YOMp?DF#LWA9b(hx5xfy*KaQ|FvmyI5bTjHcg)OdZ+I{f48g27oquZEftV%@y%N{y$?Ml=A6t^KAvtPD28I=)JR0=T-{Y#k!Iv#3)Pevw$Ydbdr}K_(r*R(^5nz0?r786r&_ -cxPd>hX*ETdY4Sm-%G{tb9y}CU3@c#Urs_S*{q=GplJsKT@hnF=E<6(z~^VcVrpFf`U-k)_9?sS{Njg -AMX`>d|}=yXf}ezyB3+IY3C4W^p*cc}iPmDP06=%E@~Eu>apPBk1HnPTmXQ6m0${`$@Cazmp7(@|6IW -z$@vaPDFQQ>>jWr)_gP%b|DeY*Flf_gSf1tCa&(-As^y;_U35()>?vS{gH1r_Br*C^pXCg63!hm7O+o -<5cymwgQ*u7pJv?93LK|u6h;>G)tEsPfpJ&&UqS)P-i<022T6pm;_@;x}5~DX_e$2Gz+F+p`8UIi-jF -?V7g-tkignGaOUh>X9kRpCOuR3Ra5qO%l!GNE`54_a`y2|+ePKCstxp6v$$9;c3jM~gT1W(@z$IA-%X -lVf9RCAS><(yDBpOxCuea~oV(6=8n=)cqNnFM%#_0VutY#^bt4tSoaeN7E6`-8sOO` -Z5r<~P(5IO{_1+X@Z`eFI%&HT1LEoDYI=-)zcGmYG`7n_Bu3I_rRSyu7V`GzH^p&(FX&=#!v<4!yD25 -uT*WGyyeONjD3@<>=U;Hkx|sgpjf4cff>>7%rMBN%k?I_HVhw{o%PE%)We|>$jkR1eyZi_O&K=p0BOo;t(yR`G0g -=-f<}cGpGJ%7GaW^w{x<9$jzOk2x%VVP6G(XJdl_rF4@4M5t%4j$=&TDib6i()40xJENr8biQ|@Ju7A -RoC7cX`7aWkrSb5v9%)1J0v?&fHEk-56Hii~D>pjBMPp{YeNG@VM^+bAh>U?&p$A!g)QBCDfbV9}^fU -A~Q~Bm5#QvnCHDhERq?s>sSLzCW$rrTq_`@NCS;An4Lr6zGlq)CO8O5^z(%J30g+S!QKOV+E`AQV!y9 -K?rA$hL{d!#;M`KLZQG}8k9aN#Trl@B!=03u^CJDGD~wKd5_rZm{9Ol(rg+Ll&9ID5%{TVQk^PA%Dj~ -^b5j_!)4z}vK(|jnF&Gv7%wWU*skdB)M!D4<35X0j?-4j(kuf$ydumPb5C}5HAP*tq{t-dh*n-+NwkQ -#)#a6j2z89a68!?Kx0U4c&|>l9AF&?hFHmem-B)6SAI2d -cGJ-Z8<*{Pc-~v^djW9KfpH2TJ#S>s(t%I(Mrz<~BLWvh>z;1Dan`T!f3lqUd)iKI-fei|nvVb{U2)W -vuba+H@K_S{%Enx(yyJye_GWY%0lItPdR|#QV?|OdfTQ|X9WV=`r6r*vC3Hg&fdK?&_ko+Q&)YtP3S~*9` -6%dw19^oY{+onObMOy|bXR4!Ayg!YT!f*x(I@fNzTXPx`vxCuL+aGs@$|T#%@F=_K<&CN_lwwA@(-jA -=jHu8MkD{!~Z@@S2QjDDW+nr<}u3_HU)!)6_zey$llZ -y)tG*;D!|Bom)&f5KuvlcV$wY? -Hh+qCC0oqBWfk=^s5k?$MN56!YM#1{OGS6B1D3_y%Crr8_XFk`#_CW_}u!$0X{8lb_B*vAC`XVDk^Ny -GRe!nSgZne&Dx2URB*zoA(n-jwUgebWEJ_Jxd=V|-{9)N@{n21bF-|6&57swM2T&)@U>G}HE+7tlaLX -B{xDlc504x=>=ytQzM$yI`%)X^7yQo%0&$OX#cvUb|`#%OiA3!S&0*+5I_;HF&IFM|C*N=NuI932N<7 -=8Vdk3t!l4QGNPb!qr{}R&dy#j5-7&*3zG#2C+IL3~_GTpF_wx0yV!;t}hb6)zcjBMW0~J`XLl -y*U=VU1m{rlgQLQAG*-oahN5hkSp(8Fi}T6Y(55(#4${LNnwWz7A~|HBB3vKaleZcg@quzEs-yct3Hq -AZHaklud$MiV)xj=_7$<*VOX{jB@#<0TWnGDSB<&Q-~W(;w>2XknpbfK}zU(j~K^z -1E;HKsUtc*{$JnJaJALQ<~9PN0u)+uJZ!23u@}jSSve`UJnJI#(0NXV_xC?FiM(thZusjS?jmS;O`_v -6!2lzmuyq2JD?&NfDiRCs$G&_`G9rnF;nTTzdK{ol9)k@w8NztH{5408z4!Mn^h!)8$1NxHrDE&#cL8 -14EkgF1*Fw&fK1OXOFZ|zjx_(s5iI<>G^tQ(xhJOYJR+b?CU$V|aA4wvK0zI4mP1C%)4WMQxE*2 -m36XUojZ;QSTi&-(b^uSSD^f)?l@&faBwT027G0!d;AqgLaK%JAY;4t$ZIm)fGVKyL10~yPvlb^F_AS -wXPED?`K!?cb3c(q-D5N*EvohJT&`>$2LJMP!U*AG^{DEVOz?!iRCb#$oPD*PWSQ&TO63^=@w%e#Euc -brzAq$nYrM!vbz5^@y_qxN;5_Wq#47tLO;f)SJbfz_rS$*KtQIz>M64sQU-(gMATCA}}ssq-a%6X2R& -hWU~Ri?A58#>4~y1$g~(q&LmT2+pi(Bf^V&3iEHP{=BdMO&rs>kNHh2M(+p?zw~IhR`+Kt_m&Hb_ms$ -)A7jBIhTZv+5A}+e_*358+B;oyXKm_l7_}=gPV6QXK17o2SHiN8^m$;JI)&iD5zm(3_KIo@XDeY&7Os)^Ce`Zua*lK1}`vd1Anr?j{p -wU&mOQJNyySbTp`Xw5*X$d#V?9m|&_!a)M(ZUZzH)$r04xmXu5;xeRedwq?1d-Dq82#2)6DsIqhRiIGJ={jO{zHs}k?enT*f8??_#MT2r`R}uJbT@ -2lR^T`IxxA*#f1MTLeF!6Ifby#;35VaMpHk^%`&2HALV9wOeN@i01ZHIVam37WpkUrmHA*3ecm|8rr1 -7xSJO(rZSx00bLt9Qa;Z=7$i%{2&r8j2@u2n -K44SY+p%$n|(+)&Xzd_ymP8CUg-bg9L8~0VJx=P5M<7}f+iz@bVN+Xg1`!wfa8u$!U$#hWeagl1GvtV -T>vExi0|hZWe>ac?>X9J`t*GuDwzs6R&8WUeNQ8a+cDpV8MWFU5 -IKA^3*3n^k8G?tGyXp|=2NI2FQX015uyn#jb*j_gm)c!8w>HhZGa$@&eStA{QJ&GLpC>3#-^bkginvd -vLEzY+z_&1{0Eyc)YM_jsNfGB#?09NXxC?Sh#wP-cM$#>c)*y{>+XX=E2W$+lX`UpQVcihnTPV!r% -It)285p{~Ts%^#=F01dpGED+adsK0NxsdwHA*{oE0Tv5Ov!8xGYx9ybD8FM=2qGH!x05qR(4KO<;$7) -J$g3l}+L9$Xfja(d*lFx0S(k@cJxkUbg?B;R|Xc~%9cvjWaKP$N@%IuVR6HoL=*lYc;9o~lVn&b-r=%zHDxKL1!-%%M8g1|n3^Eln$k}#G5g~CO4 -#A7IwJw!-ZK}{V~Y?NdSP>`>4;(s6!o`<@QEz}&SsfF@^@+=f$S6Js86!Gm_s0C1gg<1j?TBsFJ3qo; ->MZ#|oPnd@m28>Ucg&fK^#C}pE+KcBt4gK;9ZD>X~AM)hmIw;Yx+-QPwfG{*0V@o`2M7)hhn1x9KUK_jpctX^T#;3t4=vh>c$hDPvi6Kv67bf3;{|ox65K-^XD%}O41aUAOH*}Wj$?PEQK99+BllNhH}X7j%s9IHIvfQShD7BPF -1sOTdCmy&u+dl|V2Kzt&rCWb?L9TP5GZtLn;3Y(f;fZgGBsVtax<@9RYK3l65)DoX&&lZ(8l&ovFDkG -ys;wLR}VD7wE(Z|p1^TcU|PGVn&LJLvCl@dVs0;|W-ia8rB5~ihg2E?KXNz@MG*=jVmGf4X+T4j01yrMCYD1rS$gim_4_Cr8io&}S(v)afG)GT*n`}Bj`UeWsJ)UzvO3CdZp>CWTK!v#7vTd1Pd9= --*;+B~X0A0oU89Q2hv0<&y`L-+A6}GUB>BjKO1YQavbkgX=enUfgz%C0mG?V9>b|yKEtV7 -%+L1#4;YN@PPm%y!On!G^F7#^uz0>-0X}Cq*u-goh7+3$g3saEOnv4*Ao+|LPV(Uf0yNATdKaMKK6e= -W_nCY}!_LjFXxLdh@ek60bn@~t-(#YQ~z77|9-`=Q7DmLw(~hmyp^{FWpp0f(3d%2S2+%3kH}P(ct)g;J(33F7(yVNuN`PGb~m1Y(>7)y;MWd>Z#^n`1O(be0 -)>L|P9Ay63Ng>vsX62_RGEiOltyp~Zks(W1bG!AC$4@e7h#`}8Ho@n)*|Lc&tmvon}^MiZ#{}f2)uh4 -9!cn|3l{HQLK>>e)T%GwIbPzyGUto;Fd;oK1DAz)VW{Q14}qz_ZKkG@fX!UMFbf>7Z2FcLj(Cc{qD={ -nuKwtNnHorht>F3#gH9ZF7&L;|$d8fjr=VtPOyS4#cu?Mi2Gh(KbLD;U5&7_PX`itM^B(&{goT_F@xE-;qIXoE -WtU6-|^t7)B3eku-zxbeWPAZ1B| -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 -S?!@A>xWirnr11F+ -^1n)#D$ZZw~e7E;E)tT8;Nl4PAt9kB!t7)*ZDp@3gI5!)EpqR;s?KNUfrv#l`X#D&yR`XJ8nA%o%e`n -C-a%^VB(t`FQcc=cvcZ@T=C4oq)22&sH=qV?GVh~P%tg0M@qDc?5S$|=N}b;B;&v`qULF7em*0b-;Kz1{xSgEpOZPbDKwK&f#rLJr{RL;F -(kaGdRq3UcxxxMp4$Y%-Yg)3Ar27{;Y~LoJ-UR1)Kos(i2)#Y0~#pbJA&A7%;w>6Qb7@(AK(PCf+JN~ -ViwJ=#$(vb0bJLxxq!{Oz-Y-?(U4$9SsY^mTi;iJ05kf$0(#E3i>M>?UGT-RgwDEPTt*w>DT0(8fU$( -m9AG{;L$s+zcytnszM7ocg@DaEz#E#RBShAOPdX;3*9^miVKWy{`qNwoEk7Pix4lOrIB(7jm<_U-ZE7 -m`jUo9Lf<;Yc_6vfok=E76y>6aM^bE8D7a<YdeS=t5^eEs2XW -AR89Nb5RzuIyYdc*C4o!Cq8G9_JhsS$JyCS(jQRf{5xz4=#$h)++zZO|w>ehL{bN(>nj7&nq0_NfqbE -;VKZCj618Y27@VhTG^hbmjuH#ttWpDQ=NW21jPI?pC8$I1xtxn;nx+rOG5|Oyo!aXB{Y|et7_5Q#2=1 -3iD$jk%0>&gUPrBL49Y7p>HJ|aSNf}yoBd&M}|5WVP`R6ovX0C4TG;yp+tJpF{4B<5E>}oY#s_Yoy`y -f$(gfpPRL?@x3WafTGF&qE2-E!#Rw?Q7MwDe? -nMk4POp588P4}5s3LPS0^V&#HzOFz8`>d*F`=hNV@A-G2??j05y-KzgRl(es}vz4_$r0R2);_;GlH&C -915<)RfSm%?YbxJwaA4DM3 -kQa~mT!jV~=Szo(9A_U*1paM;(7QFKvEVL-@QlQ!(mRBQCvAksw2zcg1a7H7<8zMzvK;}b%#kJNsW8} -3~QHX{dO@}_dLE{m)d4Uv%h?#kTlwel!I?7wg>tNAdOFBD-4^!(KRg==qK7 -bvkDZ?&O-ar`%0@oE3f8ZjaL_6w-CU9LsIqXn$Z;<$c#(5ECDo7j`OvspM3fB!2Y6cI~(XEI~-%ss4w -FNQB{C;#S7P-Y}C8iW&D2SD)KadESs1IZ!o|1>^%G~&X;G7#z^ddJNW!y1zsFP!Z@eZ1W;Us#a^p1x!q(amP-5>xEYWI>{&E~2n;s2Dtiu!J5Cl@mbD63JIa{e}V@8n6Dh -dc|OBlb+JUL?6tP5;DBs2p=zx@I(AjZ&om3c5>RN{o+Av+y@Vs0(eh|lHQ-gMLX`ex84$o9%t@{=r6L -J$~&>EsHG1ZqcUGhelzp9lyv>%gKVJr9R8g$lj^Vki(dP6VIh5maPYdW4{qg%N|V-EexNH)Jq%M5S-F9kvXd1a)CM|jC|Y*a@ca0JzuM#+NG6iFJHk@U>}yR?MG?B#TMOIN7#0b7*4m)J%Un -5&1W!OHuvQXkvv@#tgR!IC0F#&f!GBSdo^-h|b}JC$QphVj@sm98!ha;&+yS4oFR~ot)5 -uUv37qiqEGEn>m2(i^oHT%^bi{0~;}H<^W>&8Olj|1Qs(XWP})b8Z1ueJ7TbyV~2!U(P{jVKNT2mglm -&`Q%R9Fa>lZRc*gn+Cyu!$F=KsFEGsurB6xls*{Ex`l>asurCkL_fm1R(QyGgl@_2)&p#9i^o8SjV^*S!oM>r#aFHSI -Qc2Un2umaI8QF@-IK8s#VX7~^F@duvRW#{BBgKR2pm6ncP!xfJ+irifk53j5s`mo-L=B;wWc&{&i)Z#tAMi(l%KL2;_=8T7S!m>kpRw|P}=+VC4w#%(I{yYJtatGoR3@tHMM0-P#fTi@m&8-Ep{v=+s&Z~CH;D?y -sx1lW=vO|!QlpqR36<3#E-MgO*CP7CH~R*yrjI&`-vcCjAfqzWZgkP)+y(?&2Lc)<@-;OVX++k{^t_! -uKxJ19EafGMHKZb73&hy{VjLOxM}XT6Hs2;rb14poZVbQOAm;~rXQk+^Q6ndwuCwa4gpDNeqPf|Q(h> -`CaX3+7b{RnNuSkRieOX@9(5Qc_0pTmWZXD9IdL%kTY-0T&s`xUX5tb40LcovsnWT3w8Wl$b#E$SjVq%Z_C=bEn) -`nvMb6IKD-Pao)H-G)LlX@zy6kA!m^_d`xi8+!-_2Jf=EA{z+-=%eh3;H##PSq^vL51roud$tW?JV3` -jL2M7t5;>?CrvSZ;ezvb8LpndYSTO7le;#g?RU^@OWi%lsE2+0G9ygtKb9iaW)J7ZLDgw->g+q!yf;JQ?OA`UbX_9&Q;;0DkwR@LnL7- -1iM}y;QWDuAli19%8n9 -RYG4NoJ7^iPaG=BOn)oGJe(}s)o#JxUtvIwrw%lzP@U{o1yz(R0NT#mfqzY3YBFfeu;BmBDdytKtz3m -JL3OQ?nY9c4=HVg5BxIlEq$e6=BKKg*+xZ@8Hj`7y{9UpmM({d-jo@$il&hD_0_8mWVaRi6;j@>MJDD -jTItZ1gzT`Vd&23%BvM(JrJdS~%YJdR=1_l|GBc~Gsp&R9(e#XBB+tnMxq8yj2~xywWsP?P-jt64biI -ztIzivuYSXBBs)Dx4@ildH!us61k;px7NdgI-Il#0*Fq(BQr{xjgL~X|A-h5qtOrTi@X6v7k -$vuC31Zk1?ov}5VdEb$|(ZhSO=OM6hk`@e`*}yL{H3pX!zQ{-&Haq;yU}Rr@;m2HYf8k3Q7C5XLV3GS -G0%%4yUu>zN0p2)6nAWF?2lybzmySHm5Ps>%6Y{t=j* -TL_WsoS&#FxSu3yo=diCnGsYZcV53Ij=4evE4qLi2z{U;CM3 -EJ2(!%#&_8@dsdJ!w6Df_>N0O#yq^(2a@b)3(1Pm$O!z2==O_du3qLegm7L-RYo}! -7*S)e&A&~YGIK5)W{F*PL2G)^R$(p$&6C4R(|FtmV&kNJEbsefgyKFvP5nYal)CpD^@n;Z;GhA1+9>50m|=@4l%ss<>4Sw)13F8B8gSSG^reR -yG;Ui-*Lp4~$fSXMP(FQlrZ&d6u~DA2O`>#x -rqJ$kgs!bNmf3+v|F-c@grX1%NDy_;T?^b&6!=2|HEi>L_K`O|0rWeWW5@v0~`-+uGW?d|Q0+tG_OTY -fW`PLIENtK6pQKW{G{|Ej8etiQzaST*IxUY_?(y~4Xl^ZW<31yuXLr=RmMYyN)rZ7rn7k3V`*9tOSJR -habD?xyFLS*8TpE3z=`MR~8(?+AvfUp-~AvM{f3iY`hvWzh>%6RMJ`vW;rfwEf%1D&r?SyYs`1zNGNf -{MEEwk^f&%O9KQH000080Q-4XQ?aYPpWj3P0K&-u03!eZ0B~t=FJE?LZe(wAFKBdaY&C3YVlQZPZEQ7 -gVRCb2axQRr?R{&L+cvW3cmE1TQ+uu1mE>1;lg#n>9^0|Az8gQvo_$o!am{ihNW&e9)RNT5N-F>Tq8k -t5NrIH-kv&nlJQ4{sfJURy-Dq^ri+mZKoz2(Px*%s~QLjm-qFHj4lu4eAqIH%`^B -FlylZq5^THc!jh*h$hoL!M@mKT#U`6A$KlIr4YNve37#$`E*t`f4jB-`@dJzk*Nt_b~wFeakbS(2^RR -aq4zUYf6}{E}qJPoyv_(bD96ouo5T@IFn{N0aloB%GZ~7DZgqnfj0?SyeZ%jH`=fdP2Tb-ZBU!p8acGR6D9NiYQ?i0?-8;N@@7|2eBYZrZ<wU%6x!y3WC)moQ>r?Legvf{oC4a-a_o~V$c9aL -Q4Xpgbm3>TB6fYIjPli&c)3c+5&QM**|P{222j}_)8u@DU$9momI(fm#o6TDe+mVkJw4B7+h1pXnp_Zi>g{3J$SI$Y$ls0le}0wc=X+O-#z#O<$r`oW#?wzkB%I?EK-gdt%brN;;2@H3l(Rkm`i~e!+eZm3OQID_Q~Yg)~FopZkc-`INz6xQ$j5g^VRtsu -cdZh!;?g6``D`ijxfJoO0d4HIV+Cx3??lOE^79UyhQ2l0Weq@|a?n?jB$E42& -!1It6$#AHW#m{q-@kwP*SD|Uot(Y?`1}X>qOFy0zdnEX-CbNOz6Bw_*$asZ85jBNY(}Oz-P`h_mO6&> -bvA`s?> -Rw*r&6{=0hPM#w%*(g$s|Z6V~}32ql3Sr4po6;5h3kNU&^?Aq%3iW0cRM;VLi7EmuGY|z|*GH@oOsZl4j;Rql;s~3MvyMR+q-Rj(GnC%VL-SA}{hZ1s;k_j -iMPwf+MXiVvwrSG&*<937JhISf{`!wM86FDf2(440=lCMdVZ$MZR8KXfPymYFqwDefRVMF$^6=MVzp$ -h6r(v@!*)Vb5ay^l~CSAvT44iyuJV`&_L~nIZJRjBd+Qsi_cTS-bYKS)X*}?^dd_0MM)7vCqMzEGj#X -_$wW0>YxQ^`jd32jR&#U^EhM?~TA)=$CNzyTp>Y*Pt6c(U4PUwWOy7x4izE^kiqdo -ETAMV~sQC587)mayHVYN*dblPjPnS_%*?E$Pso$2A5=KYF`Oow`NO2uKrz9h?I?@LHq)|I1ZeaT-xesVmneCdms~ywCGqiz!` -O+#e$2nm1;n-B_o~KD7T4GKr$XPGt4J=sX-vAv&~{Zjm8_$c*0seK926MQi>d?a=&kr5`wGIlGtDmA_ -gf==}Me!ff>Wl*;7~Ppl-O44WAX2gls}VRx7|TzI!@2CzQF8Khv#u@Yz#^;*au;3d-=wa;;rlh;OhYU -G+e#Ds!;VVv#PcQ5gJcONF=UXqJ>9UmVk|j?5`F&&F4Ak-(!d$aI0j1Bo<=VQj`Kk4v^y&v1uYGy4KwG -tDDY0cU^d^%vp-@#}1aH)jEhcE!9CZNTN@z>F)+HhU;w1)MWZf|i`rgV7^ed3N~K0oxyM#BNHD>M>PV -P+7oR%tRtyf91P9{@XtP3u*R*230ELm>2^(Uz{xFEe(#tm@ub?$V8RqkY{ODR~$Rvp%dqc?x7+d5~~A -sNX8g^OJta;i_j8Q7dzy#aWBLQ;NUJW}d?AjZ*YwzJxJ5hrwPcg}=|Iz+_#8`75O`DoL+Eo^qA1->OU -)Bzi0b6Qy$po$gizxnh#7V8QBJu%1+EquN`ldr<|oF2h-$tp`~R$vU>VXI&H&SMQkMR?DH3;b(%ZPRE -ZvOEJrOi;K%1iUIsx&5hK@3YVe{sGSLh>Hz46WdTEbih`ii0jCC+EshmFHYvnyd1L#wEICw~07+-?c~ -rLwTIvuVQ=y=wEbZrC4iBABRSI!p$1Sn(0Sr@;4c*gt2m^bl?zSy>mwQVMk8+-PomTbR2577sjZ9N%@ -X)*KFknM5H9Es-ti;b$PX|CUgU`5@=<1yp`A;MREsSoJi*&oXfGuVjr^zB)#>FMwI1if!#%YQ|8X8=4 -=&qvQStRnEcmAS%{_{kMit^C&m-K^0UIfdg$lS~tLS>w-G7ecI;rJ2k4vC_S -2FwL3MaN;-X}msRi7q$)40-}euk|`E~f)n9WT?oL>0oj-hp#_YCWG`X89&1vjv%1Di3_jZF --PVR(O>svoROI$6zZPQ|%>zLzr=yr^#$h_vogL1wTJCu6Lw$jWNPs4pm$E-{C|A6yKCe?9rU+_~ixFv -y$q58NGV{T2U4_euZ`}UPz;}V*F#mqO4*_p;%!;^(K=%u%)QB -4Zh*B&eXb#%cA{zuREPn?o(sSnpZh;hMaM%t7eopv -FzfRpJv`CJ&;yA4h9<(H#da!{k*JXvLZvs5CV`MW>VTo?&g60H>Nff=AGiS1SLS`dYPd32M0nwl`GUj -m+7qb*5GN02$w2fZ8e+$C{ZM*=>OB%C1mhQdl=><4$BydIt$GC<$kSVYe6N|!}e*`$Dy~=X-NS?wW%z -5 -8QsQx(7zzMX7k~I2WZXV9=G~7nCXXL{3x`%>9DUF;taUn)c}we1!dyDY1Ck%h1CW7I<)fmR#NcjXCUr -Ioy5TV0l{Yg*}A(WWlGOUlw0njSPUh{dHOSvg_u`XEG7DUcz@U(jO20@sEFu*O@rTz!y|W7WNDDl2N$ -9K}5?VsM4lM1QD5WpMQ{6bp -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 -2{#02WGb)z@u+IL4ql!SQ;w&L|*4 -a+LTeV#{8P;%nnfz+I1c;z}t7$R&xbdrc2=-YD1~s}FiXKDI(AeE^ -il%12Nlq{4OlIRxQBz0#e1!?R>@D!a>D^6)vhUsI}93=9B1y~FAcI3X#$ -0a^dfEAFC0HIX^cS7-`7D2P&}uUF>&bb2x2*Jr79S%mWWILS~U9ZMqI@kq?*QTqu^qh`+7qZzT>rI_# -IR2n4kJt3swa3ij^@7q!mZn@|(-cTD&fDjw7$0>*XfxBEMX{fpJB-7}ztsDCxR>rIXu(w -ZnOW6o(nM+0m-k03vK4)M9ZWp%h>|EVJQSKmm8KyF~mbD=d|pQQBLVTaKiR0-rPTBcAvG -O;Y$-73<|&Usi+XV*n=-Thl?n)@V!`g;47FMFOLx#W^@zOKepnqO|mjE8uP@7gDPTe5d_HA9T)8qk8X -0C7&O-!H&1e0XeI!=BS6j?1JV%lwKA0gT%kZN#44!x7=liJiL`;r=%pryRV(5>bRpz^?9=&Y;I$hO6& -O*J~H!BUJYYtBeeEKC&2cXNy&%q{YP%V~KJJ3_@6<5zlAj63qD8Emxv^u;4Wux}8fx*t_wuY*)LOtN? -^t06U1(G)5P3wOL>)BS>OU!tJOr-8#w#JYC}$jw~M>@OVyR$WBakC>XK=P`Iw7+p+%ViWJ*uTEstXqb -23gP9adnLN;>|kD8;EHV0tFBiEsV2j)tAIFV2q13xkH^6F9$MbKqF+&QVCN;e7ip&ps4}p -o)oAF(lY)NS1*al2qsN5$>6f*q_IqRNz=d?yFdm{qiT`6BX(-rLq=#)Ld0{&JcBDGxZ3kKlG>9+4^ke -8|{t~p~yJZ=nbjWSaS5sG|5_aKKQsL9y+DT6|siVCjEdb#M`*IH1kc1j~$zz4NDyDHqlFlX>si2bvB#>=j$YO2g^XTcSAkRk8(=;i80G}}@rKfO0A -JeWqi_X)0dP$eX#th-Oi8D-aH>0i7r4*wlRmQrZ`yp}XA-_-Dt~-=5v*;+KF2r=5iJoU -0K!%Uq6mqYqsIdkJ~HsC8#>+exr5Wtl@gkx4E~%xI1V`w#0z2QMOJ~@P~uIm-!~MKrk7@!*9Fe#fcMbM5btl$vZ -{5#q)TET#8B*kd`verZ%;D)G-%fQT=jw3df8x@~pld+zb^n89^m8!0l%j>9v^S0{9VIl|cg1^-PGt`1`4%o#JxKT3pzFd6A?u -4+c+7u^&Dx`f!`n@sROOl -X{w*^zeXy;lE2r5sAUm4AZ;&6E4r+;3?@XW$h6d~R0z!2|&%nI&R2YcD@D%Kzf-30 -l+ZUY%+OIerUTL9XC-jE~E^lS9{zTCT6f=8(iqn2myr80cYjw2y?3$_qc*a8 -ng`!^ZnyPIlB%uff^}WI+uz>R58hg6?8Ohr5=&tEwXG32ax)%rm^uu!Z(+YNU-K0(XQgL{8o6ec_YQEh|3!zL(bpY5@-01mKX>^1!r1J(N^-J_loikMUKI(?>a66_+^emWj9E!TUKz>0ohbv$x9~c}1o3>TO*7drioD%-wrK=JufQ8g0kg% -%Px@jlzkBe=JvbOG?}s(_M@-yI#;gf$RU_xAT?QuwT`|VXxVC(z51n)F>8W6h)l+ETVtQliAIP9k&z@ -rvrMO2zmqIKN|3&!ErZW_rZgG2R<;04Tg-d&Xl}n53STGtNpntgBbH9#|+yp%0FPq8!<2ZF)J3$a#Cj -ZtB6G>i9CaCM7mPPZH+deCXNB-;{5%8Tr!V{C -GxIbd$juG0#g9+9n0+I5-YGsd4z?K>0I37}(=~{>215h%s@h&46v-HKbEf>$B-)R=?>%8F*Z@KhmiC> -%7a+)|-o>{XR$=j5IzcjZwUrM*L$wjnm{O%vu(iYaJ$uHndz-A(O;P+S9@zgya#HAO`4Jg`wF>5*Ktg -JjWvnN}`kK6`p&s=g}qEZt`MQQX2MRN%3B@n*5pV6=FSr*(mgg9kgCTGvBX -kU5cw$Hq$uK(MA+351wvYEgcy2iC11&CJA@}Wm;%qHi4(Xb#Nga>-K2cri&e#*f7Mrm!n2iSf4;rmBf -xCqp#><$^*^W^1biK@eDDw4cM@Tp(cG-6)I3M%Pu45u;O83oxW#KiWF%ob|2yp*ghNG$JNK) -9vQ{@-022e*I1B@6m;u*!x#W#2d*=O01gC3SZuM=J*C~f0p7;d#dR_V-sgA*k&>aYw?96Y0$v-RV6|v -NBw1w5)fATsyhVXxt^7qvh4%FV)0cI?m|CM=Byp!$j)77<_z%_#S^tT^whla|l#cu?3O$78dDOV{SNY -iB=Dm_NhbrFyE`IWMBiZXECFGvR0ATj-zxQfh6Uc^@b@a)<5FcG~^c+^kU!$#v^3K5q^`^wR^h0+qXI(b`w_y<1+Eq-@?mBe?3uWu1D_F -`XlW9jbea)zjK%41>?em+w;_C5(u0eM<5#tx7n4=*A=&KahorNNA!U+l)lo>s+;K33m`L^y#~?6(lvn$6?y9gFdv(8#eAV=(pYGe|+TFapZsv=kw<11~v2SZZp)lFs_M${U4 -YL%Yx?S-u*!BCEP*{kH=l`-+_d4K}u?C-2a$?9tD!Tpj^5_c|%Gmiu~e*=OMwy&ezcw@{EYZEu$~Q8E -)?_2>5wG%Su-?J|6-ue=cdv!70SYzFZNin_@(NS2V&p_EO{d?B5|U$cn^FRs@$uiFgi|4gSl_38O$~exno!#lXh$ErptEPi~xPO{s^KDDhs -%`>Evy2AJggs*I1%s7VKsaFiUk6H$67cpVp~bUE+Utq|BW&CpS?Z`7%w{GjiO(n>hk@XQ~YY_Y$Cs<9 -Z7R-!Z%M9Jl@AAc#W{9VF+O8vvvvnP?( -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 -P?e7c@)==J4U=l!*z6%eWw}d -o!VR&X#Plnp#v3_$v|%OJg5TE`5Las)S>(Kct-~M+Hg;%>wOpFIJlE>K>yV8=55 -@a>28<^=84xR*(o$o@q2hX+j+%FTC1Tq{A8q2?=;%m_brhwZHJngTb*MXSKSS`fPc(4KId=djZV@&+p -Dt7rWAr)TjTJg(?Om;*uH{o+mE_28d;#rdE4I4P63{uM9;G=gfEZFD9@scd;@0Ul9CF~f{nhsiemnM& -DB5jAJ_=~X&%?O@=-NF;hZxBN(&@z5HKP6Tq-*dx~!w1O5Q7QcGH~p7B1SBYg&DAcWkG7IbAV%X#4Ss -j{bI)r9)QN7BAdBBy)YLM>|o3TWkC9L-=$t@do%y|Hf{#I=`_SQQ}&TDE2A@^w7||@WtCZ)L;@zEz&g -j#+v68AglF1BQd>n_35$Yp<7kv8{LZ?>v+dwk}$W}nfH9;nvO;2==N(&DDq7|OF}dR33#1B+MZg}MNb -0ml&_aR)Yh-Xtm(k^Xn81_Oa(g_3IJU__U=6!5Hw7xra-QI_VErwl=&UQezPImqzxfd4R2aSH*r0mcF -`3Fqf;D&{yKP@{r$u|zfeU!x8cB$+nAOQT7AsDcENj>1+z&v@m4zYiFA>=%w{S3x9(}zLlGD+^U^$TeII`pDn!yTGdk-XW$yeGr3Yd-z*rXbp1rAhX7?$?AUp!=7Kn(oVJ5c^ -)9(SUTjKx3Y6cM2gUK0VSffv=>yr|{_-^w~gJ*JN1Y(=~}xcP;LkZ_F?ki1yBh%f1~iW8B^~5Dcbq!) -LmC?urDxFZr}BGg1!DI -ywkUwNBHm%dWquN`ybvA<1jajD#k2!j6;bqylYQUmw9WJt}N>Hj0ziaPAfvOI=k3t6(~hYGbzRh7>%Z -Z_OY6kJIXlW+)GaOO^5T{Z;(?tdHc5*2Skau8FsM`@4+ru(l90Z??qA@S3O|c2^M?CT;ZlkMRyq#?Mu -18G?;=~k?W1~?1S;x#bz5#3$0m9-c(VXmbn~OJ?Puw{Rh#DHSE*X1tdlRD!N-_#RljmtkykfZ|u2`8H -AZvY6}QRro08W5u;Bd>P-CVV$AL{((Ar?{7rae$^@ml9PTP-22m9}wSOK}5qea~rYm#D9=1g!werF8_ -@=k1wAbiV_vR*K>f4hal`6N{r>fnU-YhhwW2X}!*6z)m0hw)eIR)B{DKHlS&hK4 -*X(R%qB!*6WuNG^IB;j=UYGYvrP%YtlK=>_L5YXcbX-X4h6@PbK`#nBCc4UBl;T>QhT9MYG -=v3WtwUwb@lq3{aJ;>s}}~(c0I!E;PijWG%d?-Jp0$WtUmqi2TIWp9I<(q$4v5qGJhV#X|+hVs|!%k= -=UG+dvix+9oo2B?R(Oe;aS#%5Z`K5$Ufd$H%pdrMW%V07g(45eu=-s>MnC;nvoCQ<^ho?MPbYqh4K4q -04MTU3yf)+tSC=JJj3@7vEwMss52*&46+$inUfdzwY3JKhxV*UzOZ_TpSSEmK<(u!&nR)Qv64c-;Ezw -l<1k1ioo59C{tT#@UFGQ&(Fehvg^pvE%;#&m3X;VI@(Tp=J$v}0co+bsl&fVKuLP9(;oG=vQ8OqNm2< -E!5(GTKj{I -nDxU5Ke#BXJxt&zM{%iu6E%Q{TbK;DqV6EpCqmMsi6G2IA2qiITf|)-)(i71el-y3(b4-!F%?g -r8aJF1=|?$`^WZOF-({Tp*CTSHfACOM{N@EnRit|V7tgr^q&0!e3r$%~@V%L@((8z7w=9 -xFsYO3{@;|Hi>YLhuAN#*+GGtq65CL-*nP6Q@Epj;W8dYH`=Tr~#oc4e|r6F^2Nw($i@G$y6RIX)WB9 -)lu@s!Eo$xo1VwP!BRYMDwaNs^5@OBhd?nz)Am9cDpx;?Q;`#!_#8b?j@tuFh58ce0Z21TBi}9I_=G5 -4@p6r91}nT)@`R(6_^G(5X~+@%BgXaOu*9V2OPP$twiwggt48g>q40VN~`imc{-r7>%}q+aT{jJ -8^geJDt0I5>xwutm$P1l_Mp64G&XN5jh-gm9i|oAduvzY)|K|E%JRSwA-i@C+}&z;;@5fyfr>l|Z6oT -s_)goP0|MNJX1M{e^G!A*UnrE+8)oJ(8b^<;dYyQ>Jos$PnB49&AASfJSo`*TKvpcC6y&@ -^I`<1wQ3c_)84%u6Pv5{pNWd!CgF1AiM!bYw;pEuq<2Hx1@XKz()Nq*E=*X0P`kQGUviKZ0-6ao|CAB -TE8Z2@uC%E;yn=gyS}Pkh{05bEVKKBg(bjvW$~%Zo*eXUpSTc*w$uwS5%Exqv;P;x-M-huX -!5n2_p+5yN@L9y=Km#J?a-w1n$~UwE_)cTGHb@b~Hm-CV0SW0&wB~jtdXN1c`Ka%qX$0<~GzG9>&{oD -VOf3t>)i>yeBWyzZIVw0<+`#R%YjIFiaJJ_tru)Y?KOC#Ejlt;LZdazW&`yqIHIl{%kX@$;kiSC_fel -E`%KAT?PHKUN2nLmwN5Fu=HjgZ -OHvXhE%NET!IP(La}*~3}tUxI(C4oTN9#oG>o(D67E<6AWH0;gzgHH=J{w74+QyKyL503i{K_an(IDW -EY@AtENGR=I5$%Y3I=z(-p0yMb=I;EpR(sFvF%aXKq7u}tBUsimCd$Ie%B50w(qYQqul$R*rGaj*Pm{ -TOI{CVZh}$XS+-^?O#5PI*K73K!Lk=@O`GM@HA~v+YG_Zvgbnd~&#{EP+MQflRLv7+n&?qu&~qkI|Z!z&6?$8kIf?AZ!GezvdFu@%--d%e=TC}wertj3#Yp#o)jc$zTT6hc@V9(X_8e(Bm -HK$FpHzo!2v!O-))$9i-uj3MUp;>)a$`^ffEMeP5sL*_>$k7gJS%QqsxjO+s3SBQ*lC_WBc#lAy;Va};@_;7WU9SJ3b1VGYN%J#(7*NOjd -khbxrNBlCR7gr$_U_jV}{;1*Gyu^M -QAJ&vvtvOx!1#{2GrtHk}!;A|`3Xn+t7c)}QYek|mnmTZ7+5z4g8{w(Wa0)pi-fF5 -UttlHPyatqO9KQH000080Q-4XQ;2if;kpn20FONY03rYY0B~t=FJE?LZe(wAFKBdaY&C3YVlQ)La%o{ -~X?kUHE^v9xJ8N^>Hn!jWD^Pk8QCnG#-KLw$mUnU!_iks>O{aElvlDx0h=e566v+~#Y;|Y<`#lE$Nq~ -eX*}F5l@=%t@gY(9D0MI;7S0o7LWl`oV2nb(gXc$$nzneZ@;SP=7qI!5)jC*ktVBK;OyXqa1-qLP6@=3oZa*!16rJN91u8hfA90Y@V|$rXf=ggdRK&( -?k?BDQX1klt-5_TQRsdBC|A&8BN|6Mb2j+2iy`FV0i~PX$W-0HavM4(SkOZ;GGv75dce<`imZ1OPMqF -K;wHrJ0d|@@JcH%WGv_-Uh-}=fQgWzPJ-Xdv|u%tR~ZYDLqVp*^TxlWJn>s>56RcWyYU9o^NTyrH6Di -M48&SxJZ8T4#e)G`$pEffZ`eljfh}mbc^aYvvis%TJidAb*9lywaD4;Uw{X3N>pQsq4A+~R;wJI-#Ws -U0yvgC6!8N~mJOTVCfd2&Wp8)<7z<=_3UwkLP#R+h6@&>N&;i7N>jgt_r2ri&8 -qRagL#^h`w>|ao{$*AfnQC& -F~pjc@Q1^R)) -=ldoW=zVsRhxT%MNM5hyr${HkB(P1n~h-M2~u%9W -~Lh|>p_`$HSsA;}=Tg5F-n#TiN0`02y-nG|2}6_05iZwT7tC0%7_Bl2=lz&k6#v4y8g_RIPfIK2m7h} -fJgi&dq=4)qBXP*^{)qUWRGzt%$S+k^(D$x$nur**CvTeu${EJaFSmxpv}RlPr2|dL5 -0JoEeG_HnBNC!ASyHYbzZH_5aXR~rg~b>A2u$Y11V2HxyKU(d1VPM`&ZKeDbGlWHDv8)54B-%WYr1V> -+MT$#G9AifS)!S&Ad`I2JdZT$ugU4{uq}Y4vpxK^L&?CsO$pa|OVqBXx7k3i4{d~vWydtjKrX+&Gqfx -kW@yg_+@{1($c_h(7p(BZY}nSfgH3R`iUEiOU7?NVtosyj;_PK;81cZ1rSF}24x6+oowX>rRcP^c`1n -GcdE^E0m+TRR`*J$D9S)7T12tem{HpSR!NU>JPNTqVDtf@SQcccphyiwK=X;%a;ElG%h@p-lg5{jX{1 -4{yBrBmF#gc34G5qIYkp}kT$zuAqG+`t1r$5#3d5~s>&x@Vk1R*&k`a48C{VQ0?|S~6 -j0vqIcJek;2lUr6Php7*olO+5EN;{t^tlXG<1y(VM}2`Rgx&k>RT7CCEx_2_J+VGDA5H%XH>}lfTgzA -B->2F7#>XSP~Wd(J{vc9#(%y3=|ioa{0@fFg1|fwBm}t%7HRUz?4psSVJ0F}5Q>TbrWN?*BP5R)yheV -zxVRv)GOihrA2O&RK^wX{mfyC3H~d!6jjpQ^(i(xb##Ev62j__6;(V4yn~U@8;;;Ox^W*H|{8-&MmQM -%68cE}ECb|(xSG)+6qG=oybRniPsH>3`l_jGQB9;I8Ngh3 -Y;fb;v!^mOcRy@BPK-}XG=O`1rI?lme>t)o}k5p6)UJzxnPB<7K{N|@UVo+T`Is<2~B -{ZKbo7$96EeWAqRCl1fsmx*cZP!%}oq_zn463;#merRLCN;Jbl3Y -HDgP<<{*fFcUOh8yE_P<4@;J~kVP2ZWT^q2Pek)bj>Nh{jRRCPexMs6@2ijaq*oOUWw$y|ofGm&FtXr -lC|i^onl>71=1%zTr+9Y}uEaxB#knCYq^y)&!v##o`Np5+erd;b^J7B`F?o7d_>^fgULuCN10bA}W^=(mt{;04piDDD8=ZMk{PnI8xa_XYNZ~A+*q=bc2Cn5qNXtA_W#yP$!;Q~Be -ZX}Jh($aBj5Qv)Xn`vd7Baa~L0+PJz(1O$;tZVx9FWfE5LBue$3Z8o*y-5rsM3y|Ed{L7HI^?cn#k}R -Zy<_6WJSLfwh0;(W8z;UQ@Gp;DirA3Fq4e*38-o1{qm0O7LOszipu3OiycJ|eGRQF98cYsa>FHdxWf2z?0zowOs!!g94LW0>FkoyKhKr{g*_HDr!wPvJQoZxL46kPM@u^Vq?tVB_Nh~5S-gC1A3eBC}PQQOfJWZF- -ELqetcmGu(z>$3z!M{89Q;=&`B5ME11(9Ize!0HO8Wd(xXx^IVzQ49F;<-o*F)D5=4_wGFVuoJVXjae -oO -<>MdZooAV;TfR7vqP8=BSD%fYJW=mx`0b`^&bcU@dKV#C$#vmfWo+xJ3{^F}l*kdMRb%3Q -Nv;(01KOpL`!q?R&IWq2HWjfZqgS9Y@3-K*vxT62cUMB;mg6Vd>$<%7H@6{#mkSw4$fGF1>A@^+a00M -%R7bQ+tGI${dxTppog^ydJ9g;(fsXCk-MwyE?A>I5Rs{dHo5|VbP10iN1Ci$=2SNp9U -zl&iCitowbs$`0Vc$-u$wt-9@Mg`MyVEsjK%>b49h`h&BDI|88y4cw(1lU)dE?oo>t2j62aJ@Tf;+t( -_MJ%+ibB=wq?4K693morVQ{Lahhc=B}=oa)LKgcowJ$f(-{C{2cnt`gi=^@-0{ohB|QKs~8Pyx!y<@rolxlrxHU@KWe* -eqb^#kF&U`{Lm@@74kUSrp8ac<<9tLUi}lBJ~7UtcTfMCM47hXxy7ygsM};2w6}(ZqSt<$S|g92xn$s(bXX7DDB7h>qhna&u@67niT>b8N0XZgcLn!7 -ua_pP^Oe$4xCE;(T8pG^D>tQL=NL&Xd%vUXvT(TM -aa{n(f=ZdYy5%rC<{m##ZBrmspg*g%ZA?g(H|pbTZvGC*^~d)gC*&=ZcZf@s2cpbmip09nIqV)(Qqy- -rtSbn~)qW1A%{w3iNA&0$aqpfS`n-#T@7rJnh2CtPqyBw$cV7Bhc1L?7FpGIpTyyav -%0d-Z9-oAxA*s%~=ZhEGYg+k={DP89A)ehsE=^P_sndD25&?m<0)nB!zH%rTmWOXM7CX4EqFk80Ksm!;Lf!rA!D2)tyJPc?2daPGF@ -OT=e0J%l7hgUlG&3d_R(K?s3;$jD>Mpye7R#!-dm*Ab=ft5Ip<+Of^dN34p7#-F!Q$cA~g^Ts>(H*|G -Rd(h}P6MP<6GaaG1)|$3QbR@)UJlqzre*DcwD#dQ-}jkYiMMM$b+Lms5WC#e+kt*t95##KCyh7HTK~H -V?$*U#>=?zB`6HNSSZo^L=;>LI9B;8(zY4bEbD`_%3Obstl`;DHWfEa2{LD&tYfZbeaCI -L7|Ibve+w1w`7viF+=zG-oE?r6Fj%5-huT}fH^oUP%hjw(-+o3r%pc!a-dPMRBNxwQedLfREE{x@ys# -WXt9^6TIuJ_zC{DGAO{hD}Z&h-|A(Sg4{Yq7sp6o2G}Dsplbri%oR==OGL5>w~cAVv~{8RLg=TNad*- -K+6mV61C}YTMM;!@ZaKyt=cY*}60VgFAL}QF1BGWqsTw(T8I9-y-RrG}p(GxS2lCehEm(uK&Qa?Aqu^Z(t+UmpAjWp!_9Nt7x!fD*#@?874{%of!bASX&YlPEEv?}mJB# -}v9S(<%%||u+)bzs1H}yV${!A`^`Q_s;US|JUKpw~cC=jw1+FZf>*MhDZU0i9#{$P_et9AvsEvASf>oBoUu+!wKTt~p1QY-O0 -0;p4c~(=>c!Jc4cm4Z*nhabZu-kY-wUIUvzS5WiMY}X>MtBUtcb8c{Pkd -3V<*S!0vrT@htwpUw9KKQ#&Zsb#$BH?^VQO0!ef`kSMm=oQY75Y+f;}#k5tXk9*wZlp>aTY)LlTnN%u -!&;k(O$B?f-o?IA!D5yTi5$EnT-2yjIO9KQH000080Q-4XQyCiPfu;cf0QCa^03!eZ0B~t=FJE?LZe( -wAFKBdaY&C3YVlQ8Ga%p8RUt(c%WiD`eg;Psw+%OQn>sJh$i%p${^io*ZLk|sv(vU-LAqd$X?TTt8Gb -1N!O8}?-V=6eOLc{6OhtDtwL@hwVg0+O;UM4(|MA -Re8_8gDH&A!2!{>gK@sOLd)b8-e=0_BqEEbFJ -H3w2nfLbv^MruzGBAR1ejKCH({KkO`Myo662({U-AHBngDV-bW25VecwT4w~ajj_6@6ZLlZH?AQAIL- -nYv8^^Dw3}X5^=d4HA?XTGw`y^*1HXI_>aHY__D*R&5Tn -QpJ8_yAOHXWaA|NaUv_0~WN&gWXmo9CHEd~OFJE+WX=N{P -c`k5yy;aL@+b|Hk>nj#MQK%ra*8n=CMSJYYw;&*BX%#O@lLAR4&fj;bhegRw`an^hEY9w5W;ujgXHOt -y+lStvlt8D>x&Z3nt?mQL@w@)N*PI^8n`#e& -|iUP)7Xgg@J(T@>Lc<2T)f-e!a7SPL@x2M0F7oK%O@%0kZNm!;sIO+#YZJ39dZ;*+>C=*l&2I-Jau0; -#zU8`)yXk2Z}vs-t;Qz|44>Xty!1XW{G&PPv)tAu6tjX -LmXhTDF9(zyI~V*5g`-&C=q6^`7I37PiS6C4$_6^Fij~Q0N>t3Ai-~ec6l#xmf6113o3qQ_^VIM}Rwa -QLrgC#~5m_i-j-)Xe_5(epgPSC*iDF%;nntYJ@>LDNhYCJzpJnoc!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bUtei%X>?y-E^v7R08mQ<1QY-O00;p4c~(<^ -umljo0RRA(0{{Rv0001RX>c!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bVQg?{VPa);X=7n*VRUqIX<~J -BWpgfYd3933irX*{z56Q$U*do)#?t7$nWrc9F$` -c#Kk*dF7_h?_A_E>%a1)bR)-`nYVv`NNhRNGDbYz@mis({N^jf1?sYgt7&9%WfYFTJ$-TfXriqw6!Ef -+=6Q4a>0qj{pa(#iHy0TN|d{veU$0cDJ13}dv&1cHM;#GDqmJ&MSjNK)PUQ<3S9>sEp@UI-984wOc03 -HXc*t?-WoWcf@H4u@~S)8PPG7?N=Pse#{=YYB8CgfNU4l`Y?M5ORxC%IkR}`Ofye9LzKDJW{Gf4?L65 -{DmUtLbom)R6Y$O&(~(q+nBg1*0bk-Tnxyz>^|&esJ_i+Tf>_AxEB*!hbh5V>*PKvb-!8WFIGOf@r*} -e`$r7H7jM)zax{qSw06x`V)Sl}sS>&VT1G?h9G=L7_3)}w^BzIN_6uiMsx#$YxmEd-G4d}wl{J)K)Cr -yc!_-i<2_9a*n`(Q9Qek?-HC)h5q%HJ4P*+`BcgR@o8&Nr=YFpnGct7O9J=~t?e+1q{?o_~Dq-F_sxH -m!sl{s%vvnAIpxZ*ERO9KQH000080Q-4XQzC%-#FYR503HDV03-ka0B~t=FJE?LZe(wAFKBdaY&C3YV -lQTCY;5+cFc -7`-6&gAj!;Ith>?XaWNhg=|(A+W}tiWQ_fJD%e)RW1-S1V);HYQG@lYrf~@9nNuq?IaI6s0xR6+{seM -X4%e{9dcPMu?i{DNDqY&_c;6tEGrl0#PmyDxPc4O9^APalPjnJkN`Bt~I+7xv>i9-K^P%bB~$j`~J23 -3o&Z8T%HwxoW&WhG~oH=&BgDZ*Ehd@ESC)ViV2B|Wxx}rQkJN=ESx;hQ5#jqbSgjr&Fd?UBxh -EAPr9;S1zK9dQkyK(2P57Ui)#*tCe$}Gt9v48L9`6OrvoucC0rc8vD(Y@nA-X*A3h5JE@or5)WHbdOK -@D&s%)2tK8r?fR0X$Mx*iYOH))z-8Md@I!aY76Z1XjhY*_GDA{39f^@O(7F#)~s$5cp+|@HkhZe3&Vr -2d9e0uU1Jp++C8$d|TQf*Np2ZLo6eBti@9lD~aX=L@uLO<_yB1AJr)!mb&D0AZQy-*%Qxepwc-V -2{)t{GgG=k2jMV#r@RHDR*?#AyBzkQMQnwvhZ)kcnJ3_GCMPaQlX38#g1bGf3zME+hmV5wPR4CDLYda -R83zwjFkLcQ+c8`s9Z;>O%y%FBaIb~lRjXYzs?`MHAAZ -fS$iKI@!HJ%*lTTG~&%8P7<3q$srViiY9Xi)SYN#5+=B@&o9`?o>^jHhX6ZX=~zbH`yiCb -UW9yr0(BqXM?mtrO5ruA>6Jw54k*(Jt)d9Abdf_<#vfTkvG&Tdw!GWZq_8d8WO`gM!>*@mF?*`|2TL=NLQeOD5W>yz#8kuh|4>T<1QY-O00;p4c~(=B(PW@(0{{R!4gdfo0001RX>c!Jc4cm -4Z*nhabZu-kY-wUIW@&76WpZ;bY-w(EE^vA6R^4jbFciM`Q=AFP90-1ZKrTj?LSgKt-EK;7Rr|QnYD= -CZx6MZ1eMhn#|HR1>D7#v2B1@mX^M7&2X3(0iV`J_<^ -yP+Yo{(B57=6PM7>j-=A;ZzhEC_-m=f=R5$Y~>VibeH$wg`ZJjIl4m$+8;+tDaDTu^M>+nbLq-D-!6D -*BJ;4nUJXgF1O9uoHjq^IcP2n(mveZe=-KOC4a6Q;{HAgT1oyt#f*X?q_A-u8qBjf7***6=b-UtB`8z -{x5-Ax$Jp{mv%J+6Hrh5km!zOYQlqge*c~poer)I#-lTdxuxx~$6uN+y4bpuLx>j6MyHHJ7tc4N7=xBQXKfvt+tZryss=zhCW=%{yFBmNdXL{5OX&%PeDFz{7Lbb+KQ_obC!x -I3SBWjy;k*#}o^hhQW6Q<+aDZ46Jj}1CO%2iO2?TWnYtJ -v>BBU~$83q^dn^_lGFY;}oLy!^Y7P4_|fWka1=u(y8(G+g3E%%H0SZZFX!tqb!rYra^+3NB?A@B}(N? -rK3w43JKYkD&r@TMNs_fqP>6;FRK8@on5QuJtUInIcNGg0nMu%3(^3(sej_<3Zyb)3@+j|RuYAwiX!kf?0lN7 -%{UO$*lS=D`u>gyaZ|*CFGi!A{W5Y$YNdd!eguDeXe%qIeT}we*yP@s!AS+)=JlGbKndN7DnxKCR;Xm -K{Sc3U$M-16BsrgV>}InW3sQ*M?Gh?WMKH#{m{f*` -4<66!La_a|=6*LR#OeCa<_Vo>&|M6wd0JS4?bAqoL7Qw8h3&xy3 -vG2aU2mG$;OsN(v|QT`=es`9;BD0tZZT!34{Y#$@gp}nJRN@r66_?_Ul|5_`b=jyMR|-8Gs%U`8=C&; -LL1!CRw)OnrDD+6qvJ2l}NSdFw+v!UFFopu^|8L?7gF$rGQZ6^uA$aX7j^nhlN^gfJ3s8T=dmu`1$sj!|nu^A^Le#T{3}v~s`)JsbfevvC7W#mscprgrZQaj$C(ntadbq=$ix?V -!($0n*u~K3maRk1;wj+F=s86 -m4^-1x@1|;L=k2k{02g2QE^ -~=XR}NKpgVtWa5)K^8hB2!L1;^3?}Jf3dlfkEhDUaHM?oCW5w*xEk(MD)V`w3%E{W>{&V?lq%P|H~2VWrZzNK}?4s@$3bv0K@A5 -klT<6dkQhGhn!)Jf%`NWmq$J^*mEEFLR$%sU@B6AAf}W@{q|+VYjip2(#4t957%!5zKqL`}WFHUm7homuU}QVy6=Wi8f}8`OlTbWS6~=Zi<1^Uy9FH< -CgqFrY8K`ZZ6|nkpEF42mt_qcX|7WKx5MXEXPUix<>by{Q_^YYK4woW@qEr^b$Q~N7E|EaER=P~t<~f -`%;Cu<^Z*abX^L0wwcBIST6@9Brir^csrPkx-j5j{>t*mJtRpzBMz{zlhVbbURAr^5-2q -bp(se*qs<=@e6#Q9%S~qRxcF6O~AaD7@JfQ(6`0tvHwgTvZ%vL%T0i${8TPsF?Ali|6&;#yrd|Z}E?| --BF(p^ -L8&_^0*mDD}9(YsKey5B1~)ou&BiIE*~&@(+Dj!%8LIM**PNhN=Z(<&Y5wvJF~cm)o4P1mG}m-Y`T*J -ch{v0lgwCi+_1Q2}#VUjFL!RH>EvEs`!y^zuFSPquq>!bZaNe=z+=U~19vB!#1OX?qjR*%K1>Ijtcag -_3R_wQThGk63qpPBktq8e1@6NWpXZXd96MDcN@2nzw58q(s4;zBITrM@11Pf&vBGC@0{A -ta+CHXg$8#)648Dcz>=a*_rKo^_6xh1CFETBQh9eU00x8k1hD96H2R+Bl7~$5C|JoX2{t;3{~U{D@b& -!b`O(oO=fTGfmppjB(K=UWr>AS7H^p)uX31%d+&s+kz20Q$sZgrm>h#-h|McSHB3NbeNTl#LVOnHqz? -NB|1EEzARlNiBS@7|@n6q$0_LO9wMI5u!r9pv4kAw=KYL+BCjevNj0tSFQW+4ZVo(Jy$Rq-I^Iw(|dn -`XN}q=DY>fB`863A%-8h!c`9JRVM+=+DhDU7{y -}2;utt^9$bxn5HSpHbaMn?Yi1XTT0{BdD1hIn7L~CI6ibB`pSeVqM~xsp3lvD*C^)Vw{2sXrSQ-U+D* -{b(jVwH9zDGyXqoaCa=J+t_5A7uQz6JbJqCzELOSa?Dg2zVG6mS4I`q*IrNz2l!F%D&Xa}tc%c -zSZEc)64wj8JCjKJg$$k>pBwkRP_EV8*4=FzOE(@)*^e|4y -(k=5a7nBzz#)AZ+!#xCr!Z#$eJU)!ri?Ehik^8Wc1QhQuQp7Y{OYs1#SSanm^0mS}`G#lzYY}JjIDe(nXpVo{12tN9At$e$72{bG!6tf6SIX{f{|x<7>vl5R -XVD%wshnJ}7(`Ug1xQ7+kT>jHk?FV^V&neWuyh2}Ji^0myRw0DqD_zqRKNlS6->q$dI>oGd2@C1m;Cu}DE_n<7ZXzxMsa8`qY -`QvIW5+5n(5j`jyUpY8C4~l1gwTA?cT`N42x@DgJw~$%Tc|*eVrZK?7y467&V3>`=rOarWY^;@Jp%;n -vDmI3(=_-^W*R+ALk^STIGb-B-D71}~L+;3Fx#^&7SgJGHH+JyA_&#uma!BO6NINf%f%&mMNq=`r?+2 -_#2W!AO^>Bu4R2R*UGi=H_SpG>wYvo7s%M3W22S}E|dWa0BSxOI2ZPy1DQJT_JKx3X3N}ShOJfQDA#G -kT38`H!5AX%ygGm#oOj79!#$RagJLJqCI)F=^O(Tdd~DB4!l2s{Q-)Ztz*^_s1Ql*+-HVd(==l{`!y9 -TzmE@jgGy!_6(1hvzL8DQEJpl0||KWSBPUL4<_K+X4A*P`GBe`f(UZC?(SM%s)P`*B&YIBV_S@?GbLo -@s1u@$l?caQVvDBHNw#HLsPCZ+E6wK+ry%xc0z}T88c`e8w8;}`l$y)bEZpE$0a>aPr0H%!#}L-gSgZ-4+ian;FiY~7TlU#1TV-LPoPs -;yz?Lk$#SGKRbefWwrJ>XMavgSTeFKIBG3QbX?)H&M+pTq=bYpE%1cu;1F|cdnFlU)*h^R3B5>`4?6^ -3A^4-}zPQAHaU0#e4)wZkA31PYd|RxX$Xl7bX&czDa5S?a86*RYr=XA-~x5_D1A2cTO!i#rO~fPu}SO -DlLKT#=7NH^Q=tCD{o~tOM=Xcnh|Q9cCanXKSv+XQxvvvwdU>=AA6EhVvCGT)m5U%p6s=m#iC*!jZca -O1558Jc;%xK^;#+z0VzF?rLi%sHkD?+;-xr8YY=L{xwRs+}n$obxEPMtIg$turqlZPATXH(oxbvK|#b -FDce9B4oq@Ka}WkL%b|GF&0{L42wn3l9KM=G+Qi-%stc68U|9jd&QtT%U0|(&e5D7h4K;|)CQl8^(sd -^;ZQz+h8p1-b^F$+Hw_Slku+mHt_&Wkb2yepf2oTy#?M!C^w!+bJi10Rl?&5M?0 -=k@~v18|?E5wrLoaq@c)pdzGTh;Ls7yPLZ(lwcqZygXCl>yz6Ad3`?@LsZr-C#w9nXAC%in2=^2n~Tu -Od-`hp)#bx!Q?wr)Ll-gfkgs&lQV6q$n(z1eFX>KEYgTOGY2%R-S|Uuh&GuUY;;5S8kpq+F&1SWi?rk -~x0A;3r5j4`d}KX`p -yWjy}WzrT_LK%wMSp5sHSw=ep2oF@oI5;Tc3vIT>deFe&?BJXlO2?kM{OW}E_NdgD4K;4~>bzXMK6T4~LPY(Hh^Nw$`&= -`r8BUad4oM0&1x&Lq<=vz3-yhZ{#^X_L?ac5DI+__jKHUuANp*8AfoFnVzj!BHRGh6j64pEGTt&plrY)%m(bJDI4+@nVW+_i!q4(u{ -JJ2ncI4d(qUj^k%8-&4l2z+^__{@=e%5Ea;4Y{_ElTw2z5tm#3O6B0|-{Y}#CvI>6eDzNP_AeKPx$kR -gwWTcj-ZhDJ`g?H)rW`+paO0i|;|FMNkNpECOo%v6LvRojHy%@52z0V^+*z8D?#z@7h*) -{hNpB$7U2g_ur;9ZxKzJhRmKXv;XAy(+T}szsS>?2LR0t@%p_`mqrN3paUR1>ie&G@e_-6oNm_Jxb29 -|1%6Y&PDTfy9oiJ`pyPS02xZro|ojz(x_Prfdo(t1a43&&rXqYMYFM2j&v%hg9i1K=6Z>@uT3<2%dI; -yZ&eiv~_FV67BE-&XM5cWyL^A2cmROibW#O5k+qw0buXV%|BdzM~IUx2XXObuo0f!@`oG%m%h*zbILG -Kr0W)X9o9)>70jLaKXIbAVMcRpb*G>(wXSJGbwvV{y%3~@+ytql}W4FyM~lz0TQ?nYEei*eGgR!Tf{m -xv8G7v5Yn5^L)!`T`t$IT#L!k7xy42A9ds7Rzf|aY^Nd_o+x4R1tT`T_7Eu2HcMm6Mvz$&W!$K-z*Nx -6?2&VSVy*xP1JP@6d5v_8<>(*5^{6aP@;tayg#pW=xSgilrIbl=7dnd@c;MY`aK%W!q7P?2n4zr*Cm= -OMuo`Hc=Ol_1&?dJ>}*s)wJB}tl6VAn&NgDstNEum72_P7NxFJtyuBn9+Xwq)sQ6_C|%bCP0K9+)7D0 -2VVm*5-g6RNT-c3(R&6S<*&`6}&v9P``9a%Bij1A|b04x&~0LJyfqe=38c$ui}j9<^{iivYzYZ0vlwO -2|56?1Ykqz)Zd0xDSrjpblzf)!ZWKEFKZ~qHeE!_j8?eMTD|A2XlKB@%-9NLw*y*o{9wC*nNwNzOBD! -JASuF)38t!dNQ2mI=VexXq@cY%44nrOn}HxzKwrEYq}^_8~2GVV0%zBVM{VJ15*88B{CwVb -XayQPRk4cnqJfT`-Xxy?k#d(&HO^HguU4fw}d>CtPDiO^0QmAmEp9iC7j6|R72xvI}T6|52T7*wx)$? -TWH(yBOWpDn#LirKu$Y}bse`sB4$m5!QX?!ucUBWR|5TNU9(iH5?pY}IBhId6LSL-|F!CE)_UH4R&FU -bUhvT8@I>$=d0|o&BO2LJB6Wd1l*v3ztbNRn@k8tqn1*FxQfd$-E``h0I&6lak-$5B+}iVwkP#a1*eg>MAf@J!g3iHp5u-7 -pKn=_QKoNXj=y|6oxb!F76kYGN(Xy*NSlv>S4x3{Gk3V)nxMHh1Z2Ql^JMl}NK0hlpmcH(kIM)@3d9oerDu{@>bNS4)}>(Nz8J5lW7qpbqueC|5g^Wl7Kmba;K!Ki>bShInLanbVB -z;LD~yenbcJ@uO*T-fy(4f~0(Hm<~2<%dvr=Tz{X<%6~g-Xc1YU!VO4YkY+l-!_jy}SqhXmYyh$#kRM ->$fR=aWsZVIgQbR+BGN{BQo+s<`lrjXJ=39NX8xIV2rwgnJYyrQH_mfdWs@)s@Q~|;@DNV~sfIeeH7GTrwOHx@iv9ylhTi(pMwFmx{Fo_sDAb4&D&otzF%Ctx%lbg?;kEtFdnU1D;)*a#CsjFwk7$ -!`ma8Gc>m*f|M+lmIS(#-F>KWw8US-WVPrZn_c~%1cupP73+Q~#L$MO@y4*J{gQr`(d;j*`#rqFGUR+ -w_*$Dr2@vmRrzW*M{hhA#CUv0O~ZMu*g)M>+5fzJt$;yuFX93xW-@+W9_V}#MA;%`5cn^YE>^3_G359@W)s`8?1zdb-lqZn -X$>|qu{c+Q3p8;$A7wMke?Q3Y*iBpEmr;?;5) -e$*C}+Z!s_Z3Lvg1-J}Tp57h;-=IGwjP}Xql+}EpORe -f@wun1xKG!%j&D}$HG)};?P3{F4;6@+v>SgXrs@=lqTNv@9hGluV~&j5EsZ`5;HfKfGw0~W*WGmLS{T -W7A$$0Ph6)3|!%g5fhKE|=ad*1T&x6rxa}%r{bz9CM|F)|2JFYslOe4xVT3sWJyQttL||1i{~<;JpcmP=}W0OTxa@tbe7aS(NdUkz=c$*-&^wOlid9F7a1Qv -o{WM+sC{_nKZ~lBe5*)PUub0&N-OUHvthL8;u@{C1EY1wVqxOvgz;A`p)h)Gp40&| -CV|xYp5zIRhGAEk8+++oK`F&Pk9tDw}tlXm^y1XaZIAYdbyZbWu7tC1jw!9VDeDVOJPSLIPvh1=C)09 -coqAW11FjM&?$s~V*L4Jx%FzxIFW_3uCODs-OJ=&;TvQ@=$GP?SBefRX~?E3DTr`LD?v4RKo^!i%e9E -06`z|vd&tD7zr>I(nfOs8{{EpT4Mu&QBLmq%L@RZekBZd>9VSLc0RH9r|$gBxB&=u1W}HCpan!8M~Y5 -pR8Q^SpkTbUJa{<9B$6L+~@I(*<5#b*Odp6J#+O1B;su4p>8M0fQm2WQ+z -kJ}Nrh;lDuxX}bS;Or>OjY?0jB|PXbyhsh*;b4;C_8&?({)~r9=H?^T$=D`nJSCadUDx;`8U~H*p)|_ -vLae%$l_tXKxYc|)264VtPp&8M=jzu-pz~M1PH&E1UQZvc_CYWXz66^#6HXk~3Fx|9>1T&jgmv(USHm -l#ImisoBuJYa|JpEO{^Uyrb?8@)^B_StRB+eI%i+2X>zead<_nVpf>E{J#lLY<-%zOujoO{IPS{c{6S -UTvj`^IBlMeg7IFrE8v`QX`?A{`B^!)U(dNB$f52O%n%v)FL1)ODYb1;c%QWr8*y}EHM!6a2xI&czfn -@G1NU@kOIAXpuk&^gQT`i03jc&yGxkJShsN_{ZKQZ<>0Spf!wGr&QV`u|+uDpfa4_m#44r_;leJ$n9F -RgCQFF2)dZJRwlZ(9}*R`gsA>=z+PZXJ)>JFn)xR^E%G(cCLPs*l)KRV4uytz6p+l(dmoP(NV8TDP%~ ->>m~rPu&ijkM;k5f9i1vg-RP1<@1sU1FzT?@;>@B=Z==0qzps_C-S9qzKjW#f@o1c@KJ2`H%?e1>xwd -(U{!P}NSB^%bUnf^*C(j-|o?okG&-f2F_t(4G_3nOrb*-*a2`6ZIz~R88 -9Gew(}6xK;!RVoQeuc8(t`J^VE=V=?8X({Z7jD@98<07hzJ5aJ_A>LT#X1`pN;zq0Cle_kAeRobvgS-?+ZF;l>>WPD8amj=dYoE8+<2R`IQ_K!#)XIy^e}>x1<6MQ-xw3Pha -ERNt^#K3?z61aODgXcgaA|NaUv_0~WN&gWXmo9CHEd~OFJ@_MbY*gLFL!8ZbY*jJVPj=3a -Cw!KO>f&U42JLe6@-eSHsTrEbwCe8;O$y0Jq*YKL$T>qoh%uW+>ibGQMTMhPCH;a7?DUmd`XGvtm&by -yU~vp>l$P~80$eCol&F5dfpe%$_MGB(FKfJHm1c|Nsm@2$5@Q9$XFL}F*tPact`b448W%b2Mdqh34t=)gMB@eUg~t!D0VSQ!(pKdpJs?}`=;p#L)OF@iby0R$L(D`OKE51w#)3D$Q3vTK<(BVJ! -t&2UpiFUl9(MeYILI#g&;{*CxW5&@IQ&d}mK7S5y=YtH?70>_VS}uopnS7gD_u#In``*Q5;Lk(U6pPp -RU!Wp^bg#Jw{hC|SD%1-tvh{NbD~&VxH^6~&vpT~=sXymI@0JF!h18NliFL;j`ZlJo`h$JmaNnFF?`7 -D44nc=AMpPw=cJNz-D`LgN2Hz=W{Evd^FB*du`x7$;Q^mT)8mgfZIS+yG@QBeJr1y}RXbYEXCaCvP}&1wQM5WeRrrs=^IY&`WM>Y*T5XrTw8hf+dzchq3aq)Arv?VD`WTJW@)`Tk~SIOi*m2 -&z>sq7Ps&!ihR)$R5x~SbN$7S%jErA^NU~olhl!vJ|)I8Cx9H-Wi-QCPWgp_*5Hec9RLXQ{0ke44@b} -?Swp_ZOMb)J4ylDxHr#6*Y`N$0*ah|o$;*PpbcByo43@!3RMM}Bu@~x!2Y)sXGOczm>dIYUL=%C4tptCw(P8yw7tW03VLtLam9T}5S8I$a0@W -oSue=`>SX_DuJ|MW5*z#~}tQ4Eg_x(Kg5xbU8Q5jhnCLldx+XMdrbOZTsMap6Pu3s6e~1QY-O00;p4c -~(c!Jc4cm4Z*nhabZu-kY-wUIbaG{7VPs)&bY*gLFK1TScv(qhfIewi)P^O^IfRTKx#m)YPz#>Qj3os(OwmGnUXC{QOy~ojrc$xa$hS2uSqQhrDE%sEv%U -Sk4Ix@DL)b0Im%aqVF^kDNhQi04ncft~*y)G{IH|)28IqYJ0$|ZVxW&*hAq -HzKTJypy?oqR{|MB=lM=Z)Oz`DAgRil&x-O+p}>il(f06}*`2BAC*uVj}Zf+Zi>K#K9TH3N&wVL(LOs -Mb=h@km*m!g^#Ep1b;1st?kDf{DP6cCgbbDZwystOZOOhR95DM`-_-Sp$S#k9@3Z~iH8teD73D~pNal -E(F7*okJZAyseaZ?=9;}~cc3=_QS51paJ}vCeAnuJFAfA?7Wm(Nih*m}90rgz=d%&q#Fok2RdsOEkHz?$LEiYy2#=|ws?bHkhB=z{I>ZgLIcbUCa`&)`X7J-c4UW@Her~iz^XC1t<{wZ?0|XQR000O8`*~JVfN80hL;?T+@CEsJgq+2HyI1op6`yU;^JAuSY^V6^r -)Q6o!6lCxP#|9eNaqqXxv_psGrwVsdn-poi!ZA}A3QFp$xSQH?e)>seX*%{S&EQaq4DtVAj8l6F>Woa -rbl=NtYa*WjhY@n11yByj)n#J -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 -ik7;QKRzOuBh -=>leYK(4nl+TFgsyGQh51cX$n&&hpWG`q>&FFMC>q0ftjtu4lIsuxO757?uMmH -+I6578WvGeV&<>hk847vRrP>bE)j_H@k`lgyi+N8Yi~`Y`3kdX_u3JP&Ehr3R&D~%_9ne`6sg)0rAIn -qLw`;g8g6qJ@%ASEU>=4mQ=tX9S$|XWS}A{_X4G3zhZ-I`Gb-BsY|z*)K3x6z{@0*w78ONNlet@(kob -e`3ml-*%_Y#|YwII>@4s#DDp_7G`UU<;ut@JA%h`mB{F6}-&s=vRDC@drg}aybpr;+gQ@D1323$Y2J( -ZB_scUJc_ne>^uGcNK9a)OhqMNH291?oPvv^sN-U=6Cb7$-XL_y$Y5)d70NB*dSE=%uAuw!p;U4pyFd -cVUoH*Av}J?A=TV9<&Jg84XzdDDEqV{%B@!@ih1-umFCc-|utQEG!xbfZQU5PBIm77kqI+p&tI2@wF& -c|d+%B07dQkj9rbk6*TRNu`b-wR6W19Fe~-?8S{2CS(u`*G$Jr@0duSb2ozb3ELz=V7(A@2pJO$aD -D6Rmi4YvMPY#<2I6jzfTnCZ=4?H -tnk6!FlE!j{gRAIe~twY88ZCr=hC0Yr@#@G2X=l4U0kkVDh(dKI=X@nfS^Jpbc`2U<(`U -wEg4)!r*N;4_YUhB6*m$w$y0;c;Ed%P80#O~-UMnc>sP&`bRnly@*Tl}{x4i5wbkz@znx-t+^Bzt~XOW}&Qv(>X9H{9blY%4}r%(D3JiM3=KsBO-eK?p4I@z -K0*4nSL7hJ~8DZv3&6*e+q;fePS20uTrLRT(9|H`GD8eO-oA#wK@;={PZO#SwoBVOr~cpndnns7^O{h{&WNSYBi&m_g-S-rU8E8lezA85cm4M#5VJ;9e-^)OI+ -o3(8&HT360G~ns=z)t|HqTDv-2IHokpV -e$sIrDYm8QR8BH9h{guwj-$SJ`6Xh`hMU8Ur53t$U}EqVv&Crq5aMdKyoBjp6xLot|Q^}7f8nSasq2I -Y=>(dS=<_{q)J -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 -OCreY*zBs;@tN+_MpmYhq -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 -K6;3IeE?+YM+;wBUY-c}{ -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 -apmsc$X~=y~ -7=naKCtX7+fCRI(>=3=yQ*?PjNurxnZkM#nJ$68Df68c99!dezI)l*Ll5RZ -o|FBp1dAuWTWUl4kZjGXe&PR5&qLl5l4r@w?owRKz~?w9n>MjK-P_>*zT_kT$vEAAW}3EG>YvqKX0;x ->F9NYJcFt>g64j_GB2FH5JH^NJfo%#XwtPLo;74%RO89yZx6)Bj)SW?0P2}fJC>mrg -~D?YxPb#FR^iR!pbL27)CbG}XeAWj+fCL@P7$ETsxM3c!VLu#b)WVIz8Lme0SGxVtehdHO$dFIrhHNr -T-Mv2qJ=`mdOP_^zrXh5DTv;2r}AHy>YDwEVB(o-;8ZfCJ{PcefYQB6K|dl^Zkr{avL*qfbdZIujlFV -MTzDa{@|#bMbznawcZFq@54PxpGBoU`>x$NoNhpie&B>$2ibiujjgOlrjM(0Tsq&GqWfw>RW|NSvX6F -G9-USt2&C?ny6D=pN=V8qMJzqh(Ima#GT2#i_MO{KpG*GFBO53jZ>&|KG4SM;nd7x$ZDh0?m0xir^BD -zIm?6C70d@6aWAK2mt -$eR#PD@+Cmox001-{001Ze003}la4%nWWo~3|axZ9fZEQ7cX<{#Qa%E+AVQgzb2T54h~M&@r^$#E2@lhV&0{WDM&sP`R!HOPHXcXts|id2L~V@6VVdNE -CL%YR3Ei@MM?;p6$Zg7_NzAqk_D4jno^EJHL!b_{=W`mzAlEUu^3N_#KfaqxXa0!bc=q}I1c$T9Uvg> -kc4BraFi~;uuu#GX*=KdaXBZH2mm4XoW&7O)u^8 -5h=L?Fwdq^j*-n+f*ng}&z1ztrzNpu$SBVlI7*yvcPh|XG|93wt56wEwFcXK?JDq^C6H)Rg+xIrV~{& -8Hd5*}L~BAIr*gVxHQj)mQcYntfiecEpX33#WErP1|A$Q~9_=ARZz6As$k$^HDpWefZS~u;A8|Gk)#4cQYIN-8e3BV%&u#|kweOXV -e(B{#8EO3D(^zgqAdctjA{E`iBY-U;GomdK(HpA=?H@~O6zL~<@pAf4jgI8hjN#%9P>C>N+Ow;Dv$g> -;Y=yb4S1!}{;�IFd54FBm%`k6dc>|9Rk5+25g0f8J{(zv0LJU^p}y>fumTk|gSMU0=13X#c&-s*Gz -DsMqES3f?P(bPOVGwU{$Pn+cc -}n*#d=)veg%ZsSBj@@;5i04IXNM+Io*hD{9DYI@oirE>U8t+;1CR)5pnWR_=!8K)a -2Hzp0R|aNi$TregNK7y&}E2R2-qe{0WlW=c|N`P@b2Z?^S>waPjAj=lgsxnPbb}S>=DBa>p39<+LratvG&M{I*vCWCM{RxtX;skD{3A)p49d+21vLc>^7@mC&$PLClgPndIaJ!LcG -Wtb7dTEh4lXv{vHnT1W1P?22W~!}I7E4@0=T-{*pD^|V{RGexQM6b@=@hD -&|junXF3fF|l-)N2Qg^Z4V{NC^;T*aBr_1}HOhx*Ea7y=CIwRt90aji@>g1%wN2M(QN9rvX8fTEA~;Q -6x7>dY2eA(umCf(^cgS)V~-M(XbLqUitGD)^=?ec^6jGiCFiIbYJpJVH52%ba>fMZac&9z -xLZ7sdBvfS$!p5@P?^HXa>*fxlJ)nHc~9ZWBQ<+6m994pFsz{sMY((vDfjQd1k8bgE)OrVSQ^}-8Y!z -AGBsaVB;4i&eLu30_2}xC>vHw#*TC&stScm7$fpIjFNCr$E5`9L(Jy*IBZsZ)rm2ZPYlcIY8jCG@_~z -FnwkjWUxQ=t#NtFbf7?lXMoNaK_kBLnZtyf)jWgOLPq~&Ok05(VHPV1VKSrg!1z42c8*k$|IL)rAmix -DVmzx%dp*|bghe$=NyE3KZ-QnpB(-E1yD-^1QY-O00;p4c~(c!Jc4cm4Z*nhabZu-kY-wUIbaG{7cVTR6WpZ;bWN&RQaCwzf$!^;)5WVXwc$5G_A5 -g#tLskl-4HY8%q9M)`_O=6++UDh)N{Lbd@I?752;bO4($qW1J0aN- -MqlvBLwXfZqGwQsbOVYqE`15S#HiL5hcSzl))JaWdTIf!R#r*4NuvFIOwug<@epSu*~DXzSpszawN+@ -a28A6Jt-yV@-GQ@wTex3&k_xJA;SEf{7Xny~e5)Xhzo&L%d{z`~*6BKXjH7g$5bFr8OESTDWd25Z2RF -Cv^Nzp9Htl7`R4F0R3jO{wGnb99E+d>XseH?8EbO4?L58nzl74WZwI|ek7O#7??n8_Y-#CLsh?Z>}w# -KrhIqY}aCGnv4r@uhsGXZaM%89lIzqABvI@%gt@&K1`a|bEue;%CgPqN>iE*pVod0lp8Dtb@8kmQNrxh`*d -G2AgBl`Y!)_kyFd1m%F8Q4n}jk4zu%`uSsydIZl%9;krYUQ-`)v;)@r3s6e~1QY-O00;p4c~(=_&&Na -S1ONcX5dZ)w0001RX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7cVTR6WpZ;bWpr|7WiD`e-B@jJ+cpsXu3y -2bU!+bE>UN*9dO(*rMSvwKG7sBeWCA0dZ6eeQNXm^d^uOPd@={>HHX(pz^6t6k9^R3Rm#WNJk} -PXeD@YP1@~TvZ@mpQW+Cb8Lk60!)LW{D9SY3#;%pj4%fZ|en-jXoVQc6e-n&=kfN@buRI~A$%+>ioiU -WqDBHc(`xinX|JU9?f6SS4m#K?@wWbGKZB8;XBih<*_%qt-w -@c#-He}27r6MhZiH=!STo|nOrt-#FY3gD+&NaP*NB;-&S8amIb<_o9%!efu&)0;>9QR{u%lQOrjnBlH -l>}CR=>&y>hr0-ehRC28unf%7(9Wo;!;>)tQhO|~%Y~W!oJZ9`QtF}@U<~S3Y-%6O)MXez_+HfW4y9K -4k_x(@kT`;o-R&ixST1&2WGE-0~&}}jinhGm>FT>wxQgg&`H`CftS_t#zrfPbKI5918b -LJsBFx$nSYiQ{U(m0EOUuGwkYleKnZ#_4rU -?776Pw&W+7b$40qt;zN7=BNNOUYNvH*jzK2tT=yK0|9y12Tx3wP|YU9NSB**4TbvDX6k -EZH4UM&;t^;ip2g|fu=U#ff#M6dW!(*&U4Q&UFGCWz^qaAp -6m#Q|5nog|_oQKF@yipT;D+$ob=nM1#V$X;Z#|U)8r#r02Lq-YN>WCHzL1+&fG%{UB+r#NVWb7G@2EAi{fWs2EG>R6n4VN|T)l3R -)8Vv|S+{Spb$dUiZW-u-z!X5^s=W!W3@{6D+AnvVEU(n!P7~Wu5OM4J!T;5&;Qa7>+Jc72NPmc2>|wPppwN8b$+2BQ`CYa~o*@2NUzE!V7d~{0tt8wwJ=9$_LwaLhQt91tZC7CXo|oPXoA)T=62GlPc$Fk+DQ@%_J3SvO$QFmvM4nc~;0+@neId*-2(FX~~#<9mn{*uDAdN6`PfIAG%E>+p=R>W5mtMnyO~=xP6) -M9R<_&VltUzY(rSC@c2s0nTl>E1U`I2z(hqJi=5dv_>l~F%FRpjjtP(hVsfQvp0b&mcSnu}YO2;zLqr -v43~8NBsY*GY&dC(40Zyh7*q92c6!}mTdRDCe0s?Dnv^`09&UKQ^6cd{{5a#TQuVN0c=mnZ)JbW!Pbw**C~5)OF=eGDzkxIO8q#YaWg&e6OYrm=H{=i_#*6V55@o6uYJZi_GA -C}tKAvXtz=i%QKR4F0^(y##PThAIKfag<<`ph|RZvmCMuHu7T_Qx0U>tn3yj7tMl`Fo46+J5A1fW~V^ -1y7oPT$f|>A3_+QpW`8@u@_MVG>~)*nO@drXa2xkP$DCmF&q*7(b5chg1aYqhv`WB6`|OJ#7!nQt{Dl -@DR@-M^q}>-p+UI}er1K2>XX?Cky=v)vkaUY>&lFAkGtOwE-Y3m1jSbvKiIFN@&4_FYX&(vCTeMa1`2 -Y08RdFl6ZqRgtE~3qe;7W&btSKRPT?JK48zsNH*F&jStbM7GEF9S(h#o*a!(bfZ%`bx+E@1oxV2Xi2jv)=j)qp -b^su9$HLAAW6)ng+Ld>b?6sGm1pFonv$0Y5lV1F;BxV_oV%5@DQ~T9ku$3G?@Ct4Y0bSYY?$A_yZiI) -;&C~@h*S?yVXy{k`RV<_@*msj$viqEl=&W*VZwX%I&nlO5Qp<_(~+zg@V=d%F>FAjv7)OPtpVmefRWo -JF~ZyW9<{6!9x`!o&@&~KKXM$n(HZ?3La7{hp(Ey)i*PtGJZMEZ`KOE-jXwPz -X81z9uT6hbrOIh7|;UuHeMJ=N(BvY`7BAU3aN1DohJ@+$-!*@zA=u^1hyq$#E%@?quTtt_fi_3F& -}z8U}-)UH)N!r%MeG{_?;h0di&%2X`}f-Ma3}Xu`*jcYOaGZwqLM23ftMU!^|XoIg9NHnpZ=~QHO+q$ -DPm~|9R%nKf8ts=6?SvidL|lv1Wh~c8-ILzNzQ1Fp5`?7QZiSIynL2G=&&{X?og=M_8SqE5pAgn62lg -)0FMc)d|@P|Il8z~P!q}248GP!w(A(Ca951zDM< -@pKvpRDAM~;~>`qO= -r8%&qw{JH16!87_WMWAJng-@x8fZU|Reig3;l#*SoDBHdKf;O+RDwO!GqSshE;d7AaFe!ge@q;1Plo{ -e^pFz`6BxFPi)fP)h>@6aWAK2mt$eR#OvSZ4k%;000FE001fg003}la4%nWWo~3|axZ9fZEQ7cX<{#Q -a%E+AVQgz92cuor^wb`t-=En7Z;(siG(_DAQ)0#yj{@rYRO+9})0CLBMEuLWsG7J>d&gZKhgE{Up>5w8E4BzQBXfrGYtY{qRtOzN0Rn)eJUg*H~FdB@7h{D6hUBx+;*`P-{o#^69@YmE9Oa%9|{P#u{V17}_x_z@xWg5CvLn; -(O$r8K>F2_1SUD9wOAN3f}@4zz)KDSfX_bMIK+T|U}eUfu(DEvky`==*oTImZL@y?qY(F7U|FFYBkt2 -HA`Z5;$oMTtxcbeY}T@IbCyZ@6aWAK2mt$eR#Sy|x~Loh002<~000 -~S003}la4%nWWo~3|axZCQZecH9UukY>bYEXCaCvo+!A^uQ5QgtOMYAVOHlB<(54w8Ti!Wfzq?7?h%9 -NC{(YLpu6xv~6LagDIPtxQi&aw@Xb#0|XQR000O8`*~JVWffy_D+2%keGLEr82|tPaA|NaUv_0~WN&gWX=H9;FJo_HWn(UIdF@wAZ__{!zW -Y}U>A|+@S}8~zDj5md9tsjjK*gb{qD?$*7H!tn?ph@v{yVde#Ez2`^@4c$U?;oZJocN}&+AInOUUQ7L -g34$Rt8Yc>k>04(Lb4BGZY!L;dyoO_T{BgwTgm)h0XQ)pTelJKFzA(@^0<)W7`Pw^{z3zmP|y^w3XZ% -PRrWpDMc^HlJZzKTwoI4Oxp4IDNfpF^q90&HAZ`XetH|HQ8X7!YdE)Y6CXWyf6}uk0=i19!ZH$#qN24 -h!!kgdwJu_96rY=z&=9U8n=YO~LQ@&gErpX8KIxm;%An4GOLM!y^C~!lCk3qib?)q?7}wa5mBiOlw~Z -wOOK%JdCQD&SnvA}EpN!(Xs@0O2#Jf(@s2@+(#w}wI1x>3Y%toUO#vMKk2M(-Rnt?#+e|9AK8b6k#z{ -oaDj=A5Oq&VKkQJ`R#Bj03Ka;|WR(lBx9*i`F|dqw?-3d>zY;LH*{ojKI>U^iw^aoP~+S;sHGle8TV_ -htsOx)y&F^@H|wN}_4Y4^<%7jo>C!V2w7Es!hX!$R>{aVZE#EpdlMSb#rohyFIzvYn;R4J8LE;w@Q9?Z?g$I`Zz#4nauUlb9Z#uV{f3-3^-V9K -=L$X}%jpV)LtZ7h|iGNWf?w+Q@idgTvdAgX#1)N6vM(u9&?xNmycwJLE_uMSt`k3AhmxD!3soni@^Us -7#{Adct+k_n2ZvEJOgLEhyU9`*@?NN*i{F!0|#2&>sD9!(dVkAzj)Bl?BcWXxbOElv#!ti(@NQO#4k% -Ja|Fbdi7lj4R{z#UPua9qy&Q(^lzWHK;)kE}dk>Zi6%t$^GiP2W)ysZ|nYH-g|Kpnqb%l2sib=XCaFh -R@6aWAK2mt$eR#Ppb#ofy -Q003wK000^Q003}la4%nWWo~3|axZCQZecHDZ*6d4bS`jtUC%MA!Y~jA@V%en;0@M?io8i2grW{zT+~ -V45ZkK+lDlv@hkkq0XhE7S|Kxsut`qjKYFH4g4=f75Mfb^CY$l=h!O~+4E9w_;CCgM4Ep~9>>b$S((w -RHD`L=*`euf#`LK#)&u-w7DSB&{dP@h78G&FsNMkuLY>4eIaw+t<^XGBc@pZQetji(i+I2n&YFqCoXr -hT$;V!}6KY{Ycc+6RXoNwGbOu#~g;(2F(I -(+HSj-ri(T$7UvoUEz^z~S<;bG9#`ytzZsH}_>lbQZgD*Wu_KC{_Y7x-T<~;71|(W#DbGnXPw1THw4< -MAkziq7J{3t6rgLUa;HC-6CCd`b$0SOVeey2L;g`m0${RVN8V}XXHs|N@F@>@igOX5#7VkEVxhl{h%||giYsZE~cEpE6xT0B8y_y4aT2~;qZ`sE{@>mb -Fo+q8GerrCPA;$QSjfqxP11K^OSc#Dp>NNciJKF+1uMAR9-G@x_L>4J0G>+xEK1*N)vd>veb2cX# -PiHY(30x^5WPqb%;(8v=I6jzmV(%~!Ai+I62N162T+0CBEg4HG8mSCNF+!hwncjmYG(IaVb8CbjRm=s(7joc_n+&da4zF@98J(qRYy$H9u$%HZ6 -1-mpgnVt>0eItz -;!3S_X(v+^En=1|^XsmBa3RY$HI@`g=}c_#fI4BdUP^u;qoZ1Znuu347R;pTiWkhH0>Gm2nPlt}kara -0Fhs14MD0t8)|1vfJUPZe6EI;w8#z2V>g_~!c+J9_DaM?VS~U{q{7kNEv@M^+M+Ac$;YAD)UtFg}3}G -aRAIOXmnq@HtE1+1(hiJeSxaf-XmiMOJq7 ->5(TC`V|VFyGh2&8^Cuupy*h$oLIY1-XmzYZApVTY^>8^0Nbaprc`10tFRA9?@9W -AB;C=pk7K2gj=--gzRO?AL?BY{^Sm3%A;n;VrIscm{Cb~z}{<)bUjn&S$1LJ@pAo;K}|MIZ?wk)J`%5ww(mY^l4DcK;SWD% -~H4({1aDo)Vb;^;q65M91H2d~jdJ(#`xZ#&u{;Q&EhYFJb{&MSB#=Yd@n>9YAtnFxS98x#7n>Z+6)X4 -6}&Rl(7)JX9iv)stT3a`hA_i|-GI!|>Ef8DKWlD#KGZba+L&g@E+Q!KBwipe^m8h5+eipZG~{ynPTOW -Q3BF1#lOz^$+%m`g6+lFR8+)_pholHpj(a$#R4GXyQarVAh7Cgc)1~J)a3wRg^fv(S?A)R)V+)AnTX^ -y$E^@QCD*B#ajH#IHNa~qRd}imX5p0^fAb@E?580o!hr8J)!X4pUQ1lu6T65uG5JCHr&u|4D4s14DH`5kVt -_{ii&;Zr*%l$3UTg3g{A)=Q8uU;g?%RJt7`thG`n<|4 -zp}$7e+P4>+TumN=>Nk=Z)+E~PaC{+@Z~DQMw6~qRp@kbQ)AFg>Wx<|wM(b&EymEa{{;1T=kC -~%K65*LsfpMt&53q@cXA{7jIq!6UTWxpOuFAtw_$_U0%KXZZf|3k@0klczGjQvB@B!*2Bd9jbrlsl -G|e>3S`1&~P=flahCph1;!aCd#44p&(s2B<@I!5nbw|rLr=mYvQkej%azHI^>ftt3X{gXpIy-|*CJ)Wm2iPL21r!WSnj^SeEm4l?Zcu?a}N7)92n|cD5DQGq;@^S|#_ZX0)ALvcF -*Rv~e*2lZ;Q|wC_k1ZbkR@@en-7HmeFSpQ1V=C4d)5gubJY&{G@3^0Pd&4f)xrH&TQCyRy8AaO~f;tU -aHJ%3O?YwJu+w?2hEECuz>)LkU<~U7&$C%i-%OD7%;zQMaR+790|(YSH3#+ED_(oU6&t5^DqxiPrOWN@?Y(DxK%j2-R967X`#$=j01ZRPCGkJVC_Y|&I3D43t -B?tB)#aeP0i<{Bx;`C;Tda5 -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 -drBUjl4noJ!&CC?DS3EGo;@WGPsy{V?wJ8N}fF ->j~COkr{wXLc**{Tr{vjF^6->AdrBUjl4noJ!&CC?DS3EGo;@WGPsy{V!CaoRBQ -8IgyujsRF4qE?Gq$huFGNo82J?ZL5Ag6p`>pTok;!`cV7_5s#Du(c1c_JOT^fVB^7?E|cR -U~3;>?E@br5^ezGBR2w;wBc?{#v}RQiwL!f?Da-?y^+1%2(LG?*BjyWM)rCmyxz!OZ-mzy+3St)dLw( -i5ngX(uQ$T$jjYHKiX2&yBNREZB1d?=k-gps{f?~Pu>f~r+)(>i7aiF@W{rZf{z^8M~)4mY77vrbL3NEVgqjWsAB_eK3XC+;O5geA{_0=j& -_8j9of;2aI_;k+7YL*L}T0r+{U;KxY_@X@V_Jb-_d}Z=Z4sUyEE<%+?{cE;O>mO19xZKn0I1l+#R^ti -H~sNV`tnQxaG*4;Xw{ejvyx{XOIh%E69z>26AWe0GVwSRvCLrCh~+}d3KExg5?97;)G!NK%$6wC-S@# -G4Dj4cOvGU$n#Fbyc2oeiI{idlCy|_o9CT~c_;F`6EW{Zo_8YVoyhY}#Jm%E-ierZBF{S!^G@V>Ct}` --JnzICxcTIxi0LTubQG~BiM%F>H*nvK8%veQOO=SpEAmn$VyO~&sS>eNNxW1^SgIsmsw6B`5-(K}mMV -#tDhW%K#7mWgrAp$ZO2Sel@lqvWsgii9lCV@syi`e8sw7^jBrH`DFI5tjDv6gW2}_m4OO=GBO5&wT!c -ryiQYB%jl6a|-uvAIBR7qH>Bwnf{EL9RORT7peiI*w~OO?b+m4u~A;-yN$QYG&omV=6o!2E;I>bcuJW~Z-(j3b29k=%!?$L*O?nKTc3O%a^aV|yubNGVy9sFR(R6IM&(u5C>7X~!uA9 -kl~-l;!O@Uc_p||$|fIPdl1tdJDwH -ruy(bVn*GCk5T!5GgZhMB~!&g3Ilb0jK*SvOBynm>ZIz`qV2hVO@y>!_T -gCVzkzxPDc%#Jd*~j>^k0m@Z@gt$J#tSmT>d07Q4H|2_-Z5yb@xsQSvBnx}&{$)QHE67{TW)a6jWyPw -vBrklps_ai*U(rSj2rgbc(>f3vBnx}&{$*hXmFr9o5v2-?5vs{s@bo}%1=RSO9lp3F4DdH|`s1)mf2y2bmp_j#XV})7!Cp>#WFFUS6FQxx*rNw# -Xfd+*y$^Ltlecb6f}DVAUM%2zIb)4yfi}WgFoj9IR{ud!~bxZA5g!$~K^EBNZlBkV)PU;5AqW2Xt_-4 -q~-@4OX@RWg9$24=CGUWgAeo!3J-@;0?BH1D0*DLJU~8!E^BdKZ6xwKp_Sz#DL>B_!Hikz&&Xi#VY?A -Z14v3WQd+%H9FY6g$h1V<4iuCPgb7^^_i>?6ACd|Atn@J^4|JP$03|-eI~5WH346U0TTPRkoT(GH+=rZ$H6c7{EH`ng(!r@TapV~l8d(_7q%o9`^ -XEQ?H7*@J{?_)-^+#fa`B)o7?j2DCANL8#k*Sz@8#mrLHykI4IGm@$fQ*`AO5;|X8}Wb-MnzW;T7EMu --=H6xG-cZe(LtgHNzIosNh9hz}IUOA`;Vt(Y9>;Ag6L=gq@0Q>2Z|}}B>0qbxBcaG -QD>7F3PXwk!PrgGi<#_6k@JT*K+u;~(r-7ePPv<2B&L5la?U9X@^KMDOv@R>)Diqr60(%bfM$=iz3%tl;gbbQX&D5o3KmP&ySct33I}{x=W!$4z)-i-#fmKmoE-q -KwXt_N3elwax(u%AM)`Wh%k6rTk;>+hQm(bC_*o}|aLx~l}>SCAt+Ba%_{XL?+lBZ1*FbkODZ$Gvc}c6hGiIdqA%oo6fE&GtL-j1to~&z -CZ{WL|@_yh{mbJYT50H(x&wa6Y*-HXC -ADGUCXOZZ&T_yn$qmZ^zu2YafM4Fn<$z!E8g^YAMeqU_h<#+4}w1ce-!)?_@m&Dz#j#F1b(^ -3YDM6Wf+5~GJmZC{E -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 -Yq_HH;y&RA0uo}^u~3}Q9Qi+ -W9zvtd11z!KcBKan)TPxJ{W*IX6>U&Y?5H}WzLN!V*#1eUN`)AcIAOKu@JL(0l33t+zjy$GfErp1;^a -HOWXjz(e$s}+2{hrf(P6t(f}>scjNoz;o3j{1OYP#V_hlq3)Ks0x`jQC20$^kOAV-x?NmxMQx8M(XcNg~l-;5qtd1?oW4a=)z%*NEXBfHL4G7atUtt00cMLn8f+C -JV0&j5yZr{PY-6!6$3ABaBM}{r=?}$c<`xDWdbQu4KGMzH>ehN# -H{PEmBJ$}x;UX|nUaVBFmC2KR{9g9W^ow}KLXsXv;|D%5ti3MXC9=YJf3)6Z`v8?Z77Cjuo&+gM=^5D -5W}a-le+t4)9&2SOZMN&c6gGyz19`gyOfo9$0eUM{8~Zt0sD8juOs)5Ij8eCp&Vxa08mQ<1QY-O00;p -4c~(>DqeJEw0000L0000W0001RX>c!Jc4cm4Z*nhbWNu+EaA9L>VP|DuWMOn+E^v8^k1tCtD$dN$i;q -{ZRZut9Gg9Z`0sv4;0|XQR000O8`*~JVyvGuR9hd+Bc@zTx9RL6TaA|NaUv_0~WN&gWX=H9;FLiWtG& -W>mbYU)Vd9=N0b6m-hCHh^zg55ncDov}qahJ@SsTp<8y?O67YqK}+%jT(|1wm8)0(X2t8Tu6^~LmFr%8WyMc_vtr#_Z>@g!A6C7xcIDr_vU1(Z?|kR -AcivjFV%=}vUbXi16|2^~{nk6{R{S5U-d*?o760;!w^zUV{T2UX?W(ude&;)X{nvl}&-wqEuK4dO-(L -LazpVVJ`>n-?|Hr@lUvc&C-3Pw&o$vhQC%=64op;?6Kl#auAFlXr5dD4I-?x4DJFmR@+KQjN`|9gIX? -y8=KlsiH_rJe+`kSx*V8u({Tk&`QxcG}dTKuISE&l9}R;_g{cz4}9KmJQQyx^Ji$3I^Dl6PKR_ltMdu -K3a7A1}52y6yWbz8ih_d*6TlccA_b)!&i&J63;J)ZbP0ccT8TslV&$?^OMrslOZQ?_B*|sJ~17zJ>tJ -K%glIGzWnuAn1e%UO^AT!3LSqq{k5KawYCb~EN2vJ-H6NkoBh-9 -^nvYQP5o$idr+ob1Qc!4t<|ArTpNIl22(=*6f>;YGT2R%3LXsvZJ`xyp;c|6Ro!E&y2nnHDs(Ks(B+c9hjp2@>rn6HR-fX-~AHOf(;f<|EO3B$|&z^O0yi63s`V%`nksm}oOhv>7J -a3~NvM_*W?iv>?=iNDE>usAxe|3lc4;X+d2JQZ2}|prHl178F`gDnVWIQP+IbH6L}&M_uz#*L>79A9c -+~UGq`beAG1`bGDbNljEzyKdAlxbfZ@DfaQkd!AD{Du>HDDmQ#w^WHhxD6i4?&P%aZ>3bKX*oThqU6f+4+4n9Hi`K97^!@EL;7lO#8MhT!u -w$qk<&_&iH;!)FLS&y(En8G_G?Bqu&)5D_hF5D|n>+W>@Msj -HBI?0WeB53I*$&HpGKVH8-N^=p -?xwlLBNVKjBtPgSxgMheWF$WxUrT_u=?}=~#>Ph=CHVpQ+}Ic!5|ST~&y9_-AszVv`P_=JH -6$ZHAfHB*o9KFQ}+4WCu;Nj}%)5rZoDB%f>Yi9r>7lFv1H#h?m4$>*BFdUrR;y)XTcd}+=2(wZmr?p~66Q@u;>q~@vpa0|%|wK -Z>;ACXI_c_QzU`>A{Lyi3le?)m(1C&|sIRQIFOU7x0r0ZhES0ZiSi?Jjwtx=~vlY9o(P_iDR)j^w^ae -8`j;4^#Jk<1TrFy0`PYLnJqLUWa<9N2gvrACfmV3D_Wop%V`#5gVj1bn?a~AseKgv5(i29EMJerN+=x -1P#>vQsX^R&*6vUdsE|kQ*YuQQ#VUJhabwsQeaZELFSG7<4q*@9DYoZ43e}TUL&~)ypW*%@CM0E;FWm -}Kc)_r!RkgxZmbUJ*AFL1ZWu$h^A&cu$Y8Ff@Eh5}wJi4r{n0j(8-qsj@1yNfo*QK#@%IrCksD8gq~A -y6cXOi#B>X-i|C$>$X=Xws1@CMixwftMqxyKN>$2)x_H)=! -f@y-sC8?_<#cxM;MjoOfVyt9YoMs3JF-q}ZTqc-Fo?;IexQ5$lPcMg%Dbr%7)3MDFptcx#1Hz!8^kwH+)9^jiv{QP$cl~jFViCz#$TNA08xmfv-M8fO#_{@b0#gTn8{A5_li3C%LzakI9uGj -rZ{ul4B-APBJ#uf#Slu{LRKXkj1-8PSRxYLS*soZY8;~4rKA}lJ_!MyfE|D@dk-OEhzzk`p-!0`F)4P -m?Dml+|zq^jO3X1&r49? -PjnyRf%6ON=UKzh8F>Ia26ccntv2Vuj*OS~ggKA?U)88WWx8Ls~xo^Yod)0TKz-ENxrt*2whGnTxR53 -N!$o>6ckmRN~8@azf43pdxX(RXdF}2X-{vzc5{&17z`QneRkO86dfzJAQJR=V)fQgfeNBRM+Rb&`Ya8zlE^e?n{{LHEfml7nqBsyf&n -CAp6lJ{cpq=bWnTBBb>`nIO4m{*y_Pd*(lpbFT==y?f-~QqTWAvc^{$2;kJ@fZEN$#1y*F|# -A{Jn0Hd*<);klZtWub1SW`Fk5l?wP;0iR7O7dz(qFC$=I~o-t -R*LUI2>?HRuda#74Js?S*aru-~I{n@<(BsY6&PfTwB8$OYvx_6%BhEL?E?p-9g;S)Kkd*t~|jw(Wq>K^%glcS1CSSmGlD*#K77TeJ$?lGim^uBi0>_7D6?&*UG-KbPJE{6YHX7@4j6 -+ez;EyuX9wp3nO}GcCiVWLd+dxXnA7`@5)FUU~O-liVxs{vMM1j&Xl4$-N5i?<2YI9{2Z?+$-|_0g`( -~-aklk@7(VnBDwD>_YafYtNDIE$-SEIA0fF{^L^=w!5&P0#(4B-cYojGCVNr%A2{<`^|S_s^1C56m%YdhVYmxgMBf)b!lHNOC=R -#Hi`HKR|LlFvqCrxqpS^dSH$*ufI>8#LVks%9xl81wr3*C-s+o48Smpr*3U$ -*?OqCzd*j+bz$0YdyjoPMuCC2pl0gcynqjyYu>=Q{Bz!=u{rhDVrfKc -L~iZq$Z(_5*T%ridm+5zPZ~f2N2g#ti!bxj$1x6Ju`ufZU(i-56t5{eaw`+1?mqKK+2)pV{FUqpaoux -j$1@6Qiu=0lB}_@QGyo19E?6%VUf@{R47;nc)*T`Um9xGQ%gb^AE`VnLUp&67vtp{h7>sj17+u$o-iu -k1>jF9+3MpMK>{aIzAxxXZAV94V|luvB&Wtg+vXV;*PPu@gW664P6rxV{hX_3WFMYZWp5}=ph9_4L!G -uvDxq;g+2{k6BA>j;X?{~8amSxqsHhVg*&FkC`Lx-A-Ut+*dcN`56KCVv -wnfAf&svB}@W$lp99cWm-EG4eML$sL>gO^p1_L-La*e-k5r^N{?c$=}4t-#jEgY4SHQ@;493Pn!HqjQ -q_*@{=Zi6C;1~ko=^{-^9q@JS0D9@;5Q^HxJ2An*2?S{LMr1lO}%?BY*Rd{G{1-7$bA@ko=^{+{DP-J -S0D9>XTyBCp{!TX|^NAs7!iDe$rGX#mLz_BtL0#HZdxb9+ICll}RzOI1kBBn#!aK@;8q*kX-XwLFVR> -pE}CWq`s`7NyQXERgklJ)J1ZwwhFQ}k9tV1)mA~C=FvuyYqeF7p?S2KvM-PJk=*c!yvw5lBsY8_#wLI!4x#1I8mPbcPZum -rg<F9-St+;S<@EM`uZH_(UG%(Rq>^K9NCrbdltSPvlM>4UpXMiLA+^D>m&#NH%JctZ;~AR --y%8qA0avTA0;{XA0s*VA168ZpCCE-pCmc>mqTm?dnZ3%Pjb)y=QPk_2j%DOB=`J(-a&HD|L2_~_xyi -Oqak)ve%?)TeL7Ob?3%2gB;&Kw+&Z)Us$$A3DkdvbF$Y0aOqQq8&;>0Ov%R5GnBOviVx`pQLsi?%fog -$1%&8jCgouuVTi(N1tBKd&muDZ#o>fI|@^P<1ynep!QHbwFk2fjAoDrAna+j(pUrXBbp;k#?4tpz82|ow`#HZOvM~;R?+WXtC;Vpn(wKabJJ4unarHvW_B>ujP=*db -h=iU!{f{pv5uY5PfqQ5a%!BgI!z@) -Zt~G}dp4cPFV|ChZk$fe)HCxvnNBp-GjqzFv50zRkEGM-oqEHZh1bxLW4&S8+|ZF=yO7a_*LXUPlJ0Ie|Ehv{j~N8$cTA-jZwTNdl_HZxu;GhQn*UMn+RD>J9zWEDN$W+K+Qbo|LBI8T9-~}%>J6uM%6H6yh3KS9Aq -`awDAg=@d}xlF=jPAK4c~u%gkXJnK}43Gi&mhImb7v8?Bm?d^2;VM3$PKVV1G8)L4f(!y!ved(5f0ne -jkbWpR=C_m#<> -{-b2A3!=8XEhZrW^eX1OW#$uphD%1!Z1o@pD)P3|pknD!W3%^TV$|!rWcCj#4Qka_`ikkT?bN540GY3SQXsR#=nH8zF$ -ikG?6{(J<3R6^9U{Cmyv8_)=i77t6iN@gNJ$AJ)_FWkJE{wk}jK3~SA1F+bR$=^gVdAAC*W*VaGG(I| -pNt+LEGO(ZVdfZa(=STndrQ1?$FB8G-;FF0V!VI@q23q{|HNu -dzn;d#Fxt_2@V(w7|V?AJUSYvgG*+6c$y=EU9?$4c8{0x9@RP}l`qRd?GC}lqWm}m&o!;1%nf{dx^BGgoS>}3R2-=LV2-?iVHfTf1> -h!g|y67cNr{klx}F^!5}mNVGx>`QV^O$C!9!+2~K2AvkNQs -7c2G`O&%r)&7>*_&0S4SWWUQ~R)f%-JrIN@CmMt%DH=RGqD6hmzGG&%56v*^MD`a=owXC$Uo=B)5Sln -K2<=INk(os}QR{TTG9REC1?J77z_!`UuARtOdz9!-9z^!6-pE{C6+~tUI(W9}m_LO|)v3O#jMRY)GLi -xrVk84H%m{jT-!(>ZAbc`?Y0NvezN=~`V@_mG);8(AplS)KmZsY!_7@W~tql@0uMLtw2b)0>+V3*UZ9 -!t{6oRCpL-!!DP9rh#h!fd%C8nz+=7K&avcG7}E-{G|Co%_uCT6KONK9VBiHr#+=6V??GM}nh4b|F=q -1WskD7D39-G640X==@sx;T+-gjH=VF~(F|;*@QZ3l3`bc-@-qxpn*6^}u-Nx@}iI)brAyZpvqz$bQNi -W<55>RJV60)T@Toy16ydiA;y7TfXY4ZI|V%Zr0s`x(z$(HaM!A$ySh>Ix#1*pR!R&YP)D^y?JUjZw9H -km@!E0xSm>{pIVoeriS~}@|D^VJG1tc#fFK@MoXD}$IR+6D~;>O%neLIBQmjOBQj^UKFfgfr?~ixM*r -nKax`dQ?=78v-~b$#aE$?+m%xWf*RWxK0|VIG+|B^DGH+r4d-G2+;3I%<4{T<{IIsrx>rQoSXCwf!gO -L!(PDUaiyBLXq>}I3_WDg@%AbS}}fb3%g;ZMhYMiBmV9AE_DPsc$<5dL%=Vx-jsr-AA0R7XD}1&||*l -tAc|>qaW{bBqz8pW}=O{hVM#=!cCa75X{Fh|tezMudLOFe3DGmJy+!bBqZ6oM%Mn=K>=_KNlGh`nkl2 -(9ZxPLO+)o5&EH1vm2Sv&s9c*eg+v4`eBEX3H=N+BJ{()CKLL(&WO;@4Mv21ZZabDbBht7pAklcenuH -F<8dPs^iMD%=x48)3Hs@1?uMPKHf%PdVIzfxS+x%Aa#xUJFU?ApV3RXT0Z!yKz}AC}HEbo=Si@JPtO)bAF_P -OgRFxbhb}Qw)ymSZjiZ;;%T<#WFNY(T=xjj;)LXq=+Un)3>%-Cx9Wmwra|uR{@boU<3xnwNWtQW%r9B -Mb*5s=U;MOH}0SHWAV>!+v=lfa^mEJwh06G@i9b!_xbt*?l%GXroh)C~VU;qKn^)W^~-yd@aLOgnRfB -}R%H@X<{`uJGAI~?gC15gO_j#!lM#mW(i-et2yBzl+ooH!x=Is?$iEe5cO -B-9|WZVd_>NBlfzeK=8bS5zBKRc;3Z`M?_tF990;EGF=BZR1kVQ;u{;NY=R=HGo&&*iKO>gsK=6 -E&5zBKZc=n#3Lbv5TCDyfIi5tL#KKTwXdmnZY3Vrf5VD?JvA{6@Mo51X?*hMJx$(MoIi?NGP=#%dQGp -9f}k#dJV%1(5k@S}k>GiZ5zBKVc -%ERy@*D}CZ!=!n3#_6IAC_kI1zXr;c>v!lsOT29^rAo --iuveE;=>B}A7pX8&w&(=HOhGy7+Ip>~l7p4mU!JGF~M@XY?%UaMUsf@ -k*6_GaxO5j?Yhw$t7s5j?YhHfQlVk>HvAvpJ5}i3HE=pY5VXK5=64)F_Vw=FH-tz@f!cqdX3n18STI`W)qP!0cEK -3b7PA%Hx1J*2am1K6xB4`<$If=#$3*dj)ra)yS0}PcnklF6BVY?nfu`um0}44A?gpi;ScE#r6{ILaZ* -1vS+sUXcuBxag;r?y-K?f>xrZ6na!RBClVIMp4prQ>_o!C*fX2sfSpKK7<*=W(RLx$3`f~Bn?r$vLM# -@JvS&8u0y~l5nLV@FFBBAFd2p0Hv)KseM1p7b%;tb#ClWlfXSUaE7h)xFls&UKCfJDt&+M7)_1uM692 -{lOYz_+!o^2F)_Te%1%ni$P13ZthXEqlnIT3gsH?ad(I2NGyCTyt~xIS&+MOz)-__wdz`KNX`xRZ2kgb_MJe>jqJ7IJPw$nkDW;9lg9yjJ9| -+IeeyV94nTGyp-&zM?CtD@C@3G}abSsjx2Pl^<8i?56)8%=Gmiu2Fyx>VS?Tfhj9}e#d;=qB`gl7d2* -1WV7{MEicQS%%m+>w}@FL^gjG)?OoQ93kRxX!zXF@4*#N*o;5pTVN5%JbL84+*2ixKhGyBQI0y@wG~y -jt3V=2B!u$B!{0-rzVR;tftPBHrL6BjOECF(TgJG$TT9XBZKBBllR^YV1-ZB*(8ZB3^Nj5%G#cjEGkp -W<c0;Dd5TlH@YYa85;~g49x -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 -l&BbMh>@O+vP%X2DtKFf&ZITbvgXT*#Vst;L+ -l{U`AVUy9OiMroa5v~(C09Z1Lhp3u#pR%c^ok3I60Bvna2Thj*}A!o_QQF=Quf$;F-q(bB>b}37&Z -zFy}Zqk>Huf0dtO%6A7Mq9I!cxMlN{faloA86gCRMGmiu29499dJo7kU&T(=gM5MzUkrp;0EifY(zRZ -YOjc=5q8SEy_@k35zGByo!I#8I0CS#M^a>m^5Z^_Nc)#0-Xm-th5rt<8{4*nE^?CI;Ii`*{z=jMu4Co -<~I>te) -=-_9E_&MA+qg{+tfox$U0dkCy8jw*&{7iYaQ?<=|+1aksjATGAGUA=>Y2eX?FaofBd%x)fG#M{B_mOYGAfE;9`3gi|e-VSEBZeXMaiDb@n=n6 -szOG6Maa9%cmk8Q9JU^mCcL3-XJzS6HXWFV0@=V+8uSdV&$?=c@WP=x6XUBhb&_AS2MvO}1;4_|4vAP -l5Wr*_-@r*e5W1bCMD0XJm*G=x2nz7`6<|j<6TQrh(b9Ym7iYW2yzv&-i9WpdUIuFBIpz%}%hkan9Q; -9l;ffbKYhr*mkgkVD@$gBVIpqz1*{K&f8os_eSh4nA^zjgY5-#yRR@30Ab#6H^toE9!4S{+=FpN#oXR -gj8uTIPsB|XbNkr)V?V>(KDIaPXqY>6l@YI>xx>sGPNbVV%p*EZq?@~>THy6FH^6-kC(_LgsP}<>F0- -~{5y!B8u)SjL%5FxWpDWxOv9)4Oc?#^Sn7gXJ4f^4A&Juennj6@|Vn;=@_W~mc5Vl0@rfBw#F;WM@Js -3MFnj6{Pu#cj-k)tQ!-P`laUe#B>_q-kTiGkc#0C<=B_i0c>OeY?`I?e!s8qkN -}9XbMp4S!+|3q`V&3Lnwkj;8H23ag1o}C^n!}1p^S}fn(9c00OR%ugJjgmlRd4f>{0fh`ex`RC -iYZ(}8DzK0`K?B}2FVb6yJsreq&f(npvM!bIJd-+YVuYbOm`vR7y=6l&wU{A>WM($ZyrJCQ!aTWIW&u -?Tqz+%<>fvt={KL>bRz?#+kagJ_r2I2g1ep4)6%^zp3vE6_EH1|fVV9lT5K8G#;^Jm#Yu#h!>j@yP^| -MOg%BTgNh=Xw!w=HUDw&lj+)H9yFVV)y?%FH4Ej2IsGHjD!;a=C8Al$0FDK4YoI&GdMrW`oUV){22FB -LOb4f?sru^ -6hl7cQ!{K|hyx_JPXog-bk}!qV5mCHB3j?q0aW;{sN|76v%;g9`73VQv8y!WKrjH=@dWLCuh{9=0&5z -76^r@Bf!wlKk?OfFK*%mEhA7AD!dpbC6pQngL5uS6{lMRRh!?&L(im4Clv^trq@X}I6l%Sw?rTx^B -{xTwus^%=E^j0@#59U?N8os1ykvbBp5y@(LC#rkj?C$cn}#h|Fo6sSbcj_P@)mznkh3^fFFkRiPe=0x -67XWDyb+BXqs7B`}{Qha`Rrb8w;qGzY@{40y}a%~hu_EX5wO?9qk1SczYZeRq(e%%)s!KuNW?Tp~e;O -|+G_>Dl7o<53RaI;^dR5t}~24KV4@TfquHqF#`Q`oo594>Ea$ -NuEwBszet>RGS$UBSlp1&#XVSDP1|*u5$LDu3M0@@7xzv8iW-V>`l#aBRmI5sr;JXd?8(9aLO*+sR!j5sr;Jj=1%pX9ytqE3%Uhxh9;4;G%)_-nbWpZyaYacFcXx3?Ne^=%t|#eYyB3#* -bnRzEI8e5DabZX|hZ*9!kZuk$#APAf+=IncA>BMAiHkzIc}No1gmkla5toE?b1xBBgmm*TW{%*D?CCI -0B#Kp4uwSm>rz-Oux^D_1lFUls= -ziXED_jdh1CSMMPYS;ZBtk(us(%l0^6>zhQM|xEEm{Lh2i9fZ?9@S;OvJ#w(n;wfd2lt`IN$-zd!bmD --8PkN$l$e}5XerZDL5-#Zj%0rdCpTa?}d -=P4dr1*@Wzi --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 -ec4%%^1znHczyj-Pegf;OtNF* -Vyt?`=rU}cdtAEXaSBDzHvADWs6%PnlTwU{)LVWLd`wa&4iPug9$Mv>1Bq|{^Bq|{^Bq|{^Bq|{^Bq|{^Bq|{^Bq|{^Br -72_CMzK{CMzK{CT`z0n7Dx3VB!vLgNbXnHHJ0az_5Z<-N4X-b=|=5f|cFS@Pf77(C~uQ-O%uY_1)0$f -)(D-@Pako(C~s)-q7-b#EIp_>o2stU;=J=K|Q+V1tsa07q7q9d7crs>8*(Ma`8u`<4W7ir_#7)J{6n2 -r)g}4sx&r-)u+|PWd!h}YEjOe=Rv|157?_k98ToF9)V#M-X5j^i<#PVDbJnv(~@>~%-A7I -4tToF7UV#M-X5j^)ZVtK9zo{utOd9Dhck27Ly23$rIp)s0v|1PXWS3*^ymKO=FU4O -5HtKj=2!Ui9FBuH!tg3F;iaJ$#Z8!)1b^hHnp_X2furporn -y!)F;WTqdYn`1bI5Mhuq;>J+{`e322uWrA{qZx0VJVr(`+;la0uuP|b4Hi-mv!;Dz!B0=4CMl5xapzb -Ckmbyq#H^PXeE)vv@F=DBU1a%XPSn47{-EBrJbt01M-@u6J?um%x`a2l0Jc~%Kzl#yevxwySdl<1ii% -712BO{h)5y|y$X2kL=BDwypj98vUB-h`^h~-&Ca&$VaLoClClI!2ah~-&Ca{YT4u{?`Nu74jRmS+*k^ -&eow@+=~`{zHsdo<$_r-_MBUSwwREM;Wm^i%72jI3t#45y|zl%e9eQA|kndcDXi^OGG5s&o0+Sa*2rK -`q|~$NG=hPTtB;98_6XilIv%eYa_Wt?91)}_qZoQPog2zy&|akCSFrz2Y!u{> -ch^Dg(1nhA>%5osP_muMr+L`0fL*d^LXGZDe#5%zL6cuYj_c!a&24IUE_JRV^$XM@KC!Q;0_*vpv%YM -lsnbcDT}xiC3RQb8SiIdfsM6A9|r%b5$4ok-XbYtUSn>_mcR_HyRJWG50lvzId$COeVm`R*A8yfxlE% -z!8Ru3`ixd5HnsFMojnOE{JVKR&{M&!K+IJqgQ#AG7YUD){XHTXH72=g8DtqU=P1dyY)aCCW}DxM%le -E>U(OVdET`noE?ONZ2?>rsfi5ClWT!zS3N7oF)x@e6kb4l8>@SG*=iqkzjK-BR&uPaXSMzY2(H=MiTs -TFC+SN7bn6nbaWFVb{JY*(Bs!cp8VURHyEj!=aB$^pSvKk-yg7oN=9X9|(sziawb*_tF|* -y8xsNohHS`(mPGp9NT5f;Q+<=E4W_>^&7S({-Z3+#6V_Pav!j#;?Gr -%YeZLi>xh=VW5O!x@+i^|s$$y2%KJr;7~u<)Uu~b`nyxzllyg*hp3q6@QQFz-iMsP0Dw&RT8 -T%>I$7{R$n+fFiqbCI^4Vg%Im7{R$n+gL~9MB#0 -h7{N?p+W;dtNp{<1MsT9=Hkt%w;zZ$XR~Zrd8DvE0XNVD@pJ7IXey%Yh^mCmNp`RO!2>r0pi<5r0-C{ -)OXM_=RZnU^wYP25$LC{oe}7#uY(cjr>~O{=%=rX5$ -LC{n-S=zuZI!nhpr)Z2=vppkrC*pZxbWXPv2%npr5`ij6gqq?9niTn(Eue2=voOnYt_$`q|Ei(9aG=g -nronrb0iv7!mr}&4|#?9!7+I_A(;$vyTy>pZ$yo{TyIK=;t6KLO+KX5&AjIh|o_zBSJq%7!mrRxo?&U -{qXpc3H|W+lL`Is_>&3!`04p2|M6%E)#aZ<6I` -}fXBH^*a44onXm&M=Q3djJkDjp4tShv*vO_Zk!og6?sg&*sWwb(oIlHY{40qv`zcc{lRuli@ux~1MU{ -1&+Au%Z%LtBNJ9Lc^T*gS33OWRrF%GM5!==M?A(%tN@m@oW;L>igJBQ%1GP+vJAvnbAwE7)fN!Ftf>| ->#mQyqevcw;pG%#LKtH|eO>rgW{sBU8V!*;J -ep8$hFnfJFBRHh+_C-d}OZIJL1Xq@xKE(+1vw0&UIHa&=KO?xZ^w?2Ga0s62!Px#kcX%HoI7IR~YYto -fo14cO!6Aio%z#612;O8jBRHh6`z9keoNhB~0lWC;cCogxg@5kiE=F+mb1!QSd-j`$&N2f1&=tlG!Ii -T6`F*fXKYxII6*l9~-@L*Iu3S9KJ`sEH7Y5Wb=s!2vQ()hH^TZZLa7f{4_B+^X-@I{*5gZ=a$CilA^$ -S~A3s}nM3n?7}|8{XNBRE9zBwH``(a(;n-@(<~gWPItoS)w?$q4-0#uJRdzn$b>f@S{sVfIMa7r$_bt -rx5PvnRQqVk7+QplSg&QJ!P##pd_9n+F-e_Q`SY{beK>FYir0t_2w9uCa}x)S!8b*}+cw1*&Rw2<%|< -PDbEI+to8T?6rsc9Jb5P_HbW7{Xz4bdIolIgKY<82=jeBW?^Ie!T}zcP>nEuc|9ZE4(2xVxLpx;!1jh -D-TXz>Hg5;t+!|p7e)J0aH)ger#SHSUer{XR=M4u>EPInE;!cD^?c^N4|(hWQ=rN3rXD{sND*$a&6PW* -?87Ctup_5X2!v9K#~#Ik%Z@2RYCA%^Z0n=Q+2HJuGsb%{}TF=x6jYBZxz~Sf|K&&h{`n$a&74U`CPiY -!0(sBj-84kv&o(`VU(?a-Iu4+#3@Shj8CU&U1DL_fzCN`5M``~Kst*UMCZ@p{>;;`{e>f4~7ojeCb&a>IU9tkgl} -XahOVIXb@9A-Jt@RB;VE=urAWKfR*bhJJdL<0|Al=k{`b2RTn2bzPouX1^VLx+#N}1VB>?yDdey*SW0Sad3j`FzO5cv>}L$DEkZj^f?cEUHes}`W2?qM -xpFMRVj=aCv>p2g!FcEc~Icof^=`7#oRV4Uk^-mo8j;Q(7NHpI`a=eA)-{OmP;AMA*qyUCs$JL2cZ6@ -oZX^+xQBpX=gW0Cud;9pfr%?TcvuvLBjB)=(ks?VQfFNTfk3u?ZNZR!h$R14tWwy>|l -7WKJN9s#jIeRg|4Bj`Up+)J=Iy}6UUKendN@8jRW#`J}Q9PeOT`obXF8#bjcjB+LmThix_sAu5cZnDL -fBHzn)fX(R5i|QG#pXMc=wV@QRImGiHY(sBu=2#h<(3|btx3L9%en2sbeD9e0Hq3*!@Jb3cpU?J^7E- -KQ&Gz;%0{=F8h7t6i&AS*u96!955%{;uU5tqJk|T`3za6~Bi12S?jG+G<-@=HQ=Hh3KljGR1P$?D2z)=#n-k0`CWyk3d((I*~}vF|A@5YRd&iVD^VLF#@wcwdA4V@$%lZ< -6403by~d-y4UH`jG%j+>0<=LQ$HIjmb;omlZ?RqO-?YP3sK876zh;X8L^7R0@wV-TZ~x6V*P4iz4|cX -wGM8e*#hcB7&J~^VFZ)cljDqlwNvL9@vJS}VAI4R)ItaA4U13<2PsvNViD?_$;*tuDRrD>1e4b;?%r6 -0n&kz*6pK)^bm5>w(0jJ-UcWr`We(5vqBbZ4`@8O)i{B#p2Lu3dd5hICPr58y1K7wmpYn@!<^jA -1n?v&#)a}aj1Eotrv?!%?leC!RT{EJ%iDQ({?EqhnlyT9V`wtN7U~?KND=DSR9&P&sK%Sq4}NxMvy$E -n++X;C59aoc&1nanm@(*!4lB?8P*S$faWjp`(O!Z{t~wUOF;9(9Hd|gX#N`84wiuCCvPx<)QcKkumrR -~_wYFcq1Dmbj9`>G#(^4^faW$_XT)pa&!;wXcf=adeD8inbfH+8Vu|OQTO7b*iD$N(e>)MQ2zx^;@q9 -DMc8evR*)FyxEb+{Ct7kBZZ04{VOFXliIaC)*JZj_-OFVn_FoIEpQ&Oooa#;D}L@dFv2Nj16pBQEYQ} -@%Ij9}_ -tZQsu9JH;mXh>{E$*%DI>re4p&HA@QW7VkQ*m!?A8Q*+Npsr|GlFPh2Qw;`lJ>AS6iZ2J!XxjkWv`2+ -q`AX~7(q0lVty`2?mXhYiR12V=F`j5*DM`mBSW0TH=lB^*Njhr5Qd08>N6B? -jvd5lJEG2E?`2d!Z^n4LZNzEODj3AoW&9N|+lA61DM8Hx~b3gkxEG0GfA7up5gqpF6rKCfw1uP{sxg0 -meQc|;DeH)^Qe)i;8N@||ugoC)Z_JU#u`cW~ESV|gH>_9))`F*gIq~j(mB{j!6-o#Rpj)AbmGryJlDV -BKV``ALT#53Q|{Rc}t^QXC&V2Nk`3|k16cyx4xC7$^U9A{vON6((($;+en5o -*YX&IzGb^&-`V!DlGBn_)IMEjPv`*B_8(IVu`1lM?xLb{(Ry%s{v~~4&w0)i#&fm*~g)kxK#EsyDO~ne6yYhb*%D -yv!0Cvt2}=`y^-fQSmycj>0zEoV4dgBXH|j{3q1=H9gILrlN=?OoycW|{P1h9G2oebZ50Eam)BM^5aF -xTBQX&5jHl(@bqwfj8fAuJ{m-s_c7vfgSsUz}jTYyFGu40x@7v5Nt(l`nQP;C1rFAqKogzBtT)*T)zA40x8lILCnR1 -YcZW;AtO-o&tZ%a_^_UIRC{4hB5)QGt>a|`M7>ACr~|EfKRQ1Qb05YWXO*?Xq+J^XorTNqMaIol6Gkb -YTB(KC~A*}psKwZvOO31(J%JvPkU$OOj<$E)jRIb`3OVM$Bxf$Gn5N0DyR@FC`j -}h{sg<@GW~x6UGdwy~o^O2PHIKZDb37jM&5bE#hG6w8jxP{fe$57e`0;Bt -8B_y(&FVzS&(}SVIW9rX&({?FXDICXx{Cp?(62W#0J1qYK~2qL^@Jz;%QIh|WvBt@>pct=fFARUqT0n -h#jK&I#X+7o3MQBL<`LI~STsR3yec4O4|Of>b(udDseDa6zR@Ol`0^4%fq(`W5)?h|Vkm0Wh(?=utH+ -xd@&U@1=X5iw0#eOL1az698X(=mx`0#*QvsM4ifK&@}0jU(~A?i`Syr_N)b1?_0---pKehd1#q+S_wu`e&_--5mdbPJ)c0sR#8by+`^3P?2r`ns&Y5c(R{UziI>e -PJOWRwd?VUk*@I}(AQ1<6!djVL(tcV -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 -bz~>2#YXhGrG_DPNp3t~9@Z(40TII)&#mIT -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 -3ribPGRD ->VIRD>bKRD>fW#tftOV=#p{+;D|BsIZ0jf#3^qlNvP)Mokl=rioG0#HeXv)HE?_niw@rjG87!O%tQ0i -B;3Ys%c`?G_h)$ST#+o8Xv2siB;zT@QOHrteQYpO(3f#kW~}NstIJ(1hQTN(d}RwF-hSXF-c(?F-hSY -F-c(@F-hSZF-c(^F-cQ3KroM(q;QXzq_B^er0|bG($180k3Z7V)&Pl0$pEK{lL1y0Cj-1HPKHbqI8wM -(OnJCcOnKN+OnLZHOnDenOnEp{OnF#SOnG=yV$8_Y{u1^S(+d6+lPA;k*c1+xXi_x{xtfl-nvS`ej=7 -qSxtfl-nvS`ej=7qSxtfl-nvS`ej(H#*e@4_a$<;K;)ilY~_!MfI6zY5ft`#Rxp(aqFCQzX!P@yJJp( -aqFCQ#ue5Z?{~B9eN>7Ln95wuq#ju|*{Hj4dLmXKWEkJ!6YVTB-r^3@0L~XE+f_J;RAe>KRT%QqOQAl -EQFelEQFelEQFel9sCQ;2BOtMbB0uDtfjOQL)tI^3=1Hh&!;An2K9OfR^Zn9+8V1-(pRN-k&(=#}4)q9dtsHd$SVJ)Zu8k7{Tod;Ra4l>IaP4aeaLt<%;955$z%_18K&Cu -7ydr+`LVNO3d-8$y~V`+eoLtcoZ4Fy@_R%@DazG>Z*aBO#DP5RHSyiux -)+J>2esEk{pwyUzW3H%6W^SJEQgP -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 -kRRA+)xprX2EcMwq?rLNR5(6sbd6?pI6!N3g* -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 -`vXxRz=n3y$gg=?JI`6Hh1H)F -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 -6`YG`j=_LmVPgR_HzJ7F&|)S0#y5reklRTdXp -Z#)xk#hlm=167&DgeIY_fiPeJ*)pSU$LxLrBl^K?dko7gYpyhhE;e*AZ?oJEr0WcB;6I3lxC^28bhOC -nK;Lz$`48xgacb1qP%2->Oznz02r1znL(~%g+EY><+CEa}t;9|KFfb>2hUDK!ci8Ll3#TwFCFKMSwK- -i?s@GGc+!j)oh$i#v;V%DOK0u6HjkpDk -3;dw;SX85KlECFVbKP7-v_R?06Iy^!S;gNox2Sg~#V_ye;HxsHojSq9kh} -@*Qa!Z6sc{RyIa1wr7tNTi+F%u_w0i>UE=SNV07Rto3`hTAKa8qBNgBQkpwUrFpuZG6sx`XRl9fv5JyXtY>W(?J}{Rm56m$inaIradO?RA=g*0*AeXYUs1 -3xrC_)Fj}z>(I)Xi2N3hTSih|wu4Q)z%Cr4_DZ1Pmz(;(}himcrlWR2G&Yu8WKVHH`u@+Md#;4$hys; -a+VLo81gIT&Oeud75))&(16ovf=wfBzgxbktp0iA=JV_^4%6`Bm$vx2|g4d=Aw*>aVL>(dSUDqwRH7> -mQ#*wRTuHIf?FB2vWDBl=G_k()LfJ_XJ&4Z+TZ$J@{$+O`w-ONoZ*Z@0l*V+EGS{@cK_6y#A`f+pZ_P -{yM_j_6zU1F1(#Jg}3tr!rNI@c+ctyZ)Y9hJ@X6ig)Y3>1F{m~?LL9FdnyeTAYtc%& -$I>WFg5&|;mby{}M|hoz!)mvg=@z5D`eNnB({j~3aN*1s<%vdZ!PmsX_zo8>x7RrEzfLsXS>5MNq>z5 --F%r@MH~+KEU^nsXZ`cQN7HCH6q7-`!)FHFUOUK`m_&Iyw1dYHzb>tKdsH++weWC~od55f8D&eJR46< -7+64Z*(71rvZ-d=X8X}+?jD&C)5Q0@e-<3ezHoM0Bj|Spv9Nb^DVh}cl53}-+xEZNM=zS0)4p2Q>HQc -o6Z4KnsPZgJYwcHmvB6G7U9lSp5U_?dyWWdB(l9+c+88Pkkw9R;TCD4ZL{|x;Tj_}PLSlLFciD*tVf` -5dSbk$WkPlM04y^9pGAq%c4r?K2lbvv@Kzx$Ll!YNOls1^)NePK-3m_?j@X&S;#i@6Vga{YT@}Y>u1I -qGH=jCT)skF!-UfXLPt#*kVJVQK)L%k?J!WLnR*KZMh -iG#b<%%wHvap6ye%BmXkmVEtekppAt`k01R82Q%aDi%&zSImfTlQMR&0j!)4`ary>TfdUf6q!CI&4`? -zGHBP(AjgKwVRkCefRq~ -ZDY7r<1(jWS*GQtV8rXVDG`R~8ODlBY^`Jm0A!}j;_}-8F9QE}Sdt^!Y%KCgu-j0Esh@K6a*Xj+3H=A -BZ+-0gNVG~G|9asb^cM$W*^zE<;xW8(-1#i?l6Ck=Bc8}l)>@$)+R@2@Z_z{JBF9X=D53kNi;%_r9+a -M?058fQ4&!76yxTSK7KzflXnopYV5sL+)oGRDSQ&0r@`+yT|JGaG-OQ}%n2O<90(OyM+J9C?2WIEX9A -G1*rzlqwso-Y%EdrX#|N3poa93Z^*A`PXMg~OAq@O#X5GGb(SvQ^?hCyV0iDGg -YLO+^YG>Ev$+<~fVl#}{}OPDTjEcInq -tW(GjhdT|CN(8)yzp-r1%5rO<0O2mZ9xRq{+X+=hpxu@!|-v!w3{_M^hT6#qm(T*l?39O2QP`uxO|VQ -=pC)BM67#QIVDr(^-;!r_01=(z`K5){wD8YgE3*IVF&;s7G_$G|YKgphYaxsd8=+9yq=W(=n$ -=FsvlCKOVCbMeHm)8=aR|p0R8QnwVKWO`eu$vY3jTJLvLEFA6GLMP_(?wny_R?+2{&aWIr!Wi|7n3bnIkc -RAvuCGQpEbtE97Ofl4j`RQ~p$D>xPyi(3m)L_Gub=_6y}qL}M)`wzU_wG#C$HOm8|K^5-VN$Mm^iF{S -Xy$!b{Qx&Gvl8G5LCW;Ktr88o7ewiXKdfDobmSU#BU6EQ*@F)NW1v#2EuzIiz!jx@;Qc -s`f@D$DK%GFh-sqpq8Mt53ar9Zx9I(74_L)n2?$-+UWTjBhsmXw1DF-*_?4XWs;FoOtvm0A+~XtS4e{H*Eap{iy4rVk)i40pfEc%w8G?4Yq#mpYq)5-uLy&)~ab(RTJ_dD;ls<`|GiVeBl|K -YVLNQjJsFt{30IMsO0kG1ZvWlT$9aL2d_DwN6Yx^YMgGZtu?O>ZaBBSCEU9FJ^|0Coaf`^Ja=8b?X&+ -Y*B)`s`%5`5DT@FBANh5NZt -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 -`v`HQOf6#6`5Et^bkw$eaV= -WQuKR!u1?p{#+j-pCAW4Y{x)vmgtwTLD?CHSU*t(5+HZZQy!ux3vOkCZ$yOA_uS -Ty_pocuE6daG9WB3K1^X@J^LxV=atBBT6D)0J~S3}=4gytm`o=V_}No}txoF+&c^_1+4znnF -?887Y#$(ceAQQYt$I#@K2$0erNutyV!<=GnwZhO~@0$6+VqWoG2Bg7%Um>4Z028ND -C*s(|w8Tf@F00k3Oa@BKUIdQhtwY^`m~?D%*ijb$5Yx -9yr`b#+|sjmhi$xHvyKfAc*TZ}ab8XKydg-|+MEi!&~Y?6fG>8F%b1e|iU7G+OAMp%#T{L}nPms@)ZJ -t8%9ztM%p>Z9U%bwLE)yoNdpT@WgCutFp7v{T9p!aUZ`*+?UmR?*e*cjyQ(-qc~0>I=T2E@)Tm1iKc@ -T6I$20=MX~pzE;AmVTw4W%h}lq{xSzkagW#0?e0y17DmYo-~}wyXI{!2Dvk*@1yx%zpFPVpnD7t|(bd -%=N~Um<5YtchvVlTk3L~fyvK7y^QVE}aA?ZTsMoxGEB6V1OSL+Isw5T;;x@K^PHio; -oY@9ima?2p&U)q>!jflX?Z4v@QxGtk)13qweO{o`F+q>LpG+GmO>O!#+JHGl-)FK37*)5r%CM9(<2qp -=YLWmY|1Fnb)UBYiZet`jn_Ye*D-|e{@fvKApmOpuRX8Q-53fFrTefLy-Ji>@R;GAfhy)-)+*3AOLnX -jY%siT*4X@f9EVte8gVBJ8~W6BNh>%8{{i`-;)D_B#DLFRZ$hx9R)j`qFb9SI(|y;{*P9u;3KB|hia= -Qv%*E>|Hul0iA2RgbF~q1C*ma{w2E17s`bEkK&%rM9Z55o$Ri>%JE2l&>|8JHvtHI_{r-M)&Fp< -WS{_Q(|%^!$L~A6wNysi&DdJUW?VB{v4rpHa$xeRYZZjv3-w@&BP0iIM-5s{0)EFxS9BiHhsiHtnEw9 -@^JXROn;a7qGC*VW}L(NM(ps@JQ0Hd7jKPrL>MFP7?BwT6I;s6e@N*3H%LC0SVpL^3|#<<=&sqgPc-E -3x;Je+r~Nyib8ywzf5kK`@y>6xt^?&PB6u%dY)_e9=X|hb&g()E4I;f+r%eSrkr<2%cp)?_{U*}Vx?} -=3>$-p6CVJ~jj{XtKL6)J(2o*_x$vl)3*B&mA68c4K?`ytC;sJVC`}xwQg+{s9Mtq>A#uFuwfQTa-78 -+Hsv>~bgVCrd+P0^Fef_ -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 -(uhnBWSh=7uohn8FVVP|P>XD@SbWn* -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 -7uAMsl7;m*t9B1dv#n>b(HMv^j@1*Z6Ad -dh7i|*c!Jc4cm4Z*nhiVPk7yXK8L{FLYsNb1ras-8*Y<+enh%^(z`Q7NG)(v8 -`+-XGFPm5@*LiCN{>-&h8loLyIlDGZe{TleV>i|NB){KiExDe(cWN;m$}TKB}v$>s{5YEp~b(BwLD%G -x$!MBx9La@LckgHF?S_R$p;e^J*nov80>*GFiaaCj~ZPf8&)DMLybMyV3T9C3#9qKNS8jZxNEUKDi#pR{QFU@V4B{d9KnR~g -G&gG)Wm*R4?tcn#=Pcv~5tzac0=ha$2V+Vgf1^Nz;KF23tK7Ks>`@xACQm;#x%0Pu927e#KqMp#z$Ex -@z&ri6%Jp^_o7a4c%U_t8claLzfsH}l&Nv7^j_-_r*7kbQTQ<~vla)45oiv-7xM~bA#P!q3`e4UBBxt -+juBQv@ljgUVezqw&lbjj;a^el|yJX!HL23*JM`l`sa3<1~>awhWneDqVYXkQUk_>J9>A}y(K -OewneDdSb&!_R{{htm__J}oSvaTi!KF(q2cUnMD(qzFNUIi1zw%A`euwN`9^?uO;EXrVF1_3Y*u}?%* -`> -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 -e(wAFK}UFYhh<;Zf7rcWpZqs$7&8N|Y* -J6e6AR!;4IY`14wW4a9qu7g3t@cEr{g(apg?5hZjmT4MSFLGtSUCTU)l6lda<>^9{s|*jqjJ -=d`EGIG6ats0zvk$;}Q4YkLjOfdFAkLNzO$e%~60hui|_=hxB&TJI&Q;T9mKU6+JgmH9;esb4X)=9WFK`@LRos%C<1rhlbr4QXS2v3qEEZ -Cj81IvQ#&JdgjL5*7ATbL@U13_T)8K7{;Tu3-0xP!XVVA;mC^Wl;=&H57jp>Et&omdT}c0EY()23MD< -WJ<&_O*HG6J1Ur#6m0L}^ypN4_uXDK%yYy{+rA5&HI?~lSShIC-f&^%<@30@2~|d(tnOG^sLnFh1bRJ -QW&^wbUdBsxl;>&gyOMr^g=x@gCD#(sDMch2h-HCyqPR$xF-d$zu_?7nUoX#2I{pc|;xIhERIA}OP3< -ulY0h|jA&ZOgxL>Gv#!3U@X)T*z7$P9jzDC~>r>v&pQmKr$ZVJtgLRliD{u&igQj{_Y)vp3kG`uu8CK -eXa4M#j}iG_eVDGtL1Y?;IgYg?sDgit7+gn(8g26>f2YI3Tz2GYAwaw=xagiTDqE+}-amlv02aRZT(( -hgyCHGz4&W-{CLqM?WhBK3L&ea0L(lJE2L)>klhuLmlhL+;EAxBEA59VXkSNhi~-@fbaJ(T|D0 -NnoT!^pEaYzpsa5{rAiOeEG1V`cz4f?XR|W{79#HVu3MkIH>$oV3?nIt>&)lti{E6T{KlTqjP3y-jPc -u|VtBP{AoFFtkn5j0Q2?S$dz5+SU`L+k&-Th6-W}nvRu5wmZlcF5`rBe(qmY|eqd06!|mS -ba-`uCp?9{wd>PDrjGB9z>{T9g5MjF(p-4byC8V;BRsj(3=PqNAC4MOrMBbaV~d1j_AvW;8yJV -s0aXs`uV3fXGeaUB1|%tAv@jiLS`WPN#~$clprpJ#!t}mRiv(+cf0h23{J+o_EjD_5LDsd`A0l|Hs#E -i>zRrHJe5ViSo>hxyrAmN)G*XRAvp48+e8u+NKv7*wE)9t2Q93|vV*xQ`vf{*gqp850gcI6E|7{;jGM7-;IaIc@w -r4&aymj)WU&@RcF8!JZvmG5$vVqXODTbx4#txD`>^^y0G7EBZ$k;C+`6<~C7xJ`a%pV9_-O(MR0l_Sw -oKP^3!X8DiLQ<1&bG@9EU>gS*s(*yDMiZCfmu;C`J?B6D=j-+2d;j_AGiLqpd~f%M(G&lX|GaCfs*PF -u1x?}fnb96Y05m@YhSoQJ7X_CvV;!y?nu0Dp9_FyR(K0pauC)<9j_=Tl+01Qwd#G75Hd=mk0B#W(dZJ->}l4T`NyBk*UyhVzs~$wndZu -b=bVl%Q)DWdRqK20_E@B5i^3i`xLpPIiw9VFpV})ZnHLOga&(5_w!zeLIxcHG2L)wB9Cdk}&iQG+U0rCC3h?J&Sf}5C<07G4j-NZM -VS=O!hu2WJl1No9(@|LblY3C_Qx3)CWm*Y)9RGRtd*`oB%q+c<={ZJ4FBq1koMhSkPa)!i*KodI&1UE7)?Cy{}9GRwMFcyu84T*u>Ne>3mX -8u;@g*TcNsklr{HV5Y%uX_WM--+7~r`-Y60>6-oW9lEC-sgC6uyGL~lIZd~X)1G+Sydcw<6WhLdQc6_ -rH|xw}=uZ{iXIEyg6spEFBYCw#yPb~0{=H`d@F$DanCo>t2Df&}&ZZ;vedl}fX^rGeXY|)f-PQ#(^sr -XumS#E -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 -e^$~6J3E^Ycy1-QXj)up-*ZK!3n&b%tH`zQ9_6 -cl}ll1%Pa13Q4Yny*@1Y0(+PddBnpS<4!YfIAAF2(l~Lp|DC;FZ@etb=2s}K+r34Oe5F;L}sYN;-#s?2id>x`o79z&gMIa)qlg=_uA-)xg2~Oib -m2hv?jeYyI%G5z#{P8e~!<_kvo{c2g?zraT8MA2y%Q*_M@n;PKP>J1aLesph^_n=RO`%ec7F;x_1zbB -a4q*5e1W|+{>j= -7UGp&{Q1Nsly5;Qib&-_sA_w-P&M`22v537j9VV~z+Lx8x&r%*4W_@LK@ok}I#uquwEn_lkZTy9Uaj$MUg}gsF>KWXOU#n`X20rV* -pOtLIR+p!_XNqR-SkUxp)A%1w-IE^6wqq3Bc3H^P%{KU7Qq`1Uq#@MfUx$qxT@>eLs)~tOJ8*it2-HR -MCL0RCp&cUzNkx9iwrA=v{}gb$bh{Z>S?|B5EtoC{eR|78gXDjewfM|nnci$a*5@}E=Kp9mEWns%8<(XOEEPmBW1gG^xmLBNT%ft!>=L{oq<^ -)d(Jt$xFTtjQ(!Vv6s`6e77HqMpy?BoReE>msRC&B$E9?FxbBP#*pc%zv6bV%;fTV@lyRQ1u --5){(l!9-yqVpIqFhWL5txPTFllqG92k9AWI{0p$rxX-kJkcsvL2zJ4euJX5hQhAsY^Y`q@CAQku|v1FS%fJM{nI6GJC -9Z9tGNb-|nI(`Fjw$JuY&=~~@p;Tv3d7Xr;|3W>s;i?sofu&F=ie6b)@IqEOOQCsUczzQ83$ahJnmV_ -v7ZMZ^#ugF*M?*lbp1|OZZVArbw{9-zIF3=H_i8r=n0or64rbd?0n#JeIZ1NXSO9KQH000080Q-4XQw -_0J(252C00IyI03HAU0B~t=FJE?LZe(wAFK}gWH8D3YV{dG4a%^vBE^v8$S7C44HW2;pUvY3SL|$AaO -+Re#mH}Skw8lIKu#*)i`Ck~<;D9;xMc}{RO;W -(3C>M|EmShaoh>d8C}E}#|8Q-Dl5iPcKtu-JFxBn-23u7@!M2Dbm4(W1gb)TYk#pcr2`?=OiQX1vE|^ -TX55_k5F?{h5pdKv~5|J21hwoY24w!WgudOWy=jWTvCT7mjSn1Vy2-%!Z#@D0ieDn^Tb>>g3IXBcnW0I6aUy2Ck)=Uw=7I -3LgZ@Ogay>F(hku|5xHv*Gl9JetGZ46g5{H{<*9-4x$$VL1H)e~qU%ec(tFZo{8SP5z^kf+)m8g*hjZ -d2@KA73YvCc_K2AU|e!lu@#4v`p&f^5tUpQ!q7n)bT37b6v8q~?}&616zU8&RN9glR|rtQnyu+{tlkb -ffKM1qP?_9x!hw(7;7Dtwy_KJaE0`+DJDpC-GguglTk3_WO0qT!61a|fVFkV5f?Jjw?sv*+81jD23?_ -$;(3{u;_2E>|*!HyNESj0vkJr%j?EJ4E4;>qBtI{Gx;^*XMk7g~Aq`RHQ8<%$HxgpQe4jM615dP7Iv_Az8@kV%(e3bIa=*B~o -7~Oj=pDbw228I2YhLbG4qzlH|4lfiz)ZYvPZ^DggV+O -N9|}xF$|=uoV?4G&vZOEBPVDPH9D6Ge$(kn9hYV7KM){sR?!IV7=R*52$jKQ;yv -=aW&2#H)?#0-7rlBmAgJ{u>Uh@Q8))Es*i>62Wdi}TWlA#LvR@XXc%qNNK@PFjz;?P!`m|YGnTtCG4o{3+<@|S -ndk`?^rH1_*MVf-qEFKv(ViY!D70RRBy1ONaW0001RX>c!Jc4cm4Z*nhiWpFhyH!ov -vZE#_9E^v9RQ(sTpFc5$Dr#P91rYK~rou&#=p0;XCTZ=@eJxo*NHWzB<_%g>O^xI=6A?ZS4NSmL|&fV -{y9nSgH&TdG<=*<|`l;y(8l~mI1IOj}eIT;I*9;@4e^kZG@3dYzGd=ffQ2nIzW^$oZkltx-#f4*MKL2 -CH6oSjTUa5N3uxM~|kjLFmkMR#ZlNjjD~r5~q1;bIEoGX`!QwKLT=!L)+M*lP)BbsK^cy$Rv?_*W^+O -);$J{3@m00OhEIx@AxfLO{JHuXH-t+Vo^H7=kDu?S}mF*io|0@|qysLuaiF@3>as(O;r9ucS3-w?^0O -ibN8mjt*6Uy(mWiBZ}24o%egaSEM*Fs-@%LazG8nES37rMntqEjn7uoF+hx@@IFEZlIRTQQCEnZ1#djm}98I -tvKX>s@cnBIxB{fbtAGX77B)vF0F+tgNr&0^0dJ|=opcU&)(A?@1gG*O`Kqsb-(R*DkzJA$1`qOata4 -W1_=23iyIw2EzT-i_Vx#0ee@Uo5vP)h>@6aWAK2mt$eR#Q9AxJJ(b003zO0015U003}la4%nWWo~3|a -xZXYa5XVEFJx(QbZ>8Lb1ras#Ztj;+b|5h`zy3u;$)t79}r+zkpTq;^w1rKT~Z~c8#cCNPz=)S>qkm< -oV2^o7gOTn<9npA>jSX~vA`T*7^7YA46oeccD!Ne`UYDV&vAcbY{wH2`AhL1%*dknI`P;c?3{PcKt&B -r;;fO%#h1d!H=E7w2pofPDHX`k*$4o393(<7iGjupJXVTQG1q2w5oxX-mZt{37$r3xX^rZ1O{o?~T}%fm^dN%;>vfO$%=r^;-PZ0Z2f_`rr7M)BkgyzZc94Bnua-$ -SaM{JB=#*JpuYM2507i2K*mMAnK(p&oNZd1`LN0xwCnY+`^TRXDs@x&! -x(f395%y-OA3LnmKS_vI__A#qW{!Er?v!a6m1%nG5Ni$ZhLGRAXy&<`|ZpS?;nQ6N$Dn)uEs}Ye`Eh$ -Tj!$6%un--Rd&@=X$P5xqNBbU8PmzNhikDTzIVJ=h*sR1ZsH~-S6Z%-eF^Bx+(6HZ+ZjRo7B5%&F9)V -7iMy>D?b#Ed*k&p(JRi;c^IY7`7ylHT%>8%8SIB`aqlfo7f{VXUO9KQH000080Q-4XQxxznc(?>=4vt+4u^vQ!zo1=ub;TApEADN0bKnm_oZUlAQk*T -WVtN_kExWz%*J~fKZP+cXx1O4S$vc+&r`Om*Y2nai%9LwJkm>9M(gRrQ-VW-YIYLy11zp56+dKQ#-qf -{zKEzOsAzbsY4PVDe_n9@GzTtJ*klQ6P;pt}C=B!HK*Ml67z`4c61_=S96`_DXsk -w(mm;QN!o+wwAn^CX)E?|M;RVe{dZhm2a)Rnje&a0f$l1hd;3lYtHMVFzcN^Isrlngk*zI!St+~b{yK_;!p(}rj@tSv=SPCzAi%dOVmV -&V3M&G&iX*O9djOL>RWFgCAd-L&NglHZP)Wk$5={~*+!ZMSQ?;iCWw-qP1_C>NT*J0V>ek${0{{c-dq4>FNfiOW{Vq_B7E3;9yF15a<3h{K9kC_KnMN2qpLtxa2ayN;E*u*A%RmHO!|Zt4%v7&!Trp -4h`S+c-}}|QcCN{Awt@GdP3~Rr*t>wh3V*f=9*B2CW~&GCi@Un6PsI7#+IKb^;`zkAT`yhd+9vL5zP! -73SD(ohNUywrEM2%s03CRGTLb7iAa`ScZXJKVfWg_7yL5vGo7}j;3XyLBdPdeWKXB)F%b8EscmCSjI6 -&pv$gaGVySnjVN9WdA1ruNZW5oFtUdU!KTQ2ps<_03yxwn4s-Or1FEWG8l1A|wN0b9DWtEE$I3(C%yG -xyde*R$K%XGhC=faU9DhUxvn(L=E74F2bV>#YnJSZwaC0v`r#(B2OkfO~i2*ktCr8#L67@4+T$Y>@CY -6v(ZdE*2g~|I}b1zTa(}me;j2TLR(+A-anvRpi6y3;AHhb6O4P{1!s^P+A9rcDu -)T(BD@^&onffLpC;&RZd#Zf(6K;HOA+pju5!=_hcWyn^rx92Et^fP(mqmpdf7J$|5W!BS690f?QT(V* --^Mg<^n9_8L|;vEINy!GKA;sg*{3e49K+LSFvfFCey%S<1fR{c_1B2H3_d($W{8R&WIrr5nN9Bm~TpFkTz>uwPqd6OnUuAM`n{Pe5^Lw@@WeQZ>qK!FFX@y386TP5Vm7UlF1)FQ -?M5qv<0Ht}1Ig{m((skAa2G}&kreng|ttum%#-=X%j4dHI>s|V~RP@fou6YNzQ?=@-rO7rj@gV`tAA7 -J^Khswlmnzz3aMi7ggN5A#EAYpF;p4>x+vQx)3o}NOlLi7-m;lp>mgrqjVX4sxlct61w==-74=PmxrQ -Kz(u^NHycGFftX7j$s~;%NhMcNz*R?{ZKacc!Z#_~yR$n7RNtFV>lb4!E({xtfSE5RQ=&uYA7c>&|A_Cd~4kbw -`@p`YFHGxWo>^XmurrMeok%$i9?}D`ZPoW?`K>!w}g(LmX0;ebq^C=0i6;vG%h%K?8(ApWgEbmGu(4C -!XJ0)IGbqXL`U>gpQjYXYtTH@HQ)yyb2NfJKUjDA^{D4TXTi-S)gOc2e9zFIZ@`CJ9pGisOd-d -Vid18kmtQQ9?>IDjmux|GP(*;qB#3!SI*1;?S#oGeeS&Rt3!q_};?k*{5LjV6CzDy~SPcY -qoZT)swj#M4or*|BE>vtq-Mwnq53l1@WskMeSe&-GOcuxH83iR{ -oUv@)Wm?`6<} -+z07~nh!gY?3!G5HFiXNSfQ>I@p+sMHJSfiIP@7Z#0~InV=!wUI}ti$yxA@4PO}dsLKFd+ZgXnc -35`cRl3X&au10edx+A~2|CV|u1 -SEGH_`QNw)smY0LN;W-WQ0>3sz`X|2~~dbo458jkuV>_4zH3)BRkBM_F{LHSHNYg$O-y!w|ToLUTd_M -O4DUJUee!>#+#wv)Wz>YZ?`C$ZHSB$vx@&7#iz6kmBst``J~ztUDZyW0m}%HYlkHCXhdsfp{kHLN^wA -BJjoFA(>;<9g@G_Rz9=83g=Tr7goTB`6+W8j5oNm_D}|sOLX@6aWAK2mt$eR#O&Lc!u -2!004C~0015U003}la4%nWWo~3|axZXYa5XVEFL!cbaByXEb1ras?Hg;4+cxsMe+6&z!SX>7+5l|;13 -p|{MHkn{BH3I%EEWn^qHT6%QWd3jjH3VjX7~^(N_Mho+FtHzU}Kpa4(B~Xal&`CY+BN88%9emHnVjjc -chS_W$)UG-w>Tc>r7EGyrqb)0L -^MY1YYM*7-)?J&DSC`L!e0|wtIoQ2hwyW2?Vh+LXw#1>Cnaw-{JD|x9a*;>d=5{DoMZ@bhNBU>8JO?` -RoUF*zjKF99loq#4l&=N7qr9Rw6`QAnnR9VRD_*+MZ>7Bbo+|~CbwjU(>T2ARl|R<3p)Hrf&6R9TyQ< -B-Sn{S5f;A4{=Z>=z(-*A%>dVKt=`$c)k6+Jbvwud~gKf0fO5P-krC!>Wz@!yjvM@ulr^4dsfJgadh7MsCbnQcxjei}OZV2 -q84Y7C0qV6dB;r_>j9C^vT;iPZX=xDX>BdhUgXn%`lVo6Tp4{920MKdVj(>n3q|5iZ~0M3%XAk1K<%BY0rDZB-3X1qJoQ2dO!xyNAO+bOV6B{!T0sC!bI4u?K9K}_rkcJ**01%eh8*< -cz(fDsb1~(0Q|x?C4*@qf@T22ZnX8hkcBEcW@`AQZ!6=2<7A3B`T7zhUnA~#$D1k1B@)mbi$ogQz1PEoYuV)MfvaT*cLd -gIEq61F*NbmW8zZBQ4;BR5sKN9oq!~ByEW|{1_m`l)D8fNU+dcf -=g=*iIQDYupGm1N*eZa#}rt+7sL+$4)I7aLNT!78bW)6#;}kXf?j~XG|U(_N~*^-0LBQXYF6+yhZ#up -6aXu5t~;jZOq|+LP!Ppyu=nH-I -%#;&F%iGngKbH<0ou$QFdy0m)F)?4$rgUGsNjEgN!i04o>dQTEu@JN$-Mygg*(l_Yy+v54LX3aT_RdM -9~_i4^klP65va9pUGw-C|22IJHni7*sbD!=&D;;R?21K1}qKw#$)_a1{3p*j#wv;w1}B)EPu$Z8RjY&pdHWmdKl*SkZ>T$%P@>NN@-cv)u#gGk}!M0d)ivU@mIO -o`d62a`EDV0AlBl)?JpM2)eTfazL;o82Poc0uuC>7f_hjk|QPkS4a`uiDU5gkDUDn -(DFgj(4Vm7$LV!>z!SIu3?j`j$z+KC#l6z3e(ky*QAT2C0c48$0p-;69%qlQG%v -4OYE~%j}V93>f&8FMI9L8NMd_ykNg51jv^@jGLEa@!UWMm!)!l&tZ<|x@~N(=>K0`{`G#pl3$QQ+2(E -E!r<7_+oV`t8Uw*#Np)ic&d?by5n<+L1C$w?Gt>WUv*e6DhFcC;@T@5lSlw>q3qC4qaVYdLZu?+uqj| -YbS`avV+_+%rb*QueWhm+oi*io`vD~z^kIEz#~ktGnh8^G%+zU+W;m=e_)0N1%`NpSWcjakbJZZoefJ -_&mk;|(jp8-W&40{JfeiZLmCHh;i!E2qk8y>i=-iyDXAA)*)<+3Cyz+@x&FwL)mUBtZUq;xhbP=|oH? -yeK%@>(i1q`+ou}YOV@7%L8v-0ds~%U~<{V>w1V*Ium_XeiNUIHU -2C_?{i1PumV3K-1`fE@SX!d`dvpqGJ4r@8aYxo^HHHcb>?eEp-Ny%OK>PTTjQckTy&TETuLkAmXRJ4o -=Stvl7+Idm53n^V|I>5;bcBR2PB|heqDG{H*m

IH(f%0Y9v@x=X>vC3*4^t8cch0MmY5^c8D=Yd)hhS-8o -to7SC>r}8P|oO1#IN)+rMY{M&=)Hg9ONBr;q -?`@Q|?Gd5h)t-{I@OsyiH8=hfDQ()&fuG5Zb;=l*$P=)uQ`Y=mhdSvy5e -I?eL^HqDYbKp^BDhcb&XMHAO0LV!PhU~MLEPg0tG4*vY6~agKcKwmE#tDyg&8$OwI9MTWcIQ& -d;gbIA!<0bHx#d&_Ap8=oR@#mLGxjyr%;3{-yrME0E(J^;s7o`ZoWwc&FjZ3@kn#ef@t`Y-lxos!C(5 -dHwvpV0t7@ntqT^9v-zyQ7|eDhei&4!tUk%(egei-bgHgXGb%?!Lvif1cXIxK`FW2(`KUr9XudEivE^ -6#kbjD%Dwr%7QB$)1Lk6fB<4;BMT&PyMY{ki(6?&=zVM33--7u|FHjgWH&OPM)Lna-O)~N(#0D^sky^ -54P(|Jx06P$b(4B52dGhMn`8i3V<~AY)nuI|gAge%btby22@(l-X#hLTMu -5tSjGIu#F=;9ZaYH;bBqDYuI)7)`g#Jq8_gUZTMSD^A -bvGuI)t)K9XZyMS6^I{6@ALmbBpMUo} -fBNLvzh3RJGEeF%>>^%`Budd_f?aO>`M+Os1m3y&Mt#2pA^1!e=qTcL48R3Tc4{+527_12hhJXnR{&}QNx4**MzBv5S3y;<& -;mKyze@`$P$FIa|+e6R(15ir?1QY-O00;p4c~(=R -9NGvZ0RR9q0ssIh0001RX>c!Jc4cm4Z*nhiWpFhyH!o>!UvP47V`X!5FJE72ZfSI1UoLQYjgd`mgfI+ -+_c?{dW0VRn=t-0{L+uOP8E~bytKAy;}r9M}$ -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 -IK(&(7e$We@W0Mon^6((TyR^y7uK5?4)k -_vaU%&TB~QSzKPBBbcc&Xj3UkvoExDI@kU|BdZqPH>~8IDk|6rVPgiXL^pttw80}@g;conf_|Y|IKt$GCstd)Yq)!lLm1%& -Ex{}g2fbiU@Z7OX9LnIz&s{v6vE0*6OfGR6WM~ -{J_%jM*NoR?NW4x_^L}M%st!b*jyv2Uncm!-+yLOyHGx~roGzlR*bRo=eqKRsSBts{3wL+Cy|l*D*;w -uuclRoEqC8pH2*CZZb`+?6?13`s?$wa(Y)3FecbO|`K3=Q|tDv#~s}df@SZ!?Yi$dze?Z#7~NHFqJ%r -$oja)S|zVJ8j7Z1y8*VnCU~*?`xZXT84Oyp!4)5gRy_>5(?OjOOMbN{d$}WO>_GcarS -C;L=LhByuoUhpS*|J<|x4rj9`%c7vE$Z)xe1}~2?_UGCTXq!%b_4&vd$v-&KY24z9qC3d>nvM0ZGO+O -?kUG}%u7HHuc2wC*AQU}%@~AQLKuPOFl_(9s}H$7f_PU@h|DeQk2$WODq5nGYC%<#Zfq(i5~PpWL!P! -biBcg;{@-gIvZKUGCMp$v4OF!M+9_vG@m%bfgC~2u;`Lb*mXRGS1l{Dd&8b_D1_!U+y?Y&<9=)CX0vd -mo?7UrZ=&6F|-PU6mx}ICN?9lHqLzlGw?yksfk8IuQIoN|oUo-@>7+Py>cir3K$#Ps?UI*mw=B*`(X1 -GmnU=1`pr8E&cU%AtVV($A6hU@gH${Y^0`}A3JMrQ}hS6_0{xVBHI5BXKjS1S1&lJCTjeAlpO#L-KOI -2C>{wa+gGjlA;pJh>NQ-vN^A4@Y1Cbn_CQXO5)tBI0uFXRmHx>m91CQG8p-zODy1vBmka8H9$-)TO@A -Z;belA2&iyJ@w*}chQQz&7*W&Ow+y@a&FRroZ-BzX%Cf+Hc3RAl-4t=KKb4{qdBzK?+Wdj){s&xKzeH -PevmWqmK?EF0@pM5RodO&-Zr1~mwqW}*MyoRvY^xxH*o-E%Xw=EM?3G!8Ac=02Uq9KQ7;%lG~xc@_!( -wQhP4b?MiIVp<3$nXgedZqGAcm~{s2%*0|XQR000O8`*~JVz>+DQ#B%@u|E2)|B>(^baA|NaUv_0~WN -&gWaBF8@a%FRGb#h~6b1z?CX>MtBUtcb8dDOk@dK*WQAo`zAQB95y02wSvc6+B+Ub;susk^mfOI%5|r --$YOMW8@d6Rd(!fGF7GM?2qnr1J_Vu9=ZpSs-Qi>^FyBw?$TEMcyJK;~q!ZteIE!<&$i?Ssef2;PCM9 -;Ix=s7nfzWF570io|W0t)0YRwiJyamr@m3uW<|DaE~{Cw%of$M1jzNKsOoZ_-Bz2cs)qL~20hN2wQqg -UZZGE5x}0sA^)B07mFu!u -?91R(4Us`oc)s76wq&&=$LbdE^HNy;FqMoR@8NiK_=++&1fL9(>)bZ)AbnUArl7^!s*vke8R2{AQdL^ -*r0w_zocW7ciBVn`~CVW-MEQOFOfyZYo-#Vn-bvPL97pK|hk3NItD(2-HrVneF -wcFLIS#M}^)9Gconaqn#F&s^&2RG&Bs+s46TnCFaK$uJx+s$@eP9|A(L!T8FZL{2N$_f8{a3IfNlKN} -oeybY&ZByNp`up1bw%M&p*BIu3(=dNZ7xLh?Sl6&au6?^H+`GlL2CQtB?&a$Ga=WRPZb*a!&Dy;N1X# -ch>EE+vxdc>2B=JqEcC)N5^zZVfsFr|H>$Y@Y%JsVTQ!HhRv+jAZG_B6cLBGJe}%yuY}UIc2O0dxTYPm;ewdZ3P4y+N%s0Jx0kt8t9o@OUUhmf1a?e87aB$bK?Rw_M*=pTf6c@`K0EH8o{pIP4B-}s4p)U{OQ3Dd!xG7 -CL!C&LNU3%Bld4`c@Ce_1G2(S!7ZseMH`^M|2<^x4&{2#pkRia!6R+D$Mmj$nQ4e@tPyhCrV*k6V1^`` -b;he(F#oJ7`Z~lapf(v};2RLtj-0&a}=#9B=7vM2#Z^|dky?W -EnT%i>0#&z5CTZ&wrdKK@_=DB5`z!hlgs;BHwLbYV^C_{U!m9Z11J9!DPIg){*yTkj^T24>zJ3(hJ92 -d_CizMU5(k$rs(b7`4y;5cFBi3`z}#B~7{mlGfs^JV&#GEHum4+yuBwfX(;AAQd(!BDVO{uD$;E(ZVo -7Wui`E1;dkvJwMa!++at$=p(r$#FpBc88VtNF*!E9j+PiT7;ayR6*Cd>*L@6uqq?QU?PBq?_~^*0Mp4 -T;hL(m4iU+}b6-(gp@`jfFYEWkvYJctdjS5ryewur10DzjvQ041f|mQ|Uu5_bIXnD%2(OWH0Q3Fe`c~ -#52WmLVx2rief;aTSfYev>YPQLd&R&N@3%8aC(}U?Ih@y# -glRvm-3{94e+PYD-epK|YPJZuLsAYE))O9OeoAJU3&WG?V4h10ek -A}vLYY7{&RQk;A;6XlXR=c5FQ0j>q!=i)a28@!OL35EK@mb68;ZNBsZWMIcT>QQ4>onZBpmX`GulvN} -5<+5+H5_hOVumAsEQ_0qd67LR)|c%QJ^t7mejqGWIiKj=KJ@4Q4G$ND+16eCgBJM457o`~#&NwM`|3$ -KpihxrA)5_&4FAos9}itbI=HviC3fA?m2p$m1XBR6l#SFOK?BC@2X|~Hb-amR{A|LetgwzLc*x} -x2>0SipaK4?v%Zpt!nk@U%c0(d?b_+zSs5eZ|W>@75FQ$->M+yhUp)H7Bl#!r2|;hyR1R7?zTB5>2Nx$O-qnm6TSvP_$I(4J7UxJO1FtNWez(5{BN^Hy~}dv3BJNY -k^mTj9FE08dXJYi@U2ZN#P=TKL}}`y?9<@)0xo -7tEpSl8k^(jnGkPYR;h&1Ont45n3K=kU%P>47)+8g=1YgWR6coW^AUHP|!>`+V($3b^YJ-2#kzF_MD_ -}MBZwquVW&o%XvAmDAGamkk|FEe?$|1XV|ZHpvxlG6!6EY&EF_I3!HMGI4wI`;XgA75v0ZK^Gmz>^Km5~m1#-*|Ww9m6 -J~WPuxV@6mYG9^_-F(k8c*_%bBb|Ly)Ih4-z@G=3kIkHPg@3-leLQ6ha4YFV6$XKjmjr;$1AZo_`dL# -ifD95O+Yv}9e{MiD)n4jbQltu6`;7PZf&Yxa@ERUzGE*o|T7qh=5J0H;R6L`OD<_%eU_k*q>g$d -iv&X?$5WczkT!Uh5Pf_Utc`?*Vo^^a}S=s`0mBmuTSM~?fLLjDAi#`g69H`CfrKl5fB~my1c}3(xZpV -rn%m(PysDNd-)AW{iw%1HMH-g9trypm2=uy_u~}S!#{(GUS0-&-mEq|3!}sbhRiaQSKH;%wgM>j&*yy -Z?W2FMR;LjVo+c1jmKK@d$)k?I7`$aA_XLFuPqaP&73vC;O50U~+I#-4FLepq=OtnWEq~`g>br9O@B+ -HHE!OjhBSUqEOc1WCo1&sq_w;>HEekk9&qKm`aPH*Xi1rK}ZSu_S(~FjXba)s>g*vEr93?f@&hMsB=ZE=C=J5}yLsF4`ogvIAiTKckOwWX?x!xDj0(M#GOJ^QUOtfO~PiJFf7gE -tkM@DdRR17%g*j$P*$NE8C54BrM`v$G8DAzNzSt&;4f}c5u}3R0U88mbo8{h>yQaP{F@s3H9)2nmk^J -zaanSy8z%BzXZ6y1X?l%^Dl62R^?jM?YIjFW#10q5}!Rf@B19!-}&|9^8?c!v3~EC>r8W`>&0gH-~n0 -OfN>moIJmwOf~Z)KNoGkfPWfPpr{%cUW@`u>+^!>;d+&J^5Z0o^>y&no-Hi-w$7G;6<%R^OtTvqSPntUZ#z3e! -x2{4Gj%~qwVZcqO<`R3{WeEkMR|DS)!fBt3QrWsp#MhD8iLgUX54F3v$e|BIPSor(#LBPSnlSc=Zg@p -&lk3|`mV!(#xcQSg~;qR=Tj=#;OahoaoYFwA@YgYJ74df|rVDN}^U$BPw2deXVuV7&d=M6EP-Zb;=vI -KrczMM>_%rWBWObmrwW}KP`Y35k{M2hSILwk_1_H%4BAY^EmPCI<~bV_p};wxhb=zL!;(R_kz^Qxu00wg8d)7WxQ-Lqu04mq;;!5~dvL!>SC-3X}~ -0t$3rGR70qneiX(nfdHol$E5?`qy$7DYT4j)%Nv~;CX9U8;F*2fd%BtV;B%K)b9nVev)w}AZD%L2n-X -OJ{Hg;f#|gDR+q<$l#{b_S!|)e{Mb)bS*jDK21OqL1Tl0=i$5S$!}X71457m}#q1Py-Il;RP(XDCu^t -l6Gg706Z%HnH_;q!$F4nt;&zg1lwp_ojK$Mbq!Z_sg%^&~+orqsU&Wf)gH5!GTNz1FErBzpl9TEzQ`6 -+=-eB#E*%xk@&HQ{e__oeih{2DqPoRYGBBa;}Qo;d)T?l;~E^TAmRQZ8V^Q`*cNZcC8shARA!as@TKq -Y$mAF);xFwiN5x)lhsXLbJ}#gVW}=T%SH0Jjn*D)eLW5dNP6SM^Cu+O5gl~MiGUAo;VE7ke^r`ewR^3 -^j3&$`t(?gu(N=>YI!~W?ce?_d&V|{<&L2V&2P~n4IJ2Xy4Q|_K7}ZyRcy(q%W~eb%fJ<2Jzq0lSM~M --T?eY^4ky2V6;NoZ;Ja`NbI$tCaHE -i|~W(r(j*i+3qum$TfM -{QBHEQf1d+u;xMPe(uG^w04ye=vfr;1Tcrnwtb)M?j9~07Yu51B;+Zvz8q2l-hSF7uVd&|NhWFD^Qa& -FCA~8?A3hg?QyU<88KGyT0!{Ne>@Py46{_5^|r*#EvwM(SK-0OI~LEkbmqoLHco(CEOSpqUeU=96?J4 -$jcEW|7`JuxVF$FJY(rWAW6?R`Ajpj88JXajYj<+u-g~?~%F3dJArx&HvrXh^DB4D6za=V(ww^*QBmf -iN?lC>O9nzumCw>q(#5-WVHT&I37Oix -8ZNd*f?|%6CD5*E;3}3h(M{j^O@c7xk2UnM9q0%0pMZpXdwE|qozM|(biri;J36h1>HYTUuY#0(Ub;G -!9GIZBF=yt?v2WR#lBDXg|7~_neke$NcL0>m$n+Y{_Q}@x7&1+lo6Un9q!c@R1p|4w!1knR(fjU;$+< -Y++zc6mYUXgWsHc)fP;9R~A{I9|m_zC=K6|{a@D4*qKzGjGHfP>8UQGBDTOTU_%kWp|D47Y9>duz;-XU?&_a@{uIX2cFDG88&_<19pI*_kOLsdbZS7TJ_OVGR>u`INX38oP0 -_*WHnkq;VndjLMG3ppkQ%vBN$gjYn7hD}y1!M?6*@x8vOwTe!TrAvC$)aKoVx!0sqf8+wn`#W8sA5R3 -0J(ocE)X~4>Pe(W%5EYX^>%Cck8f8w;&S4B%+{uH-m#xCQ3-jsYJ25QkLlLYgtU!5eGvGe)w=t)1w0Gbk}=FsRu^b^@|VXjADx1Z22q -g99?{uRXO+HXXxGxI9#lEY#NcX5^J)Dozr8?f9~!g{kvQXt$HDXh#v=fwjCVq`0x#H*z0WUKALow+Fh -t5!UmC{fLBCWg0H8ZrBm|J>1HV8=&|Ry2u1-| -$z&B2llB5$GR0VkE>0akyuMF-1<0VEsBI!J)6`zAgfy;fdw1AsVqC>b`E3dV~?)A4fgp5PNSZV#2Yq}@GsZA#6AS8y~F>x1F*;+nDdVU6&50pu)RpdfJw}W0i)SG;s -4MiV#qT?7@*AucHYtxhL?7aJCH^DF&ANb;4UeKGwi<#JH1fmt^v@20Ng%X<|hmO^Mi$|LIEezM=hT+% -j9WWP;a7h1EM+MqM4j}i-%{s)J0&Tp**|H56+5LXH#vp#a2)MCb`LsvREly&#w2JMHDqKR?QU37(>=X -5dZ6}jho7@RP)t@6uazKIyM&7sI0{G5WBFcZvv23SFrhnJR(VT#WdX^$~-P;EP?G@N`+6|Q8sg6 -k@2A3WZ=55O%VAwdaaVn%)vj#+1HQccnp4_#w8>{>3jtB-!666f%pI%}g4p6bYE#Put+c9?=POe06BU -Gudi=`Zg74+_WVDV@8PL=fKtUwL^wEpXcz$2sQVtt4Ttn+#OosLoa`Wo%a(?txzDiS6zk_ ->wEN}|`hv@B*n}HRs70(sl|h52m;SmbO9|an{eFC@VEDHS!lYA;qvRmWF|PD|$0#EsOt1WnPMia0HNw -F-yXVx7WDNKm-QY|+NRv0ulWSHtAG>C%xbhriD@$klu$Uq$)fiI%HX({-Mfqez(T(&_ZVUJ*=CjaLfV -UJ;4OEYXYzwI|W3OpGLu>1~6)0F0Wjm33g1<%qmyiWm9~Ed>Nd}f%Q}wM5zo(IG2^%DuT4Bk@8AVAUt -zBBUBsV$rEipXCG#TDILS_w%G-kWXEiMpc*MOc{H!26_qCp*&bVB})JT|;VXjp(9Lt!esvVV;#lr%ob -N{0?0G^>yoH#sKm$1#+cdJ2y?<9b7xq=pHW&{Wuq6`lo?58oiZhfDulo -F)}(!wB>cILHHq5wT9g<{U{&HreU>HsvA)#m4ba|gVbVnd2^tSk$5q;$lMqdDZBRQp-?E>#qlqaD_D2 -SVEkZ`dJiXK8j~W%YGl}iNoC||c9=|P(V*qhR%0JyHRkmeWhvGKhnZOhZ-lo}GVxORq(6xxq=(55`G? -ZdZaRNm~9($aUq|+2RfU4da1FH@TRRi75&Tt^-Nl!`qz>Tm>I2`B_oI%TT@&iw`;GHoS05uOdZo{kyb -92I!4_KLUy*WXi1rHo~o1HM*(_zVlp^>zDj?96y*~}`FCXjjFkl;E#INR3lP9Nj_?*u==31Aj6A^`D~ -`A_cFm~H|*E78imuqbCRY%^#>o_EO1_iaI*3=so9%8hj4I6NZoeW=aYIFJk -pjtgMfKnBD}K -_$7#4Bp^)f0Ik&`cDaEjTHMAa7YZ3dj1~%|5OJ5lB&D8_qqr{ -zAfmM085bJ-?6S_*o@3;LCM;u2knAlEvv9jK9S3vh5RiVxVXVG& -L`4pa3#4pfCfkk;Xk(qeQKd2yjWN>_K~gIODg4jO=R5tz0rbKTz2hK$GUeX^^!0C@+xRG9-7Bv&2B#* -c+42p@eX$^@gr!dO_JjOV-Ef-PDE(d4Kt+0*nm_n1o4HVI0R7tnf{-%P>tt#+I*}9cqBTG&*|x;v_qS -7xeofY%a64oGi)5of8J6|mA5b$uq768hQUD7C?Egd>MlPzA7yz4Y?tGNz -wWwYM;Oi$5$Y*e_&d~8?DHEiJSPXj!mjTqWzqq8SZjvt>#pc-vX-Q-(hqu=*0!M)eY{vgDYz#wL -s`*f%teFq%be#l3dB-ESsO5X+NRA+_*4>gj__f0jw0~}tsi|v{mofC;n%<|p=$l}IFE{>HGQa`kV-nP -=)-XQ&%AQ-a!&|!|xCLG|<+ZCO&+tzT?T}Ils?J_=zXUXq-LpAS*U@pCYCuc}m^;p^P-Q?$_m`*05ps -!8#g7{N&30Zk2O;q|6=lFA!y>3=5C;d3YtvSrvCR-F86f8`Jpe|{VSLlrOW4IW&{pXzz?$XKUL86!d{ -3s-^aQkfAD;IhbFb$0sWC$4x{DHQUh`}I%P_>&x7KPGBe1_S1zPvc|<1hx<8K}!#kQ}T?a~H%ib@X-- -aNN(hzwYMRZHQW*lOYi&DXJb;T&FfwS4aSG|#K~& -8y#zWDQWF-^`PBr-&X|0|C%tmIi(o&A46n{I3G#;wfOz4KX=Um=GvfA(4+RE%a$3|UPr9sugE5w5Tj- -<^B|Jz^k-!_;E&b1{bvFuq6rmz=3j*}hWp&XeJty!7Eh%<5@ZmTdcMKX{0`c9+Y$CM4aDc%x5?06!sw -w6Jo%kr!<=geVpK3wGaj|}TL5I@Dce(0uUzL6zdR#{L_po`rhy9=%kjWUh)NpOss18|RSYcQtXd@?thPWUqKKL(iEnl* -0e4dtoZbX|Le@LuziF6{z%@fvD=jKc(1kuy*|b&`4>bbluM5l-hAg=kPHBu%E(zrqJD0eMJ6Fw}R~qZ -?$*0q7u`Ez_nv=n)#Z1Y>hk+2xJL%OT8_W(wOr3zun&sT5FqXtIf<|okrzFSiCdl_p`|mVI0&1SyGXN -8oeQ>E^vd@k;Tc9UTvf(sXvnd)D@EB2e$aI9eWldU9>#lAiw!xvEC#Gr&Op-vaOFF=asbfIl#=E;|g5 -{o5$5)+^sNGU>c_%Blk8XTDE1M1&Md-!IIuw=Yxc51 -Wvvi&ha=y!U&j{YegWuR7s~(zl@&w9#BQ>T7jfmaFWt*(da)qU}1d=)GO`VyoNiL|i}qgtv6EY=;HWx -3yb}ni`|$8Quh>v%;cM=wpn!JbRPc)kwmNW*^ffnrjcg8crvse8w9r=Fth~tYJ{nkCDvrAdmsW;5BvM -FE-IYy(3s?o+Ce95qswMG2(GJ6WQ>GtEO$M3t%W~a^M>|6Q8-rsUG84To`v6{}fr>>bc(&r_U!YJzg# -L-epeB*b}=jX$`yq(^z)gbjjIyyIc^z8AUvNNrZV_F3a~t9WQ3FwsxY~a!dXN>atA!&&0f_AA|3)lH+ -%$&yS4!L|vnp8m7JWJu*ZWfs%Cww#V1Ei*5p`-ZaA>=%Vn=`hGXT`55YNvq#{xLl^!%zg_u6*cs}T$_QH&P55vX^JN6MU- -UCV3YlM2=N%r<(i|M4Zk9Kt-%3Pg2IUjfYMz|)>I4}*!vo0VvS^(wVhIvuL;QtrWRI_i*E~rD1O*{>!XLBZ5_Jw69vs`0LpLuwAoui*NvEP>Eke5@+cNh-H^ri;j3=tpVIY -IQvG<#NFKJ>x(vA80tBj) -S;Z2xSZzmlPVB5&Mh`M#gd2@wUWdbtIv-b+Kx%nuOiAq2Od{Nq!dTkko|AlfR*I6{6YHq&X*jC9uLnz -M`s%Cz>WW#ICnG;6A^cc-$U=o_6A(oHNPP40`Hgle}l7TSSj6;WtLz7CAB*#S{7cvKB3^Hw#;{HpwcZ -bjhX?$KhNkgzbvBC=&35`*At(6c{HG*q^-8F?Oc-7;0ETAF8{YRW%il%i)SMp-q-~cui?EADDpS2+Ni -{5R5=1V+m_9;LqQhE&0EgB4H?^n>8N-b$N(>^D_7mho!Mc6q2M}vvu -Hr%v^Cg=vDRri1&k-k;g;)cU;~Ll^l&$I8Xs;9!Qrq5C$QWck?i3B)ml^x(i3q4ahdh6Fca^4Y-pNIg -{Z_RP+qZAuv}kyqSa!)t$iqz<)q|gt;aF=Wb{u-<>&oe+>2WVW1YSBndGAw$p{M3YN+cQ)B${AyiUyG -whm@WT|~J1I{7bldtfL)$NYq*b=W! -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 -~Z4hL)%7eMkpa`EYo+`ZcH{ -L8JG?n)~9+WWWkyEmkH@u;e{V{&dcxe|~S|I_7nl3?%e--Wc -HJ1bU9`ELBGz#ha4;EhS)Qot~i6C;I#!-8X;kj|iDJeqJ+?GR0L(K-1J-(y5GzGIW{>5J3si${M5NG1 -dLQaiQTo(N3k`f`zEK67^7f2cIpI2fTl8*g{OtAB90ZH)I>nLBGl(k>qdz425qMjA0yD?ec`7!!9eT&E)^7fW --$xSSs;tw+?g$$0 -RBGQ-KFgCLQuRxS50S*ri0UHcPqtM9N=X!K!0q`K`X(9Dq%*f&|d%6uAe(u|8)#QKLZOZoL>!BthxNi -mXJ>&3hHy<~7CNjAVi{QQfk3E1%V1Rf03pM{I($k}!o-ml{V#WoDtGbdd=?<6G|kX9n`S^)J+k%U$ajrvls9<$$JL>tw>0E6j{DiY -7iGIpiek9wnKTmvw$v|4<_j1OM%X;{USW^?hXoioPwEQMfPN7M^*&>fHCN5bdTZgQlMNVaqs9h*hR1< -V?|Ni{x(})nT2nZ41qc4r(IdG!C}3g}Y@lq^;~sum{V`ZpRDd0e{SD8DM>j<~c59)jBpnKAo?c%}W(n%6a01iWOi -b)^w5*Ip|%jS=-uFc@&WXZ%%(S?RdgKivJ2DiXFiJD~xHOVYX<~>%YgCWV{!JiWen_<%@(L{ia%k;F6 -b7;5f%)co2N02jStMdmCZz+W2aBe8cu}nL&4*0{71cpQ9fy -s&-xc<__F+VX*MO|ID#sFN=@sLv(%mr+C=00MnMMf^vtj78b<+qPo1=9BE;yIhTIRRsL;ZvlxfMEempsyA^9a}Bz|EhJqJjWL<#*F9t3%)^hvhX@;jdX`TwFVRJgYVo@B=nilIx -(!C$nVI*?O|8rwUKud2pzb7P9=3toVld3}|PV#EWY -LZEl=v(+CIuVO*bR^_`;0jcqs@1f1W2a)+vTN8WJA@uK5^2{2h{FE91sFlgaS68gk@z!lm1YQ5POOXI -7VPD(l`h;Rb`1u!0xJ)$NxAZGc{8m_weDxmk;bWM3glB5@)Pn$@$KZ`}K4IggEi963lI{*FSwI*CO%pGS*G;ogcdfV)7jLjncojO0xj%9_ -^&^o>g_Y!j+RxeX#-X=C^$vEoCQ?yCXN~Et{d%t)b3PPeeiB$P}rP&Yww~W8vFMB!&+ALv7_-$q -^R`J|d@d>E1z8Kt#IFkbbK^Gju4^g$x!%P!qdZh{`79a^7V);>N<#4vNr8$IhvWMPDHh4=z|1<3T*Yp -N;aQqA%E$wp$LWAEBZ7jHOyz*v7NzK*R$?B(NbuqaqFtt^QzOk&yAzDs+U1SDG(R>eloXt21d2||bzX -0Yk!1A^Cy$+-tHD{v()`Y?lv}y##oC8hurs&Yv>+Q^p{a7UXifUyqi<^sik$HT0V)@|x_#!AU7<}a6u -$u3_L?swR1nWcveqEG3Mgj**=9lu{tJOC}T`h1CQY8U?s#bWws6lqIt(IDe%dJ=vZK>su_H~b@<4@Qs -tc})IH`TIOt9k>v7geWoadcmksuSzmuyxH`WK?esqpUXQiVstKiT43B7}oijsv)NWW&)8lqc8^K{udG -^MDHwkGPt0?7MCWMf77=TH*_MNec8i9XBBj<^ -!%0=ByrfuC_;P@L~N&5 -C3E}vD;Ity)g}m?0c+Tw2^nect%Tf0YZ>1OG@Llpf;UM#9o=8>FhjhT+=2arY@zEW0-vz+AlUMGH~k} -Bh0{GTE=Y?u!qg-0*~WNsJqG9fC3OlYG<)v!eh -n4iS$dp=JXdr)-2v?%jC)=IWxGHJKx&~W4qvKp_{)sTPnNh8vC>`EOb9@TUg$-wO9geNoS)=iFey4E# ->*+KE}cJTfLIsYD&i-drS@ccRAWQ*-%Q%g0A}og+kLC1WHjPxdF6l%k8{5UbLYwST5)BlXH>2Vl4T`?uF|!-i&_%Zfpfz?A+GKA?zuL#ga-uS#o!+M2P&4@-mE_u@W}3`)P}4 -)w;tNaTJfM=tJimd*b3Ih#YZDl}9gAdvV#!uA?ZIG5WFt{T3Amh2)bs;~*%$IrjrMzD*k6EY>)1dp>{eC#qyi1X}m#>+$4GtSLq~oaAM1X&9>t$76AG%y0o~R9yYs)_aS1wniPNRn;=hA3pilx# -l*_%Adm6fIuO1zaML_{c}v7vOia9)WMiIy3- -&3$Z--QitDh~`HlK-bIM~eEAnvX6G)7@xVSr3`-_R62e2uSXI6Ir1UL7v&5oo94;jTfCC`Sy$!k8fyq$H1Er_wt}Hy@Gvc=6 -^OeF7U5?amP-vZFPEg;cahNmV7*IzMR~=e97X-JXVPJodC!ph_V`y%()eT3$oHWK(sGi|3X0f0UaYIK -o&yILrg+95MeBVjzS0Cjj0+BvL>#T0Vm^USR@d>y7v|I|pYCImKhhV7>j)k}369@<%_tXx-??3ODN2#0bKeiy~%#0-2Sf;lI4X8n^T$edVkb{-Db@M^u@VwP5s3qYWM-x(0wyUh$SZSbi3yF?t}jG}j}hG2^i@oMA}5}ZBdOZJXA1;BNICAY_ -sc<%#vh|=FlG-K&=L9rIR>9g~tprX9LFURg#XFVhXv^!q_{sNt)m|Z@Yx^5hSS_M+tvTie6c)Uh74K? -zFpdx+5msf;l04EY=I%a0PJvkH;?_r9GGFaN-pZH)>vRmNV1q!Nmhx%1F`GCAIo16Mxlwba54DdL%I0j!v+3-KIjd@rD%}oYZG%9E|{dELP{E!z^`2;d|e{`}6T1LJc-M>-HpG4_bEegZ5zMKP+liDCH^4%~7q${U$*x9pNJV`{kw0GS)*PkF -w|J_1df`RRkJS%}e92XGkXdw!K2n1++e41HZ25R5xLFoLCp6&_*fx9L!XKjI-`Hn4c;3JtWQ%xa(oZ8 -PR<|I(u{;z<|$fi7D!ny0isuPh4;QsGRtdXIAms8h!rCdA#h=Y6|g9uIBN35aU-n{BOCLQS$dYS4v?< -YGL=-_*9PeO+UvVMsE!AU=y(QL}z5m9Ag^3>I$P#l5x7CoZ*g|LM>Q~b?QJ&b)at5$d7iSfBMe&Z1C97LP7%rda%M)$!QvbLf@(%M2N*qEzNv -JcIJ2;1Cb)puJ?<01?XR@xQ(Ba%U`u(^7o~d!Pxmg1c_U~wxQ#lr8jcK^Rk|^>&GC!y;=?euJQJ&1iF -#1xrt8^W*+6e_1{p+amr23IY=Gnt<+YeBx->r%t=VX`XM;ph_#|W%|&T%i*PS0pD`oU;d!CKOpR-*?z -t_^hV)#QXn}yOvdhJ@X4tX^Tu(STEs}n>7j)DOU)-(5H5i2AP!Q4~5aL7&;x&^9%8*&9hOuTmi^$~~sdr}_F`1WL0a^O|ZKFe5`{RGW7(JZ3Mylu(VP9;(v6jcvVvQlE1r|(i;5 -gJ&b=j-w0>7D{*fNQZoRX&_t%fc;Y74=7P=9XL2TNkgt -t%&AG=uDfBxZnfQoVx7;nY?px*(K?@ZTey#Q}O6F<{a>l3NV~-0%IGXXIFv)wk1HAr2;92fg6ux(i6w -Ae9_Ll^n#0U(6l~q>~vPbjIC*;?WD;@$X?dg*`Y -AI@#g7sB~0`z4jnb0zkZv_3x1TnysVowTN6#4-i5F9LDt8ygiDxWs#YlXW%pqD>#Fn)1r>rPT~r+5Sf -YzsP1JQrzvVApy?*!nE1;12i&9i(bJiu?7Q5_8lr3iMq -M+e|JG{l5w;D{F?*R!UI51lax3gCZEjjG3hTV&O9E~gBCTf(}3>0z!O6WRP*OvfonZLYQy$tm%A -}hg1CS_1EUBdv6)1L|6z^PYKW{}{!+<(>SvCsJ8a&=C`9>J!UyPHdflcc4l12i_;bQ+{gHl0Rfu#tgz -5SnY{|BRU?F{sc;v#h8PW7S$CJ5iwCMG=`<2^$&NUKLVTF$_M&9~Z<^nD20DFt(189b94RhHzP4x#iM -*CPh6loKm$*{Ii`Ygq3!Ps2ZY0kc!JSMf#!qS78RYT0jbkkSbDc6~}6t+S%IS_GJ0EO&J1D8~8q&-^6 -n-8Y-qn0IAni{;EOB(hMDlK-nDHl%L&})y2I0u&?U}H*QC{HquNgXb3%qJXZ -bf;6Mqt3aCtEL8QNx0L<<= -rR2GE7JMT%+N!9qJP?0x^zm?L*mKM`O4j#v4Nazk&H8TQN0pKKG{1T{9EwugcMNxN1Jn10s~cmO5+aD -6p%9m0I>tHq|Lvfm+#PfveSWwl)GaPxU-{ueZn5X5i%*KCQPkfG^Pj&!?Pw4c8bHHIIlP=@x)M{9FDV -22kMvEF@|=JM{El9#+M#a$aEt(AU3xG+kBW}FkuL{K!8W<7ra&%?v5==0wnOCBu=^YUze0D0~`5#X?q -5(idHf92s!9LY1Wmx-UujlKeBg$*TK-|9aN#xgj`6X*l;mQ(QOgcN0W$%b+cv-lyBA4CUbGM_9|F!KG -8nmmAZhOC3LW&YPN0^CN~nvRlTqYWiX#6S^lfqO>JKiX;ZL~5=IwGH6_rpx*UmEMNMJb3pyT+nQ5Qf`P&w%W>Q~zApUvbi5eqdml4wxV9u*Z%Oz^gwmX23E*ZB+ua7P*{wEiK~@h{ZmJzTdOEWTE4SDmVp0x1SD_LHDUqnD7*nFMc8Z;5t9G!bhReh;cFQB65*Cp-r9Jt4^f -kzBHGW!>0wq0SgrB@3k78FkQMyl4z`l0lN*Yp^;n30d5N?omYfipMknav%bac2{Clfge?;YrwPE)HMH -(hb^p0oLUL^Ng|YI7MfmR5gx(vB0v!>bT1=`uJ`}pU1sG`fojh=zK~RV?Cd0aQ(1dB^Jk;Yj`?Q%|^D3cE+Bcj@E{$83#0ep3 -Qr@&25w+ns04%?eX^!`Z6aV_h~~0=B35w6PLWM;ghF+wt9AO_tn>PXSO`AH(Qw%z4tn9b_O4wM4>Rz;n_qW-*wUXg_O=cp(Yp!h3SgPPC7LyR(^KoFPVb6sIYyw!FM{2@F0 -n-SWCi&!n$DVH(l0n1)-O2)}-cX?Zp5_M*VG;$`!o(50IjJ7T=Z;B@{v03vzlA)e8HJ4olf$Q}3+n4{ -AefH?lAJEL6JtK%?)Y8mu=h^V{k*d2|5x;IwZSVb%+C2NV)%x8!h2nw&RfnVO79=RV(ny9P%h|HJskk -a`o;^jemirYeJ)vBl5P -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 -!4j#72~*mYnPPNf`R* --*7SQCn;t~Tlt#XPm+NMcinC{2_#@ap;_n -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 -SnWrs+4Ntd>M#(swJHuo{ChfD#2suaAsdDldq& -V?Y8d2QKidS22#2#gsFk1%4!w1WhKm&w=T2=Meri)RDEfGmDDoC7f>(xs9(A!7%N%i*vFOP^T|cXjb7 -D!FXU+$O?3doOl9<*&F`;h_o&#*XpG(bczFDbQM&%jn#^1Sr?}8(Q$Oy>JJ}j9sk#Xww*ATm#n#F)|laXi8*gC*^Kx`gEcLZzINa0P|in{?4u5a5UwO -p+uK7uP8yR>;}Fj`REB!Fw@7VuPs2S%V;ec`NDovIx#?W=Z(CAAjK*ot)ZFc6NGv>~%Ub!8LuQc!;rInc)fN$Y5liZt!Et?Yrjr;!cU8*7ODw3>qA81g4dS -xxMwEJTfTfgyHy}Gmcf@T|r!h3nA_qUo`c?$<|aGhkpzECYv-xl+Q6}GiO&O8tFHL4b1g#OWQxPA4X9 -S;-wt`yD}q?)&-KkHQ#3(2I|~|CDG~9(rwh2S(gn%vBXqW!F1ra{yrUl-cq=el-zf3O(*t^O2UwE*SB -*ig$(=|;ih|SxptALshdm9vA>DwF#c~JV0$}ZeYQT^4oC0{lD<8z82<11$}sj#w)Z;b|D*k!KQJk{$! -oVIJaaZ=HaOJ#M(yaE`J-VEDZ2jIFcV(>`e8o%c>Uq``0{%8+q?PukGC&xr(Oi;+#md+xqp0)BRyTDN -*t4-h-0+!sR7=B>W{t8-ak-F0|XQR000O8`*~JVYCo`j`vd?0Iuif@9{>OVaA|NaUv_0~WN&gWaBN|8 -W^ZzBWNC79FJW+LE^v9RSMP7zHW2;pzv7U7u?JUfv7zgPG+5eVz<^>4&X;lJSClG$S{P<~(b7M72Y{_k|Qje*a%?q(^aaTC?3%0 -3*+_b;{U0I7p2PcD4Dbpn%{C>AK`K~lCX;*u_&IR)7h$<(#e -+SG!m}rnNm4Ll;HqFYQ@Z>tw831a_Dt{X!qZi)`M_SXD_@^0-st7Q@}a2lz`#z5`=5s;gr!6rtuG17{)8+do)g_}$t5?x5z6f@sA`)Gvm$ -QU$KM^ZMnoF07z?eGH&beHSJyU+X%o22F>$(&v-r%yM8L`F3B>CYp4}xMG@>y;@&6hgF!Zq64s2trqC -#`_N7r2Mn`)SSO8xh1ouAAdv%h^L=P1mIH>_05;T#N+t@~qOR=-Ud3>ob8r)t(eH&w7+rC+x7Q2ccl+ -_dP{Z>qju8!EEB;Fb7LHigmfCEs(#Exd&4t+s4%gU-2`h?b4#EPi(ot$FX1lrE8AGuUsR -kDd|Ed~F)}WnW+pSiv;8rUFAr-bRIG%q*vv8l7br>ClUP`zx -rHtBs2VN8sev}u;HvxG7XeZYXPdrZ97;QparYYKrj&4~YY9T8oPr{QqKV+ojE+y|KEPJhMd}RC=}dc9 -tMih5Q~Dx0$LhrI)v5qxauu|%ylcZX!S0Xx+4G -L)1fgR*v89?v_N7G~X!$dN7~E7*pT2%PB_L;VCC=8?j8R@C&!>>riB$LMQG?Z0F=2|Y7)PfhrV9>Ou8 -3C-a31%3Aq7N@5{G9<{9fVXesyIHqRZx#a|?Kk$DBhO$u#Laea?IG=BV`uS^g93K;;5P8hDsn#ZO$q7 -wzPZB`{RuO#%kNnJ71y@$vj=Zlt#Z^9OS=Ch-oFdFFAv^c>;uQ}pl_|y-XESA+@+o3sK~JQU`EJzlkkMmzR)&=_}Z=;?G0c+jJ4cI@s^HbY+;% -P084i30T&bX=lmkb1cp -1q(6Z^b*LD(Spgcm6U=wi8^*s;Um{n3J140Kuu0#cdN(QVQTyko1p{hAk`tjo|2!fPTtzkjaFRRhs+w -mj-`w7F)Kl`_kF@>oSilGKO0H#E!+yo#bDr#6=fZk|>6@e$gx%j0$rf673>lDG$N*<-;2$@Km3TtD_M -D6=Ll*ULsiJ}bG$la7yfrXM(R1s${imFXSRa&w&Mmy!v6XUp`H7(GMMKfW$Nz+j7->f{Rr4ogrRb^Rz -vV<`5+P}?TMg2QMN>lM^5))5wL4R81jYbf;7nWFlHLVaHWap8!NIT!jo~5KiP7|PY=4VURsI(IpdLHrEXph8Y;)#kr~O0ZBn1N&Z -C7T>vgUmU`P^)kwFxqPtFAbk{LWl78{HPumO}8xmV6)|q?6t|Yd0#9h@$Xdq(uvD0%Lc7KEjgo)c@m%89$$w3~H{d|>h -YYwn=r@y|3z()Q4i)IIm&H4CCAUhpZFVA4uUOk-eyC!PJ*il`6Ps#Cb`Hy<>UK~kX>u`i-C5*ip*)M? -R*|??=K|^fef7|@wZ{)rJ@4TM1s@6aWAK2mt$eR#V_P9{#%q008+K001BW003}la4%nWWo~3|axZXfVRUA1a&2 -U3a&s?rZfSTfaCyyH+iu%95PkPo5K4f`fFtd`dfQ;rBy9t1;vz|b1-ypDSd`6;ED9u@3ySvJcZSrBzN -B*21)7J%7H5WYIdjBe_@WR}6QO$Cep1h>mrAi9Q<0~9R#2&!B<13%dG^^nvr+}s^NinB0-xclUC_@3& -u7*1QK7lWY1Xrg0WEl~l2M%sxj5reoxDHo^>~U-1V#BgP?}1u9=V?TUdHp~lh+>-azF}6XA3$cxgd9v -=F>SmgU_NM(>a^o4~WRKXQBluGDa06dd|=*W|tuV0zbwbp($9Tr9K1ZhpteQZf8O{<5C;dcA2zl_Fnq{^YO3-nW)L&oh5VxseKu>VP~ll8P -)7j~Iy&i2pwONZvzi376!iU{msTu63b?VW{96Px+K$-c^uF#uBFPwI~81XW^YyIzln$%Mp0Qln&;4ks -ZfTKyD#xxIidwt<1jSfl0{|tS{)XsTPtmMX*GKE@R4?VNY}cwNM07^hl_*C4mIfW -ro7plBoggBl4NziCxXvbDB=KI*n_DR~?RHH^%MKMN@eCiQa-Xvm6;5~^tg#(c%H)L>g_*{bb!o<1a<0~LvwKeH*^?Q25k-q3uXa{gTf&|IXXMJJ&8 -VzF3&e9Y;??I^&K>DPy!=>UiCf4m^T2GeysxZk_&A#fm$Rl{C*R(YtzWEr-J4QM(2BOSS_PAKLj%wMB -qEi6Z{il3=ybDWf(r1>~AZJ;+p;cvT)O3;9o-So=sU4@geHh;*fCF~HDOH%!M}prPBxNT+u5 -+JJx1-?&{nG>(^}!KBzro#`f%}aZ#3I7w_l>QQT-f_-AhsS&9hBzfIxmJo@II(J|!CwwhMQbP(ila4| -%o>z7l(072EFoxUJtQj2Am23zfQdJIZ!w_V1xh`pN01^Zj*uZEerZjzR6c*G)lSeAGJ^#3=1{&_#`|9 -DH+oa=X8xG}eB~Q5OUIIi<QNhk}4bq|}^n{Zishyl#iL^K6z& ->o4ZGK083EVC(h7;@Zn|o0u+f{#83q36)|uV4qduTM+5^0TD(3$jEvKydI?@OAtur-8DM>a(RArdxKo -<0@Sqv?0+S4=4NnpqmAciX9v>;733XTB@P^u?rvLl+|VF(g1i#uttvVeUM#1yw@14cvsCL+$`E8jd=e -h|JRwKRepjBl{&IDDae0n8-3L*%%)Nfxse}1iUCVvj^77up99|DSrYfJE0R@6~2XK3Sa!-T-8R1E%^v -fK0fN0nPiu1s7j5}`5i_Osv${5GDa>?Sjom(e7V=O{r5mm<1h^J9)%(xvmR|=gD#a%-#Ps1xgSK#|K0 -&mXw?V+}d{-9~BX3}+&QL+A(+4X>2tek5xtbxgpO-J?I2FH@XWxcww-!-4t$LaBJYm+Lv>DMZF8rKZXaB`Y!sqWVG5Jb7zj0j?7qtzbZ7OJYxa5VT@E4(W^JlkNlp8HzL)KcpC#J@1xpMSXI3 -7vW3ajcc{5Q?*}VGHqeUeSxGWbtcJv4XjlHmP5SMd4D+9lcg{aO{1(%j>uzYIe2XbOhCm`2U91A*BuP -k0?-}4{iWw(!MiX;-5zcP#7GWxXqanKsm?U|=LWw97zy?W#oSD!3)Eh&dC;#MM?Z##i39{ujWCDtA=+ -@kKppmf$YTm^4vqTfVk9(`^?x>By-lU}MH{P3u)ep!g;M@#{QW$&r1E4~{E6YH4-6E7mG6-|ZeDAP4& -2D`0di@s;O^(;SD56;wMR+%4;f$#92E9q|KTt~p1QY-O00;p4c~(<{`37n|0000`0000Z0001RX>c!J -c4cm4Z*nhiY+-a}Z*py9X>xNfc4cyNX>V>WaCuWwQc?&@Eh^5;&r`_EOUp0HO)LSim6VjYxZ>l>AX4% -13bqPLMtUZC21-bxAPrzC4I>=|6CDKuO)daXO9KQH000080Q-4XQ-&KjFir;m01z1f03!eZ0B~t=FJE -?LZe(wAFK}#ObY^dIZDeV3b1!#kZe(wFb1rasy;*y2+cpsY-=BhTH%u-(HF4Tu^_Fa@ldi>_$KrI|5C -<|NQ#Kb_)JQ6cSM0m*j-+JCZ*RqdppnFT-|vn`mQPY4H3{`JWva&Qn^3h#iV2CbB-BF0inxVXWfTU`84G@(Pd0^B;@3TOLhFFQ>)d&m?}j+@?0Jk|{NkE -s+XlX`02ASD<|8DJsJKJYMrs+~8Z{K%MzwGyq)AR&H^!r^A(zxIMq6n{j#xxBE#7l%GE(Q%E9buT){Gtxp-O7$2d3FYIHpFnc(!5c9hJn|%nL_B2DGa4H+Yix+ -EwgAj#$uLN%)XGEdMy*I*brI>CMrRxI*CfxI#9<(KpmE09MTfY7^;@v)TqJBU-yn`o6fED|7TVK1@GVKB$av1#~cw&$M8Mf?=Rn@JyW?NMEs-jk7Dxs|JrlzEot!XH{PP^Z~lwt(BZN)7wv~pE#S -qiS6Iu~bW7w1Jm?OGcv+8oqb#7A3(!V<6Ta0>Svs>qb#;^28q;%4{IG6 -ptFMQ;(o}EAN|6jh`@aL-5XE8x$`_#nEdRz7dFNDhnfxkz-IkY|8DX6LLieZV=ZBD6B{Et`3kh0+PQsh6@>zr~OhfX!Q_^W0fTKFzXL -o%GwJonaF`EO4S)NPs!~Q8c}y!d!;Fga=?wdla_C^c;o$@pIE-6(x)2%@%K8c;9PxJQ6~Ja0RKSd~@R=bd&sN16*h)6yfX?a8u*HJUnSB -d{>oK?jzm!BYj_+uAPc#e13I7ug0#uVbrnFDqe>E{sRheWMg-qe8FTZohMS?B~A3@qEgIPS$%-l~HwV -{yMegNm{5zPTg&}UNbpHF`eIv=jCPS39cEaN7T^`40IBaG1fv-|?9@2<)E -oeVOX=wIzr*gt(rVKUDX!*T&kX8%6OV}1adKigjIXq2bKkYTqIp^jxasttSL;TR*>yEK<%+zAbGJ<&+Hflx2d^Em;= -MWB-u35n%O{SiZLoaNQBhq^H%JjDo$7obBO9Z^NwhyNh?mPsC6NWB=gFUnl{W}NiK6=3En*)(?snsM# -Ms@PGiNxhvxuyzMS5867!~0-v6f>oh->u)63$rGc(rr5r0EA|F!b>2u3Rl*vA%tCI#!+ctGvvH|xwmh -Jn5U4Xn!Gn;2H2alRi-{GqjD*?hAZFXAWqd~h-&eF65fP9DgGBbG?$6q@?Zkwf&hXUU@N?_-N-dn*f4 -Hu{{T=+0|XQR000O8`*~JV65G%(0tWy9t`qFa%FRKFJE72ZfSI1Uo -LQY)mLqAn@AA;?q4xVR2iqPFR66*;neg>9g-k9CI_Tdb(Ii-wY?V%K6ahh|Nfp`UJTf7b03dLF)TCBy -zI=g!zqok)i&qzg(M>y(EIa?_jJRue9kjLUl`tmPh8N4=i>I$d>Qg&6lKegz0=+)-Lrhjh2U9AGNZL% -Ly7=6q-7TK8GgewrkD$v^T)MhBo_b^*-XBKIip#YLtqP>)jD5gg3$|?3&N`U&DN4;j1e>zEN72fn&)9 -ESv;gwo;~xJ#lQGi;Y>) -0;=Zk%1UD1i@;rWEj>6I2TAN*U#r7PPTPfYvHBXcq#Xu0Opw=EA)Uv2-ETtd{j~0n!e}2s*BjXhl#IZxOSa8nmC?vn;tw1 -CQRQ{%E%ua_J+{2;GV0zHmAy=v2Qz5B@e^CYbr0M*3HDev8H27sXAKTcU%ZQc%{OSOxO05P3OTclH_P -mpiHd-Qzu^K0-k6eEEh?*Gd1pYdm~1@-Yn0S6a=%iT<0tZh%Z-JE>XMBT-A)KIi5t%>yFJDEBC&jAR)=)ymEYEu~-~QesT>5k!k|z_rB=i;3|@E|XD?X4IH&45B@gaG`VM`b&dG#-;at&!lGUowf<;e(lv`-} -IWn#m7a)ZoTIT{3QZDQDn9Z=a%B3JM1WRThC?*6TFZ=x=ot-^_kLIHEQ+o!X5v;KdfX>^?LWa2vq`;d -xi^iZW6F$%P6$7jW>y;{tLcImok^QN_W(9)0Eb7fzhMRS8k~Yz1t~9?fVDnPz9#0z1puFe0Pw(KGM -ZtzttM-C)Uemb`boJ^0muZmnGpschkCCzrv}rlhHNwW09&|i;YJr*@6Ci4gXXOs6x{)-JBN!AG)NupE -*oJaSS)(SZyrE=?Eij4kBRBJX;GqV)#uP$3S%{v-B1xG5*fT=(6*>!iBbD419cK^IF8Ne-98-|9jr2x -7zgUxU^m#&62fP7>zzuvjSF*kmPGNq44QP=3hg(JET|9?&0JeKKbfcOaD8u))QWJl~lG%8W*9CyAEVG -|ER5OA4ugeXv*@lt1%B&*HWksJ$Zv~D-45aMqwpP{lZp%1W-jJQmT5a?Op1KMkn+}JJNPL;6|_;EfL!hkjCdJfR$8mErge -ns4_hVE-Uc<6ZPYx*<&kb|L2mYBwwxvLfpx8`bKS@zOS+iN*#X>w<-K8LcPcla8ui(N*fb_=RAC^uD^ -}dWT)Fhv1uy%5P=7w$=h-~RP-s-&5v9g~G5$=Av4p{k5>NXFb_^sxdCx -HvyQKA7l<5$b;*b!2!H4^I(x`Us#_UzIt7mx%W$P_;=%cw49NvO7GL -cFwzompnBthRt65rG_~gOP#(W(LmRzeRHw&1*z< -v3ytDb0|#$VT}k>rI_?19@1~y`=Kn`l{{m1;0|XQR000O8`*~JV?nCll#smNWehUBq8vpFa%FRKFJfVGE^v9ZRY8y2L=?XJS3GsKO0sf7yIfF3E4+!5wUpSwcDAajRpgDw$$(><8G -Cml^}qqa777TET5&8raOB95|FXY?H#2segcKGbBJIA+%=^Cg&71dL4xoM1hFKcYWd0C}JZgU+b~Nn(x -@opzbiT-;Icvf3{RhV|ASs*El*1Xpli*Xz;loSy^`rSRpmArGE+1$7l6CMrbP}BA%KVlVdP>3a~32y33bR=dPze -PB43c>KLWu;dP$IIw!Ti1=5$7+02wu_rB+8-Mgj&jfgp|rk`msL*YZzI@SMK?#BfcpJQ%yqnbk9UaL@ -2?eGd$~C!LImf9kl9(83@!9- -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 -g@jH>f{GYLXm@8F1>s`EqKVV7**s)e!#I -g&5XRibh6Sfsf~3TXaef$>`>3NBFR`gfWCH~$izCa&!f8Tmiil1^Hla~Ktu%K0G)|DX;Cq1&Bw^gvj2 -CG)q}Z9FOGGOyV-o1cxvq&UgI&4>9z-LaQw-mqpvOS}d0!x3$s@w3WoajaZhl5jrbP#~U85S -yjjiuRPCNm6)$*0t%F~Cmq(kKQq}+P`L0ub-@&&X{BX}F#Bd -+>dG)7LzIxNaUmd)CZ};Y&>s-DR<%ex|e>>@}>vs1`2R}Oa=^qC_cd+X!@7`jOe*sWS0|XQR000O8`* -~JVxf(FQYzF`U`4a#DAOHXWaA|NaUv_0~WN&gWa%FLKWpi|MFJE72ZfSI1UoLQYwODO$+%^*au3s@#K -CJd2%ieX{D;BYVTaIz9PGn{!iultOS{&qx# -EJeMTQETpwzOyS)^o)q|+nQ*uPfarbvbmKwYHxRc>O$59l=adgFhZMim -@UfG(OL8lrbR(NT-;Er*@D -la2&$Z)pOaWMQW;YIgs`mWtY6C(+$5u=F^!%bA3r=iWKDNYCe>mz?m04Tm}zwm2)SLHo}7fe`N(P3=} -(Q43&mC|Xhs#Q7cCSTS@l&`$Qq^?%Xgz%9z|J}5sZ2N?)qk>!GDG8VOdeSCoR}uR8x|v{7rp6-FO-tXi -QS>=&n@{K_IU9Lv9fA#H*--5vrfGAfj-?8;1&)WHFihth`^CME17O^Rx-+5XuurJH^BFNU!Bp1QOWN| -ABmpPPh5$+bs$0%*CSw4BjBqF4%orB5GLy)6!g~;o+?&cr#+9Xt2F}9B81z+zi<-poP3*2TnLFfs(8H -%#F);`XgUkQvu6@(?CmF`6WHW6cxmD754T>p1+_bE#eR`0B_tyIh23Jg515r!%U{`yUVS&2Ji%v^L$@ -lU;+Kch)Don(`go_CMm4sWCrWeuNfty-a$b9!JIQXpdsfqjR03=DC%>uwvs4;#EgKJ -cs$v(u0Nf%yy3a<=RS66`S0qi^tV($ji8lGBusiKb(VNpe5~Y*6w|M|9#&;+03>aD06U7>7Ck& -$#T|u5}HI2<$sOh|JU*{UkD{+sGip_i_{yn$tx{#Yy~c*;37s22c}hr8=OLRMcnGkTv45ON6=4glgRw -+ZQy;buxU}NpLWFunAo!*=hE5x7DZfF83Y~0eCB+Ext_xJkx_jgXxd{7HVtqqH|v?bvKX>Zo@mqwykkUvK@j0|FiGM2VAtHD<$tr+o -}sjHt4b99@EVy_DX&ghuip{%}oy+^Ym@KK<~c-Kt>X;*}USRyo_*L2(&rFW!h`IzI+qx0UMkZ!Agk5x -`cXRn&=VmlpPnCK?|ml*43ydbzK0vM_Xsl9eBbs^f?Ai;mr~qXC)VyJQ~H(TA}Q>4V=}^Slf;c-@g@T -y&ljqK)2w1UR=mv$WYJ;L1KHgoz4vb%~j2+tTndJbbc5bxZ5|E-@m)P4slw`erY$c4Vt(lhF}VE^$g& -2eUB6Qi2Ct(KNooppf}7Xd+nnAfTnK~2g+gsW5%ci+bPAbUtH(7m^h_Z(2I{Z?0b!D@JKsYDKQVKG;0 -ZG3JTdzzmkdi7hV{Pr_4Qj;`=Ih8@{s`+8;XMEj_j<9_geHq_1Ac7gvTy8}J52&j!gRB=&+xlU~XeDR -_=(5NJLNUS@mqNsr_H2(mwV^qEM5q~dp?f(Iodu|U9c43~!j*$p^Dbb7#yFcr6nB+pB(3njYq-2&8d4 -?W*glIE?bN=MIHOp?=f{iGhDl0GOta`^q+RGb8ho{Q#wpxDoOJi+#b~be@H+i_O4sfokZpe3=t;YmM@Q2 -n8YJ(&T}tT&bCIVGIP)h>@6aWAK2 -mt$eR#VF+@nr@9006lG001KZ003}la4%nWWo~3|axZdaadl;LbaO9XUv_13b7^mGUtcb8d1a4JO9L?w -#qawm26~8>O=E;s1VQmq@KU7S3cE2oZ9;dGHGjlPKfFoPTBL->%)I$Ak4%8p^dg}D=bBa%INSgn>Lhx -A<4b*;wyDiE5hT5~&T46?Maj;!s+uO~&|}lUBM^t55qd5s -eS=aO9KQH000080Q-4XQ;4ZKyA%Qd07wJ?04D$d0B~t=FJE?LZe(wAFLGsZb!BsOb1z?MZggdGZeeU+ -b#!TLb1rasWm3Uz+b|5h>nn)t&}2w_=xH#}!?1Nj4+Dy=JG+Wa*V=5!qo}67?Au34w$o-h#1ZxQNWRB -+aCm=!+BL>Ll@Pc+e25XHHk*wi{1ec#FDhdh$?CoeYpdKU>Dk!IGwnfslu`}0z^<~I%`?UaQDK`udqA6Ixw+E5Hs)$qDv%>}z6#ochKvMv{Dn2|f$&LF) -1&v`gm)S-#yF73pyl64=+Ux{!U!UwXe`!JR?}3#LuaI%k6L^9 -_~X;v9R!=2V%LkZQi4v#W3fz=zNQtOjPQANqmq06iy&4b%ztlf% -yOZ8t~8vS)%f@YB;a`rS>6LFRFk-Xc6+1#^-pHUWSFcxFKGk!JH%GSxgvKB>V4eL&mt|iA8xo9~`V2C -NOu$Pxa>?9BC-vxXNp5cfKV4X1fX&uA;#GP!#H9Sh{WA>(`XnqMt=lVBhiMyB@6aWAK2mt$eR#TigE<}tJ001mh001BW0 -03}la4%nWWo~3|axZdaadl;LbaO9ZWMOc0WpZ;aaCz-KYjfK;lHc_!aP|jDb22x{eAq43C{>Q*xpA#y -=WJ)UYO^T@5+Ms~iq!I9MKd+`+poLvAV7kOoMi55=T23LB@$>f`rVBtkJ-t0Cv1@?GP|F$x>}z639k$ -WM@L74Lj1E9WmQHz;hRbn<>0^CpTXd6B}=x>lR6auC#$&3N>;4|s|uc#o4lx)nGTnE#cniIR+j=UG?nx3qfMTtYo -76YQ7}E{EnIq|E=|~`$Up63oF2oJemI4Iy=0{@Kz_QrdwZj=_0Os+nVK0JUX)`0BJS`zxfXd1#4F9$T -V5|dvlgIP6*~js27mNV5T6!eLnJm6&eFUT3DW1hDDvV-Qx(HBs!yJd%LZ@r<7ta4`G4}n%Y4D{!e4k=>)C301cpSUodxvGMZN~ -zH#Y%1snmqcIarda028JMqAFM*qu1v@p1!-hi_cEuSMRP~U!KQrPw#%3F!2dY)S`-imYa=8Vl*q|WWW -NSdr`$AQ&v*hiWSfuEE?Jmv4QQz8E-Fi-{6RJY%^vmTxXR;yaLua#V@54Sn6&hI;tJfvN>F&S^l9{A!W#9Qu -#4=Gr!u)kvT^&9M!&nc+yhXLgh7TpNI0{B7-T^%6pevwg8{5KC1AbciB7AK^9Wr@qZ`*}f4(Jbw5J#- -VzCRlPn%G1fqC80qwjce57P17cMn_qHlErI6)yzIlsHPIKPF9&%BZG-Nl>p>vwl><0=O?JQyreUY6_?h}f -xe+@pN{50C-93Gl{u;NI~*yR$xC4<%P^$P&jglT{p#K&Y1v31B+_)LDSUemRcpgmD7|gghYt6Q<)47+ -n>DuV=LF39FPi&Jg_xdlZFS?q+vIEm~5v5QQ0p>IEdGUk+k`{Vgn4g47DIEh5pe3R47#P=f0M)WsMkb -7NpZOko~h8&szZNi`*kPCvGx{?NMc@*|rI6a&~Zz-)n3o1U^!PCCoeG5hOEK%`Y*5T))?;HB55Xe7%AAjd_=_S%>=y(XUjC_z$kPsY+8ur^y5f}bY^* -g(?u^+F`dbMUsp?HCoIO;#jcB$&>)hQqir*!GU -ZwdACAC@=$K5GlUTB@|NUFE+x1&p?9vFQecU74g8r47(&6U2R4VRXQymlX9LQH&bwsAd!i7_ -(s7O7<9Hx1i&~$_CS`fG5G^4H)4U?0SbzB86CKewr3bq8hhrL9^a_VYnfIjz%CqZY#Ql;@fHg2w~ERr ->GvWN1y4^DYhm4D$rpm=*h~k)lakylkc@Jqg^G9?*D8fSZgGv0%_)3Wdq9;!5_imOvrxn+0aJV2$}}Z --by@QiSOgqjfNB9T5N_qzqFaR$`$2yJO;a#H>;$Pk0b1lB-=*HQkrmLK*h$cok<04!946*VLnGo$|sMLY6-&iNFhqR68Ltv9OeN -+X}rBZ(MZ~tfNeTrTqAj!oZJ4-jF04_+nvEkw9i(rg+kolz9r&oi?6NVdcu1VIPeoJ@kqR1o2%7QQCB -B-1f_tFAbuG{?45eLL*xW(dq&bN;81F15B|aat)3nl(X;MsU>7QbykgrtE$>e|L}()_|LJiNc1<3K@N -*Dj$x7aLkAEH9m39*@*cm%ZLMrBYULURf@(d2NHzS>6S++~8b51?ngP)6(%tc{n$B7wY9BDda@L7|9j -_smE?hR7u@>m52O2jQc@tEMGI}<^jQ*juCi1Iry(k|y4Kr~J94rcuFtA9h4q)gF@?zci`N -3{AvXA7+(7yz+-DKtZ~&WzSSLnD=<%^W7RP$0~lLfojm{={2jqEvU*z#F@g2|09GflKS=6$M4->=#s+ -DdyVvMT;1l -Q>y9<+ooXeOM>$LRdY(W)U1vnAiMF+}#FaO4@c?1c+>Ma?WT;p8>>WcFNTRSXL*&6NM-*?3#%jBCQHaA>@QO0oYW}$(!ncv5D?=wC5NM_QPIR_3~pkn -f>}$&jI!=LOlnZjt5?m4I)}(5S0}|_;DA6Qv4C(A7546z(O&1%FeN3#va6On->rR7WXwq;&fV(AV$(M -oRC6JGs|j6FkIEfO~7E_7Ac4-?U3$b&C*krkhwnCQXgQXL-yIVIx|;xN5B`FUd}=qGdoaATk1;xQxOM -}N>!+7X;`Dbo*j#UY$hVX>oko9gfvstY8sVv%5%25aW5Z1VhG0XKu>8OQ=7N%R6ape;JtJRmI8JMtAl -bRb)mI^mhGs+RAB+&ypn2j8;d$qn_~wDxhf^KU|0w4iHYx~8o}<4d{4hVI02r49Hsddrw)_K~id2VH -8e+1l)I7sw82;jyh2N&yiHerzBHT$YGBnQ0g_W8pK-LLbZ$NCMXYjg5lZu4VH!e^WDYG`Riej2xil=O -k}Zd&k8z^aR>$MdZ;(B>z&$DV^}6MyF0UIJ6G3N=fl9j1~U@p&#do@yj13V8X}M^u};ggg&^vh}D;ap~sL%a6t}Q%qqKCnxAp19epL)CAFlAzs -z(Hwi~VO$kqKNHBSxI_9i|KJwKh=&n$1{CKBmcS_h9I_c{$An464n3?Upa4X%%*aDNz`Qia_MSVt5$B -;%0(dAMdvo2%B4W#&X1)IThtwQk9b=Y=q&J>l=QMD6qPuNi19O{a)i^v=PAehNGFbX=6O`}#|nI<_2G -6zn>l~~)<(yC>T|+W`-8Dh(I{zV639#uLD)Y5 -!mt9V1Ce~tu_E43-W?sAK$mQkO(>PTb?3PBTj1 -g{N9qAG)WlO_xcW$hd%C@zKDk(ZnwBw^yURJ%~gx>=?I(iFfD_jS}z^*HNNysiR@?>kCVvHb?&jlBje -!BgFngaw|ODj_h(ZcS^?k4xuf~X!H*fq+KEfDFkj>a}MBlx$^d>ylE)C$EJ+twf$<+S8me1wq`Sba2| -(LQ|qI4nz|d(`c86vVxKF3^vD+wS1N&o_6ymL;lsiAut=o$6--jnW5>4b-!`C`4AJJKcao084xHVhY* -FS%d>78ZE6`FKy~sx-y|4;Aq^Sa659!a4n%r4Uo*jp&Xs(^jD-Kb?nnVK~+wyuM$7`KNX@Z4wAvou*_nR~!%OEI -1HPG6Vn^qB5bHB+x}jp2LW2rg7T8w>*v5-E?ElTZ$#2m1YrW2C%eN@AP-2C~d -_^jtDWjuPYcR{Mr@_|vke~g@a) -ITzDOOl+`K?eBJles>plkyYkfqiL#zzRe7*=t2t}5jFd@ktdM^MV=l3bCB($h)K;m>rVqF-Fu3}<3Pt -i+XxtK4Nnpq{06xF3c38+n4?|_wPY@a%w$R+h)-P1x|1+irtM0%A^HvAh96g`dVcSTPzT^;yEGmr0$ -TO<`TkR1GuDelrh7kZ~RSk+j;$v`=%O<9|zPdU;yK8(cdCY%dS#%l?&>U%ADP!5}Q0I&LG9YAlP>`A` -iPOpUw4-;|=9-zlDbuatcuLLjFuAIzv42Z9QE!>OWY2(#K%mwbdGFY4UeR+g2K{F4`4 -FB_U9xX0=#RNac%Ac?KPD_nZY^ILy=m|D~0HV%ZxQtofkrp(I@-M$e>@7VR#6}*T{DfO8KsZ*sq{B#M;q -^lpX?i672BvIDOkylSUrUp2h7nkPqWy_uwgA+)zCaK!_5UmH0GD!Fb5=`2CCN2X#8# -oalB3|Tl8?ahj1FC_HCc3Yr&wyDMf(-Iq+tV4x6h-;m(|#)uq5l83<(6?5;f*o^lS?6C>s_ZA@)hft*IFb5y -nb>Ahz9-l`qUQi7;<&_AeND4;-{_`022=D>o+j{d~Z&lz+U^{&1h3oR63t?nQJaJwQIR3nveGPLU_@U -=71us%*iu=T}FX59ryqyXP@3m^5Qw%%sfOSHFe9KzuN;72|t)5z2UQz9A{|!(}0|XQR000O8`*~JV0; -;kfX$AlQ0vP}R8vpt2ABj_c4*m?H -V4$Ri@0~Q6^B`ufs?XVMNGQq-N1pB2(mbfzjg9sP}^#gb(IieQKV#~Na+fqBooF&aX1b<4;One9g4R`rO -+Y=Z#@dX4k$;Vz|~C?F{s%um{1(z`1|oRjt6vcc0N5mJDvDHj>h|k{_6Z}5*D0a)EE2HgCo8q4{>}rJ -{eEPF@d>;g-#g3#jBIYplEG$--ZZMrKJdptPh7WahG~MfJ{;V)EsO@OdAF;TU)xY%7x2W;*TtxSMsCR -KtUx@HBfHCX!%ct6_4~j@V;Fb<}$ZiWZK2csV&cC1YBOOuiK}();< -Em@fXQt0jsQ^}AxuCXS}lb2#$L8aFFRIrE_LM5#-Ow~ceq4&1YsLf<^W<4kkR??WBO-9qHpjW+CTdq* -aYX&a08_tp}}>mPC4_gP+LR+1|VSopdG^jwK_*pM8@n|)+65e3YUQg+ARqUXIXuW(KV&IuvtbMAb7?~ -(y@6IEC$WpC`d#4c^wC`*%vZ{BPB0FugBmA?sGI|QpGYBo?W-F8dq7?P`wPnyOll`&ax2$+_z0eC|#v -4yJE8(SZZ!h0Uv5_N>fNRkmgFNUb0j1r-w{(5Xv&t1oO8#Z5P1;|KyZlXBqlr -Xd`24QQQRZjh06pFVL+`zY^qwg+lS}j%IwAdoB_Whq=E13vw63q6|pQ+t2Y``sW}~5Cw-w*hN2k4*u` -y>O5@^3jbapf#E6^1i2A|EwStjv0V8f2P#Yy{Lpa2_pe%CHisPxSu;~*H>Ma~Ia^7kOTK#rc`3PB_i? -Za9(lVqC0kgx=p~xueBClRrh&fi+SO!Is8M-sjAcsG4 -p;Y#IybmNE@<4Oad^#6E_s9E>?F5t~BZa}WFg -xqGS#yftHMiu5E*)kI%SYI}_SWJlN6Td%`$BRC5XNH2@3ej}o05Xa%n)Q_#WMDxKDtR?6`YLhdZ7~%C -N@Z^7P^XX$~dUw4lU8Avp(2_ndm!Ci^Lxj3FUnfHVd5C%_dJsDH{~tMO1Dm1x##G=2(n48oHfdVpAX8A@CG2g?u~;ImF^V%JNafp-$gc^Gn -7A`J45l~SICF!Adhk%SOOpIJ2c;L=>E3JPoOTa7HXY1HSkwZ1ApD2p6a%E?o-Ug7r}g1f~oG(%Po-zn -=Ic;Cj4snMJS(>P*?(wkV=XfT6}Q$@c%XVD8`TfG>kqxw_EvNP)h>@6aWAK2mt$eR#UsQi7Rsp007@7 -000~S003}la4%nWWo~3|axZdaadl;LbaO9Zb#!PhaCzk#`)}Je`gi{oJO_oO)>a;CyCSGDWKDK0+NMR -C;d>%HsEalk^@LJ{*#XnDSzNOe#GaKEpS?UccY(N%p -m3N~@GsdeQr5`04d77hI90m{vIh{6bS+D54jPATTR5pHR(3K^C-_=1eLw6OvqbiiUoVFH0dc5z0SF=A -SfQ*3S#77C9f+OAE;;3hZC3s4U>C>h)$)EXkNE_Vh7vi{qz{Spf<%x0+7GvZUHCS28cDRBS+$mIH$e4 -@lfYi!08Q2WK~0(uvO8>r9L63zZHwOT0w3_<1=E!T$5Zk%B9Rk7B38}RxESH~NdF -Z$sQ_z3hzL?v8W6^OJ`{Nb!LwXKyby>LElMJ0grkqCcDRb(D)SxwK*){Rf?jh`NdjM{)pAV6KyJlI!P -wMLmkb2HW)2;QjoE~P5~O6JglMIbL7?TgAR{v6I_*1H_zcOL>WV6A-?;ae3m|Fm77YF+mu3L3jvJlJhG@07<39WYCJ|IS2>>EyXn`0JMHc58yw8%4ehy5UUE3OVeV) -0%tU#>fC`Jp9(2H#Pw_At#eTfEdudW9REU)ToiLAQ4&>^7?M^NpRlCgre$R$?{fxHVE$h-lbMW&@fSA -H#i$*h7nV~9L0hz!|DsTSgLJhjmV?5PSS)>-R0tOb -R4B$1TW%Eg*!=7L@i@$9>+|YD(-ng8<^yPV2U{~XR&p8Q}ur~VP#-UVWtqJh>YC~91YdnI1TDElOQd@ -hF5n3(_*p-;AB~>SUww&&_t4QAbu|ND^H6}&!iMGj!bh|K<9#ge0Sx+9Ftk7nw01;c3b6(8z_){ -yPI*dnT&cH#CD~7^=M1CzAfZ>a7&FqM^j_%hydX4=#2}Lj1F43`?A^cbI9}V$jwt>B=KpG4)=Gn_Cl8tq9u|SO$DUV9|Sp)qS)z9ESfSd2;t+F;4ZgJ= -J_lBM`(suC{|U3k8neo<3>)oV9S+IsuZJJbK75GU!eaC-r%#Mw;+sy6ItIZ!dD9&v!_N)qdsB~*<_ssWzyG-M?#@zDU=Dnt -m)uzW<2FXUqFT`Jry?*vyswU4&|NwXkDff;tS2mdS5<-kHz@0+-Kp=B!=ugm!oqj -e7x;gJzFyj<`n<+NG+PRY8j$I6gA(^2sN)B!PacpMmy)Dav>MVIi#TZmRO1_D->#8+; -H;O1Pvh5p4UliUrS -Y)wdoS{TT?LGb)Oo#5Yi2Qi`2O44E~mhrQ|1rt9G#jSd-S}FhO`0(lHBy`*T+HW^7+Mx)m1FPe}iIH? -;L*ww(&3jea41RrTNd#rYQG=jMk{|?tmKoykI3M;+W`J-{kh!pwsxs#=Rt>syydgL?gL~f#f86j}E1* -sQ7dT$8f#YT*YvCk2bo2a^I|AAkJ$nIU&WDD)(CO}CxohKL{_WOQSJ!Y{!)p!LM7haMnL1~$L|~E=KQ -)JDc#Jh_jz30V$-XN+?%A+%&rnz0Q$+sJHE@I`5{0tBcT_ZIcdS$cZ`W1?+X=dDNh2v%)qz{Cx!A88X -X>paYKWRj{U#)2JJMDv*19zb3mxLGWk`SwYok7f1lo8qdZ2tnHszfb(g61^Ci7vDmY& -8AxMA}(GssMt&@gm>4B+Sj6oWtjV=8)w!Z7Va6~q5YWK*iS;Gg04lIR_Z(zck3iE8PiW}$;jFE-br8? -fj!j*Hd^`53)0JZx1$( -rIQNF$SkLGHG;)ge?5P9arWZm!&$P6pF3q*yK#?!Q>vS-a!t-uQ0Q5#8EwV<^>o0Q}P0BjG2L#s-ZyQp_|XUr -=W|DCvUZIq+>t%MzjG8i4oN$gu5gsxg_obF$f9r`k1Y$F%?y=KAP3Jk2&I%R%XHi?aEYI%${t6_jv#4 -P6C-5r3lKCKz?aOB13X{f;WSZNGHq&COQ#%i7hl?PuNHjBY(TmbQLsmgj-C>}JABO-?lQ&T*xgxpwT9 -{8#qgdxD7$wY_~w0;4A0m7DaP+BWQKznFFNA177G@V6}W-re*8YI@1g{8Gf;lnk)3;C2O(I%L{?$90> -Uu2Iw9GH@L!f@6*r9U|vz73O}Y?w9~`$PtJjyqAsZDL8Ul_hza|z8T3+Rmr!d&z<@6e|CNPFHlPZ1QY --O00;p4c~(<`vZBu}0RRBe0RR9U0001RX>c!Jc4cm4Z*nhkWpQ<7b98erV`Xx5b1rasRgkex#4r#&uE{9o9CG2JsPmAzwn03k6%PN1E}xy}a0$r28Ywp5zVvkeevx61(di> -gZWcw^Z9R#d2TqNi@vl3rCd}Jazp5q0;!URr{GGPaes#?f&P?nIR_4*^3gaHIPyj_vWZwB3TLQ?*5i3WrCQ@&V5&D<4b -dD46xm$(ZHMcnj7j9ZBBKp|um#jgu42pN|MEeD(#rPmMQM$Z;ValM?k#N33=*OG7^aFOc&gXOy1%%D$ -2KsMQ}5cYV}+jx~k@tQFUhwKyEGwzhLD)~;_2#_5$MZ=@?FBB_bvMm7?!YUTMOxUk_iV-(WDX>`p3f=QJK73-zu6g)Tt;lWs>;(e^8S; -1^9k24zk(|0YXMGr0`pttp^#djSVpxi^{HXgo~L6Xw0O+RoZ8C1u(LIUf6$nx?0;?{n_Eu$aY!{ -nk<%z{ipLI9*}7f^d>$*$I46f+&YT+(64E<=>{9k{l+O=6YqY5S1w0o}GybnlY_dJSNd5SSiK2tAE1U -?!jH4Y0My~R!G0x$l!Wor&)w*m<~IG-g`2e{DWD~H`HZ;5p0m)(A+vu3 -g%5Di??wX|5i6NQJ0?(3Qtw-0nz-ToVp4N(g%B^I8pU;kX3aVhy)Qq33K7&?Q5;G?D2N9Qn>f~|v)UZ -~vpbgR8>thFby4tld28p<^Y?&w*ImC(kZq&*dOXho6x+N3BG5$7p&%%GB*w?iZ=ZqOcp(e0-Wx8z(tA -!qj2WAAwRM`S+Cm8^Z)pP|ei_y3Xe!_hJPJ`r!YjW5#yRz_^r*xhs76#Boja!k2HxQwrh-)8VjoIT9o -^nc3O2}!eNbNT{jf5p}*`vFi(0|XQR000O8`*~JVy(bqsz!Lxf{zm`+9{>OVaA|NaUv_0~WN&gWa%FL -KWpi|MFJo_SYiVV3E^v9>Ty2lz#*zN6U(q*<0jZ6KPMj}y3WPh`NzQxm+KbI>3}Z8pIAo88I~1vOlcN -<3=eMU``jwa-aNj ->}f;4_OcFJ -Js&Qy;O1`G*K>cCGYy0$D3Xmt#`)1U>W7M7cY|32CW<)ewS4}5Xlh^BTN?7%}0iGV^T6YTM -VtNtO6`R@AxHFl+JfmIax%w;yED&6x;70ALT~%N!Fad0R?iyihkih -l50-v6;4vLNGAxa4pc2y(A;a -+y20kAmJwa6RP1TDLT*uq9NFsCGvWxLC(MvL|iA6fMhc~jDF9v^Vjs%l$-2DqLqGyxo-Xs_C)K4#2&U -CG8Y=zZJh3N`$$RY0_^VGM2qlErx%c>tZwfom-uD-h~dc58UJHV@PJhSo$<+AE2Jao|pXny7ZKs=Sjp -^ABxLMB(8rsL@dza##eBV_po!4a(~dgjIrI>h_S?O=ni2O{_uPt8TqcK`rkVmSLkF_`&DdT8Wiy8?*% -Lhdw|46gPLry@bcs7*azmg9r0W7r>kJYr*y70O}Pi0VL6@#uJQ3+_#|B(<7#*ZDd!sMNEWud<`s~qeo -=iob#9jcpq>XsveGgA-)53u_RdkNL6H_#k;rPh>Eome4-NfdD$c#>M%X~x(($0!YGe>00W!0vpbZTUB -CsV364yHRSdacVFa{a(JGFjtV)``>QEcN)yYS&RyG!}QdoDV`hu6owhh|F2Ii=tcwpCuE;Ajh^gXZgW -+7z5!Df|Euu0~Q6zyMmN`->;za6X%B@52*U{?t|OsI!(%<20`P&}#8&jqowun)o8-v>WbzSHg}*oQhU -rbykXe7gfr;o5Ej?Wy#-Zyu$p?kWrlCzS76^?~N!=h~Q^vD_x#$?+h3ioSLrdGK(9rofB`<~5vzd`jN -e-Rj%6nV51)9eLbnrl8HxoCjjb%QBw*K^_<4F|UI;!I!zHgE9ew5VhP^5a7XA63#$pOu=Ud@uDq%hjd -Y)Z}Z{-P3YM60(@jCfSU|`qvI#9VEDIf2eG%7xO=js0GwWMd~?oR-?x2T62K`-&{hH=xct6RvaA#tiR -EQ}G2n5kQz&lWxR)c39EkBf_PYTap`P*fh2iClQycI9|6s(CsyjMz&(Kk#HB{&MDT1Hqbcfwf()|<<9 -w8)l%cmjIeb?;~-h6$1{pPjQ8gR-KO3GkYtLCnSUlu22S$4S3yZ;tn=DGsJS%W5nUur?Y&{-KThjg7% -5x&GmhzlT17d0H5ION9wa|KANg&{!as+l7H; -RfM*V?@1?V%_Z`&PlU<*=oVA{yq$QkBXiV4UqQUnE!EKuO80_c$ogW9$0G!H46g)RkXtY{C77^VUS|H -fl@kxRik|6AH*r!Bz%yNw2|q~N9Vu2ZR{un@`G0`9egMsoxl1N6d8*|_&;&*|q2lIFNKczX%ny3%Y#V -BCAKy--&NY;Z&ep@8CrsgwoFX0|;5KoFrpZh^{EI0!VHu++XsNP+;kbr=_BTlfos}R61aa=T+Iwx9 -W4FhGobfd$@$W=B~FKp0`Ic5WW -~FBU<#@;ObraN!t#zv=Ii!6>r`}Ex~up7pR)wrOiBxL`1C^P*FeL-_SEoG-(B(NRA6{4q$X9yCt~yA1 -hjBoE89XhD@`U;UHczE(C2Bd8gHz$3VtV%EcRdoQNa-kup -1Co-Zbroj6aZyC@9r!6!brNZ&&6lbv>b{hA$DFuxwbfDa|{(z*igNg_7|BFef+1V=$euR( -$!m3C)~Jdi*D;4;ai^S^fYY_u}8-E#zE{hmv8z>7er3eteFh7l`WSc7Ey=nW{O{$a(8%aZjG#MdQSeC -RbX$KD~5GC5(01~kAHsjyC$i7)EfOM^;xCVM%w>VZgM2s4$k+xBBj3(y3Fg2$>PebikJLZZ6bX5(vq; -Pqv;WkuR-zWM%5&_o-s+5EApW$@I<;$2f$gglkp_BHHhpcj7mJM*>omd0UgJ~rv))@!JaUwFz(fUUtqo2Aj*mp?8ld*#p -=rz)*D>23!`RFYB1%_n+i3sC*9yn;r36rUzR_S@A-=eED!dd7j!G{r`G*rbPY1xt74fnS-A3iSQw6?_JL6xG3=0Oor$wT?QnEj_$H%|L1h_3_=7x-Dld`VxlTs;m<&b~YRWqZU$A4i -F7mYY*v%4idE*C{A>s$DSWAhT3|5SUh$Wnb-~ob*PVNvUPwG~TEV28L3H%%>lL}y#=lw~(CwUfNDBTp -hj!j4G47)>CPqS(E3}@xSvfqt)ypM+FQ#E~>w5#GOQD+h(dXYhW4Z?~T#mOp7jV|G7rJMG -d28_I;9+D|jxlcssaj -v;D-Z*NHc~jMOF80Un9)JnsYVtTHeC_i}>1XKDc{xuilaWQBOW3|Pae6|G?>dI$eENVKliZMN0{NykV -3Io&RJaD)>@xo6D}Yb^4Q@f)27e66xN|U<&8(xrqbO%O55z##jZYT>4Lc*V*lW`NA6McVVRZoRsZ4f54QU96i9lo=`JFCwuM{0Z>WQ6qWs=d(`%E~xobl|xpgR*A90Z-rXnV+w$1#h~x#w9#m`jD0fJtj7Du$C2# -V7ykHE1-3p;POPVt3ekZnk|?DT)e2*Y^s-^kA)20Ek={)(k;cn=p?hVz`=2ylPmJi(E^8Bf^%pXtex@ -HGrx!brBOAFxiXG6@Okm;yLAqc_>{yA>HvI{6NTV&@UuntdG`szIya$3{;Z?k=lZ_jTBbajvmSUgsk< -XtfvHLGu<#Fjs#W8`YAP(wd`bYOv2ala-$=B{;`?7H@Hc>d^Vfu(!(4Jhtqq0zEUxdibXb+=XU-9sXy -lZlU1XNZr{)uX6S4dBGFy0I*t3y=a-Q)%6w~@TugO<=;xbaXo9uH-#HH8})mw#xK-JwWb2GWY78x0$WkWnS4- -5)djj}gNBT;1pii5elXcmkV2|1(uFJBvo#2|=%|T%G;8syskyOp*-OoY(|D^i}A)3lnj;Hc1<^;I%>X -ZPh8hHc;HRbP7YKx0cYKr4$ox)Lx{bi>MG3yi4oQ*TuoZJF4+Q#`j6+z!H3hIchdR6~B@rbkJ!y&Kym -O)S|Ze1PKFKP7SS?DD}=4briH!UnsR(`<;Xs -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 -VDGEWhXq$;lsw9;}UF6?8yh+r}bUTrQ -H?Y!>YVKSG-BDpd)Rra+HIs1%ebTxDBDIg28<{3!8<7b!Bebd8C512UiiFUl8o5qy44i(7 -UKB++*+Ghr3UVZvB09wLg~h;}xF?tk3^K^+R>2EZ2T%tB>slQ+gRUB6EC&~bBr*tV!JlbPV8fjv%Z|j -z=^suO_--21ZnY8u7m6B0#dHoQ@C(L_yk=T<38?kKX}?R}CqDtzT#EveL(?}H-(qb$zJ%C`#!HAD1HE -b#<2OhA^MOk6DGx7PJW1GKnuVtHHrDmJz68pk%!H!bs>sArTQ3FQWSgQkU^yp}#mc|{9uv5=0Ql`jaA -x6fOXl1m*fScUd+Sqq;8l^MZA5W_SKK1;N*TXxIeG`9BM(_gyfe2P-L7pRwj|8~m5Gn6Pl+&qSB+d!8 -d8JAu->7&f#y}~SDQRTcz;4i%(y3ruhmE~Za_Qx9Q61?Cgv}O3z%aqLNjIElnu&uJUM2MTr3-`GhY)} -l>LWTTfrCY9(NORf)pf^`1VW+_zw5Hqbw@|@0|8foIG;M=D!U{kE<-tV9|m4{d6R6|9z$ad=DSER#OB -%VhfNbEw%^k|FP=p?rFDwVNZ;|u-G0WTbhr*X(PSHE>Ef};JAFd{NV%sTc!;{;s`vY_`Tz6L -ZcOuzibO?Z*5dP4enk%DL^RIQC$tOrJ4m_k?Y7B~qw?b^#T9@9RL6TaA|NaUv_0~WN&gWa%FLKWpi|MFKBOXYjZAed2NwFZ-PJ& -h41?-CVBvka&AaGH1W`+Nl997#=r^#Y{XrP$BrMhH`Lo0e{df57viJ5{&D8Mk}^^ZDV6>%#O7$o0B!q -qw;Hl0c~yN^;%ONh}08f?8loGieTig7&yApbK=>ujKx6{i_EB+Sbs_ZHatH8~SP$Zn(AD?;U$_obsxQ -V{bNocF*0qhuv+j4bGXT+t -hrJ3+28c4!)IBiw)M4c)CU8UhS)1M+q&sgo^`q_Hrjk|Z`!@ReRa&=bj34NRee2EU9GaKy2ur@EsIsr -Dd|)KkS67B-izy^yC^D{MWeGEW}MxqRkmxj`mwGrciT5jQ#b1RLRYHovMSG-T$ROHgDZdqLeEV7%kld -eKyG)d3zgBcH=8q^=Q>yKZ@P=RO4W(h>Z0qm?fl7;a~N@Vmagi}lWn_xVlernXj}O3&+=2)?|RqCllM*C)vLOcuRqqS41v7I0qNOPF -jLe9?#HU#ZSj-N-)(h6?YT{Q0UK+_&-FTk^)Be4G-$hKx9WC{wyj=etv;?=UA0A5T~*R0&!%)&QCx`Krtt -)3%$u^>bSbf$|C>x;m4VJb+M~1b^b<(iaSvMS8zgg)zLoKH~6%IGYZ4WgOd(U$p#*`yA8nMhcd#SPu~ -43o56Ek6acfSvT}h`Vkg|yiz-7bE&$Ik)=cfHf(Wl@7g@V13hD3l;$@x#RksVI+bY{=MjUPcBNM1>rt -mn`9nxXJOKWM)i=0So0h_vo>BD;-6j{dNQr|3K4U4j9VKtVfqCe5CNue4TUW5U^ -$ZUG7F+9TrU3D)OVgiFQ*<{|OAecD*7>V?m6c_7R>E9<%C=iX2|eb`hjRlMYRX+j^P+=MT5v-kXGVT}TDUCaWcakyU+}9v8dNOFh;?~@N`Fv3U#RDr=84R%N>J3oB3YLF^-M38*xNQ*?u#I`q0PUrFAKtt^{_y72>Eh({<>|3H5~L -AOkUm#BpDF)uUp;^NmD6Zl@2Y5{XHKIUWK$Ds^xSFGXfO($Z+<>qy!zqe&wp8*9RJH3c>dK>bpZc+_VwQJ>6;J!yKlaFwzs#pD -zmmVJl`DdpNcOJQb4vO_}T#uC;-8X(CL#4o*ww -H9XOLeOr~S+sHZbXqd_=SYBEXxRs)Xv<^2%}zF2KE2>sk5c_DHY&TqD96BMXEs%gs*&+uLYlM%IITj- -U>vxGg;e)zu@Wr@lLo|W|u&W;Ff7XT?NTObS2LEs=~3I&Y?Z%kzKaF1{l&0<3u*(CzWW?hCUrSeSG0$LXbHDIzU-^kW6-#1crwhq@b&1d%pVL7 -GrMX(58mSZu5fNwZM-W?<_h;4$Nagjbi7rM0&_@4o8ez_nKF$g6S5;F(_qn;B*Lb!1)zCf&Ta*!uI>P -KJ1qBB;D{tq0`2JSrheILf%gP~JX114= -Q3iYup0}@F*VP2{-3(72Ob6yyv1Fp@wsRcd{mpZ*6cn~Oukj!@HEhNhtHlw0&c%{$oaL`iuB-%+dDu9K|zkr~mXK_H; -18gt*3xxL&AtNCy}ac0nB-sb}%+T7xmTMs@gV@S4tj58(N20A{?B7?4mp4Li$8`k_eG40>hY -HQGEida99OCef?#BOQO_SZk(77gta|CdMi8>k?LX7HiK7bUPB-0@rm@}O5`1wXbP*gj8A2jt-^?CCe9 -=eXV1TlDY$#VZ_&8*#E_6WB~WX;j~BG6IR=_b2$JaLH#ss)Owqjl#uA0GK;?HnppEv%>`2r$u6UtiKg -8=5>rP@(zx1NzY-wkIk@jXsCd1gsmKZDbnGl)eH;fC+fwt48n1Rt2@j^a9C-tIrNM -N}*Vk35;5kJI1qtPuCpw*#b9KT05c~l+Ji0b<^v5?l20nH17V8SoaRni=;fEgi$&8g#jDLvHh<-_+&i -Gzq{{}J`|u10ijQG3p8&-eT!w-AB{-!Fk|FN)O#GPxEh7r=DnkZZ0m^g;0Fq@63aR_5%b<1fa$<2C~$ -X>L3}05bPp3iFziPG8g&#dOx@akL8y^q|8ph9%C8UJmH^RkTfjVp)^|iCkwsMC4fW-hjgJPuFFD1X;Y -Xo7C}ItPeAAW4AdlY?jMU3Fx<^wt_AAdAUT48s&Ya$SAW>r)x07dfEi0t+r*h^3jO?(>Xg;@l@V9Ta} -=+GFr8vdP7B%$-V9ye7m1BM(eBv+$KD_^*#RU55=L(}Ez~QRA1lEBXA0Cw%S*ueX+v_9|6rXMy^@ -<+XP5m>XeCKz{$wYwLPQoc|JER>HF`$H=b}4%5&bpBapT>T -534Q~XpR#a)-oIg3&o%KZuh*bE2Rcpf2uRa05&)*yF)AMwY997XR7R<~&*2d@l2o)lV -CCDeEl<+qqL-|a441Ekdv`Br@26tFK%AOdoh*kNFM}a<0=RIQw^NM3}N^iE^4ed@#yFjiB9;FuwqJ@3 ->?0P8>CYu$WW6xNw+?c<4t?7|%Zdlk6ShU!|eD}J@!#y7hwvT-Ey704p7>YyMK{zVu4M8zU+rAfc(8- -HiI@+#pL6LnW4MUB|9%VREIk4EZ;97f -67b&<`><3HWkwDu`0t57Sz7Sl8HVLjM6fDiV*Nqv-%dli*jjBd#Cj}HHyhedy)KDvQg-ofb{$1WA3!6 -lPnn1^<>8>TMs+}c$$+tvdCt*U6yfMY9918W#e!hh?zyfovjQ|$3Jzy?Yq>L|P=eH6)Vne_PD{2-x`K -dhvTBDR;qO#l<5b(4yu+Y((1ju(`oi#wP{O+y; -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 -l|pt&FA+;)Z&#jaQL` -pVS2+KK`$+(BbIfO)%S;oaLK*EnVym?IYh7aPp>B{mC3pu5(17cQXOG=` -Wl#t?*JYPYhlj{qRcK0{Il~9E)^=w!2AUWa8du@8LYg9Ll0qr{OjtQ($7)ry7gX<%HLcB85_YMMy_FR --cEeuzU6qmMiHNyTEF*fX$XbnnVbwXxHL<;6Ho#{t_02U#1+wORM`-}T0hGNsvNt2A7L&=-;Z{CaF{j -)`(hGbIYR(sdHE~vBP#RWzEEee59#4?Rr(WwXeZ8_epme=JX0OV+)rpsa52`PUNgXM7QgU4_DqhVEUi -N(cox|feVT`vZGhrh#nEi5uMte;$9q9~7&LkXV`5-4Ju0ljKK4~(zZsix8(A>tC#6i -)Etw*sm^jS|2+zx|r3CdsCLH6N)b?ZW7NBXubJ3{k#OGZLzu}!%>)hUdvrxprW8`+lzyn3=jsooqsD| -&;ekXaNC2bc1``9Uyk8uTOPQxx-N&{K>rPIHJQ_)phbU;8lLY<=n0R~C)z$q(q5oD;Gso -p*q((2kcp#x3;$BdnaQ?nFgp{^?y4-1dO7=)ZUYp_!Fj0a*tr(aAAjZyh)OCA1Ov{T40Y#z6;9afPei -K@4q*Ea4sa5bG2uoHx#^#r8adcRUvEF0bf(D -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 -ik9vblsqx1rlTYC)FhuA0BrdtJAhXuID -K1jyYB7Z35e5Ib?HkCUaztj_3m-CfCK#$HGXx5WZTcpWSBc87$4f#*V|v!@)7EOQWUs%NLKhg9O8mDS -J~P=+W4k>{F&t#0jU2J|aA2Azh$_htd!BKaoy3KT+939CS~`sB?y8$4lZE=-> -b>$E$;;JV0&PRtr5)7{3HqC`-u#n;aZ0%XOL+Ns}+T?MD&dBd|vE4E$}jL}C1=zkmK)a-23AI=~Chdq -QT5DU3FX;G)nCtgwq<%k{*|hUv%t@eVOhOuSm}f))4NyHiCa+^MsNx;;reRr;dMz#8BP%P@u}!JwGnW -c~v7F9DD31s;taZ0gm4F)0qRx`q!2PaZ#i7Nd|b;vG8?PSIwjW2dCicubU5TPrljd?1BqG3&GdBH1@R -vgg55d;0A6kGq?rrLd~-E1I+eQBM1>b#d8T4Zw{xPtLa+jo;6!w5V6Q+Ba%#(3Nl)@fAN5mKqMzT|_T -gO$=aDZif7Mm)FwiibsC180Tr5Key0b!}T?%DGc}sdSE6IF^CDgqwi*py=&ZH;=b9ym8dj(3f>I`QM^ -#wC}E;rny9iKfSg&kD0e_6v|w+S8+2ODVmelquyiX57F-KDTqJK3guqpLqtX4{(h))nie?!~fc|6o&Z -bFbHmiYbd1I@DqzVr1?}9=5)E@BPNojd-Khz$>gX93EVTj5qKjRSv+cNe9#0^azg)z*H7?H<4a>QhI> -=r>3?s=d9I#jDP!^m%=b5KZl>Qk#Z9d3j7g(vc5F-PW_b7XWlnoEQO!+*3BLV!1E-f9s4H68KK`LH~_ -$o1SFG_qGk3JhIwFe3myn}%r-`vQf7@De5^My;p7&^0tc8E5ImW}Sh7vgrPe9Rt|LcEk;tN3zO8 -iGV$N54Mcj#>z$09n7MqOm?5YE2z9CzsJ3+EnVLSVXRDw%S80yHMA&f4j7)Ah#j@2f%==FhNDz40SPo -{Z#RQU6je2drX#3Zw1zRLhyiyJ{&>JDv42Y6F=w*)XCe^x3_X{K?*brWN1&*P -Svj%8l(0O{F8N%DBkzuysAqt8H(-@xWGNi|ma>aLnAI5yKs%-z3vgb0u@pak{}q;vcsM;~wBrb%_#ik -{2t*|xh~pmaoL(4`Bpz6DT_f9-&Jcknc&6l02^USq4@SDlQITT|rWg9~z=;v02#>voN0?+_PxQi-eez -?V3FVuqb#{5xkbNX+SZF06`?nO3N88KH(v5>rSZX+pGS|o#;WK+cT2r+(CA*Q7RDo_L_gtWO+U)Kiow -b5!EYly)$K%~(td(6G%GfNye)8EH&)d*)01LF{&$NXJoe-hJ0^1U1kkFkx7JMgkm?&nIRDe -3&w$m!1D`7S0v(LN_c-=`?+G3W1dqEU%ftgcsTy#~CX^M -?D;Av9YCd7i#C-J3C1S;2zWkl#iD_aFT!L^?e7r)Z81aNn?AbOdXG|{Fg7Qzy^MR+=yhKrK-j(uoTo&Icdv&?u8t1VZ$5=g1wu11BGKrbU}0roMLrus#N(ZcMmmwh`{ZGHc}Y$c_-t4D -~|~=lWV=vRnT4_4K50CngX6YlSDpFb_cZZCNb^qd~^+&|AYtp7SoH&@Y>pKX{OR -NGYCZ!2=yfNPY4;LVWs64uf5Y_8H0aPJ8~*D3yR&oD6>=)inwbL1Da`SS%rQ -h(%%qMt=nbRrd*#oznV3<-p`Q77ZU$IioYw=c!ClEN_2d;Dnt3gA6*+)y7dQDS2K>lZIxm|}w{wFKYP5>cl!X|s-z-+=Xn*;DxM -MOM&;f)_Tp`@K2kRv*86{P@!g9{pX#Ie2su`MHr=s4AQsF-vz2*wd;B*a!6QubqvMfQOyUFaxlj&G5+ -GQhOkB8H3p3Amc}r!uYQUSNurHrt~bW(lveJV8Ot-?*o)`)L*J!@H2-d@RKzc{4T-SsJ!^(r1@-?6537il$l`sBOk#8Z5JM=m4kdcV!H$4|Z+d>KW{1TwawQz=dN!OM1L -Cs_18#N!(be%ky?NoEg8$l2;){aZ}Ffbn1s)s_$4urx6Z$o3Tsh=G)VsVPKVo6KP>jUfNbhX(Q+Gz78 -`48Df>X4GlPlTx&OvECq)h`niZOvU1mnGX+C5 -s1runArY0TVhJc@J-gd88LJEj1P_975SEtg64|D3!>jg+z70r;k_lUnY>E~?Su2s>lFLV`TYMFcm8p$* -B$$!$zx}zKNY@K0^o88!P*3ewpvQ@Go@%$)cOtKJ{-d?b#0#{Nd{Z+e$H^TWmeHf$B8AzC9_Z!TK4qp5Fm?uCCJBZZ|J -DKsB%CfaWeU*3^7T*=<>CG1*+yG&E=Den6EB1A~^%F7Tj(m=ct-dlPIkCIdkqxYan0GjUtHlOini21p -#^8*kM-0htuoG3sSmf;_0wjguXLS4}BMt~%D97WVAO=-lS+-PcXFc%l(7Lx8JW-GM# -}iu{b2=2G5#t-vFz%7Gn+-~)@QfNpn|dRMFVQ%u?Q8lLHb9TSzQ-@&>!0efu&?n;_*&e@X>W;WV)HD0 -=QF7G?YXleS{-`=I|ksFW8Y_px+7hp&T?0UA#Bt^xN#cte6(uxxu!kk&513FHJ+LWy;Erx9H;wIpQ%m -9vc1UQ$Tv}rKN>RdH?su(#vITs)F8}olc1WTlNP~gMnT0!4>q{3WPtD&N#l@7x#ADvDaz8jU#)c&h>f -44u9BnfVo8e2LNj$B4vP4THY^2-$XGA2a$)RGOi6-P2&~(nV8pNK%tW53VM#`LnvVj9z5 -;KKPIp}#Ns@eDdcBTMZVcuWpHscQ6ZfXQhZW1Nu9p9%(dK_VoOnIz?YN{r^-`5>nR^ -T7vqKn%ve7rHqJ+lA>Bm*D>B0(Ug5m=j+)m~6_z)4bIJDVU36K -Coh=Xq_-tEy1SF5L9}HUOB~~La=z0cXSADJm2O}Vk|mdFvTqAiK3Ha4k-peEJ_|8U<{&)I~Mvi5k?bM -4C(jx2r=T{y)j1IJ`|ib5nPW?R0L`(5&u=2o9lS6rU<_18=aRMibVB=BaI4xRRv6TXPmJcTnwi3&_H9 -yl|79&On52E5$wW-^9fGlv}8_6Ha<+BzjB|S(y?fb$r3r={h*ksRqE|-_B4sp~#Olel|I#<2xtPnW*!QfW1a@3LmXdqDyQw>2YO}j{h|u=!x -1I9y3bxD{IUSTu;VOZ4`5n8RjlaoL`17}amdQ=jJCQ01zP88@au>~fF0%u9)`0Z9E6UX?8tfOry_bE~ -A>^3MkdC7(RW;1_l;RQN)?uHeWBnBrc=!JNy!1NCqI2uUJ|D8mjq_5o -+*pzi3ioA%AjiKC27fKKNpQ6kE^?<_t5dV8Qua}D`q0VO!3gdcqP%m$_yIAW!e){kacKY8${11J!?;| -+6UB8m#hv!E>{)6cMi-?W{c^KK3I@e?>KU9yuI(~h8e*FJLllNkmF>r@oCeI=`pzp=1Ak|)19UTJp)C -oP*NynI&^}m^stFkcQ$sp!@aiySStGInWMlJKEXq1SNw@MZ>eg4OA*rr-uPHFUJKemz#W_l4 -b*2$oA0N-w04WPM|XhpXRN4LdMxX=6ogIpZS*9aSck$DU*x)6!3?zqo^v3Z=*i89cI!(1zq85^9%`fK -`w=6&K{D=%q95!y -R35UyII#r_Sj^>X(RB!qOoF9IRSt`q>*_EI1>^l(g+)nBCOf7n!w|IWm<`$* --e4;Fd(dqGd^f-yHw8QpiUAPyCQ8r+zHX;tiIJq7`e=*KlOo)i>{5{xC==|BjA0xb9=khR;#(-IJ_Q> -FVD0^Z9|_4mwgL1fkAve2t-P*5iTT-8yIS!D+4KuR6_smc#0`}OEg&5OeDu;4cnH|DVyVNo{SVEhA0x -ZxM0OC@^Sc~?(qh**7=ysED{dcX;u$S3-y$+7WK-kA@(_ -SA(szJgYbj6YH3bd1VuPj3}Z+b8PC0eBBH`N?RzAed& -P$ubNvVY(xNN!s2Q*gs?-j;pE5PZ1_7mnj1fB8-IoF4`JI^y13 -@&8rxvH(eZZ@n0;xZn}jHTx4NyJ_tw?-ROOZHP@AtE5{r|2`@qkhv7rJ61@DStQxLXU+Q|HS>=>iErO -X*+!rrhzVwsH{Iv#4{Ky?XIUmbK81$BGYx=n-`^!t$9W0qkp07vhd-}j2N1nau{q~X%HAHJTFZ}f-V> -4TNpj`(qf}$1JyI7X5yLBt{7!U5T4ts)|96tz^@S|L#$v?LS-|fGqdc5)n*)_&OYZ%q_fG7~o^M? -JyG{{hG1vnI!ca$?r|orf}UB{L{bgQuw&AgeD-B@%vmE&z1@GOA=TI5Dc@BmXnEkk~~fx_g@=?H8B@j -7hVTdXIO?JzbnC=X}e`LM`7q!EBun29luhMsJHr-?o%*#ZQ*sHW+{KQLel(Hs;S?1S)6!F2)nPTt1jc -|b54`_0TTR;3$<$Lr;sqgLJ(o(u>cN`E3|j_7W^krO9KQH000080Q-4XQ$5SydW{AE09+6N03-ka0B~ -t=FJE?LZe(wAFLGsZb!BsOb1!prVRUtKUt@1%WpgfYd3{&Ga@#f#z4I$D$#^Iwwqnb&<9H^ywCS|dPT -Og6i!%^JLK0#UU;xmz+UZyH&`ZCrU(&@cNI-H@9c*IvVX@dNaGPTm1Yu5Yq$yJaPg?;kmN -%4Lw*>VY5>6S}oR-a_$SuNK}OUDFjMc2VRwzj4P8*OVe)1n34+F17e`}e;x=Y{W`bcHv}Z>6$K_pP+5 -5)DJU$4pd}bWW`r$)o~|Wde+QZ`G>zV9TJOR+U#q3%e_nm#v!>#oGtbc8$EgE(!*}H*elB)po{uX65H -zI`2mLAeiBrA4;$QKrqIwtd)grl^+p&e){q&lV)Cu&Un-=&aS>{`o_G7w&tg&Me)wGl6K0r;!d)*Z@d -DpDNwL`U9*+(Th@1-v?n-j%sqTo!bx@~t%Get6xIdACWcp7$wU)i^^AQ70g@Do;~`r!bGuxo(Jw!S5t -uHLUhE*HU<91AR=Uo>!jz;ubLe{5$*c$kQDCZP&fC87Jxm)S{*y7cefKRSsVF#Y_ci;267!V(prIgIf -eCD8C_Fnoy=IfLGTJNM%kp@79OY+0?(UVZwoqKWv&NgsG%kb80|XRUIC_(5&e7;rR=1r|+9%B%=k!)_ -Zwzltn8nJNJiaH7Q|9EMxedS&ukarN#z{h7NxCtn4&OJS0O0?>|NNCK;hrihg_rf{)13AZ>c88U<1Vz -(T=afxtb$aiJINJ>r77dQ--P -SSA;qbPZVF&?*^89+V#LOlRiLu~=IL;SpqMFPU$ONco%2q39>AdZy;D#SIJ((HV8I^^Y>M!_2Ft+mEd -ViuH`m8f|lP5wJf>@U-3)onPWEuUct(N`Cwtfv+ORt{Y -V)0$7}3{5M8tZk}Pt8>u&bn>jYBBj-x6Rx9*=EmSl3p2XwgXXm+>VH5^4o{ -MAH!e(z;Qe@EH5P+gMS{z1okmguHrGZ2L#$3*|P)S`VcIq_!KPgpFqqj@WYy>J~bTv>XVzsa;-ltb8O -JW-f-NN>?T)V?WLiuc*R2P%yCfmWs(I_FY^M`%&=uD!_DY?j48D3VDm&*5Z{1+wFmHv@?+89V8614ph -e*xdAYAPY3P=1N(wu@++bSj<*!DT>%l8$VY(N)Ock50y@OWHv%OQvrQtXM6Bg|xt{l}^!&~n=9(|WBc47T&oj5BV#AeLUi_55DdK)#J?6%gh_C1p(>jJk1oV({Rq -eDL95X5k|y*V5k`d;5PaCx;x4_uI~&MKN6J(@lR89wV%8kC<*uhF6K7!2BD2AtG -{H-lybpW#z(zztYL1rWiLGW_6*8s3JJ_;^IfF`A$5;nbQzbVv{H6}Yp8yVQ0vDXAP^l+)?H<*!+v;md -3?9ah0fVUx9~5SZDa=hRXY8oHW=Aeeno#7>a9 -n%Sm=!#qIR^(gM^XHSl_$gJbGW1{{T=+0|XQR000O8`*~JV2#vMhd;|ahy$b*UA^-pYaA|NaUv_0~WN&gWa%FLKWpi|MFLQKqbz^jO -a%FQaaCwzh+iu%N5Pj!Y3~U6X42nhH3ZpJyI4K$+NsGck9vnklkt1p2C70e^S`~`++dH$%t8BTg9^#O -D=62@HEYD$iv4ldGvff@o>o&_D_)Qd@ot;GnA6vA}X4aXuaIHZb{r&tGMQ?=@FoMACgo8%!(ZKJ$5AQ -MB7+p~~rLb^P*A`^eXyf2lQ=-B0tt?yz$_iaILqX`J9O01XC0XS8QppDdz5Yh|Wsx^{Uo{FVtepj+6jbPTx}^WtCFll?2zBU<2j}@bX -r6{?Sl&Sc22AM&_LY*$i%X -I5q(cl^M2!=5fr~s(ySg}h!p{LyO&`p7I64_X-oai{35Bk -mkaeJcB%iG__=W0zLX!ds#vG_5@2fMh-iae6@qx6Vm`MjOF9TD)P+7v&i;7rGF?aEGQDEcZdqT(5FC` -1#&F{WhUB`2%xVd@v+bu(1!UCMbt!Z(vQPR4E3>S=_1nO}f__H?DOu_nMB6lcg2<%Gr -D-_(c?b%kB!k$r;pDTnDTPCp09Wz_60H)R#I4+pmnBAT@Yf_UH$J1!U8q;x!UR -QUsDfTbHHFp^(N%(XUcbX*49goS7CsC1xu1?ZBhtJeNKefyVE5G!!yCEKw)(>q%46R!i`s9&(kCdkAa` -0D5%s)E>F%n(e$Gt?Y%#6_3c0GV-uk3;7)iOWo?qsfuBzfcl?n!@}}?tlL9#Oe-i2RJHwG=v=ypR^lp -Y%C`eaN}3e?9xz&V;cE}4h@|4YvI>;IizuP7>M8q&4CvLe>X|f1!sQvx<4V9mrDIt1sZHl%bd1yGM3T -jxEQ{aKKPB{12Js;EaF$RnuR%ZTc!UFdTq1vb>TPb@jmtU$dx}I4_ey)$(TUup^I$r`QNLIf9!Jn8o# -k$T8MdaUX3 -U1)PgZ^B^q9ccU3Nx`=pRr^0|XQR000O8`*~JVR?BT%7bgG!qIv)T9RL6TaA|NaUv_0~WN&gWa%FLKW -pi|MFLiWjY;!JfdDT66f7`~f|Lariqv`^5DCn@`x{T@6k>y0KW4k(%*UByo5laac2rvMsBYEj(e{=0( -agcKQuHVzIjY(j4c6N4luALojs;!4xYMRGMb}?4VYPR(hpKNRl27`@4|FqO)RR+r{Nz09YHvTp?PA-# -D&GUGfYPBfx>m=5t!lp@er)JA+S|xc_s_HVTR8(lSEOo4IlIk+a)RBPRuy3QZ%(2OAnW|Y{D1BS$B8y -T$E-RPw1p*5Qt&L>9$cqZG*3+{7mQ~OvP0r1eBsZUO+p5s!Q<+b%bY;I)#d2DIyR#iCJzva{)ONdxiV -SFF-~XKO>sgV{0f#izGHrSPwup71Tpdh~i^K7U7q9jRBqr{ -BY#`H@+8q)xL!-y~Taou~SnJia?9io6(ZY`i%*Ioc0j9lky|RukoWL8hx>>W!2a@+WBX^5E5r)7K~A{ -)_ON(>E_)AB68-oO}n(iT#2bgf)^!_OsrdHnd%uN%O#2e8$F4@oHCc*L{F -u~iQ1pi5m`qpZ=_Nxm#qtkYzk06(g8eHp>CBCzAV*xx^RcM=}F*?;@;@Xfa{@?fL}!KXaQm{5fd$^~$ -?Ukpa?%ZKpR-FbdAMf$?JvTU536iYok8}Q>T^LX%KxB-*gNM&WX3{Yz{V-={L8$ohCCAf!K3;M{sDT-(ap#7O2 -k?Myl~NYtJ6}iY#(w5+{3M|%$Vws+7U-JffO1zJX5bg-s_DuD3hGg)v>NBl((>eJW%jbDBU4i_>PcMIbd-QUoGGGPRfIpM)tr}n!4v+~>)=cNGOI_Uk5T#3faO<~;(4E@ -)KhMF{8=mca=$*@3V8Q}<6E<6v&T|ya=}mM8-;{{+4(hg#U6%h=#0~0Z7(%|yVsDS{V=wa%_&>|52GL -=q>?1SE=P>Ffi3y<0^iof+)HP06kvb!}TwQ8qwxt5L0=9u3sKsTImcGyfhLGgDsR(fR;5q(!%lvqC7E8}E4t`X>LSh0Rf(o-HvJ3~zGg52r3D5)7j0UBj-n;-5zdGxF~Wz3!!AMkh|y)?whj^1!HfS ->WP5_0eN+P+(Za#{6JM5>M(QyP9S8OP*hls$c?=+p2=9xiLW3Z#H%Y@_LiM@O(`1(DxE2-2QV!1rE^_ -YH?l;5yVefWcWz8`mY7x^>etrF$3K;AFBrh6tAsEDlFZ@_Au`~t2nki#QTmGSrpn@XUUdu%sRXQMVM` -n2$l~Gj{ehg+2*gSJkOf`Zr@LC+uV>G%Pd}G0?TrR-8O?Bqy(yz)ZFyrCLMRf_?-@rDf=TlaQ;UY()6 -AY7y+Ua1CqcMn`!_dW}1(S#4&S0xGyTi*hUGo4wuIPylSC)J0EFNw`aQLI|tSzgLBo9u(++45&1Xe{6 -;dGmDaGBRIMH}fdn3Yvj`8?(jEc&oaesIeqIwIUCFs`s38^2F$V?%p~ZtT%Zg+Pl&C+S=T{Z$av>g)aC)U=*kx3^al9B_j`r$z_|3dRP|(x@&a -zpsx6@uX$!=5&<{!i4?rL!69SM4ATfD -XE6w{1WN_IkxQLf_Gm#v`xgA`C{6Fsv_*{r@|UIzM#8{UOx-@GHMUsQm1b)Py?UANe9vbFp5286z?NzoaJQ)OozE@_yh$=ssTI*gwAT4zd -XQ}KJ2EwVQ+(u-Vt71H!jA=aBv~8vIK@_vT0XzUqnTf5Caw{DG==~Vz~VL6Nrj(-5fg03Cz0Foz!P;t -`+mIRsH&v?(Zf>$2_`4iZ|M9b=c7uOklrpTq%r})uq2IQZRC(q|~8CyvqsMuU#VwcbXNL)p7xRgaAmp -MCu~B21x+G*yf}GYJ-Z$qpeSBTP(o^kz67H&K4wWi%8Urd;2{`{(vAzj^^ -ikI;S?(kx~u@SJFzaAjj#)Uz|O8Tv9(Z}6XY80w_u-)N1#h-$O1;l|OREeu;$Pv5qLwoFO{9O&BxY<$ -?>1b<~ZKoK3 -{@Cv$zA5p9Ohwv7MCVQt$2TK6$Ku-vi2Yy8t6$d5!_?7w5QT4}|zSG%!3}THV`Ce2`lf;RgkYhXn`UEmrj<1-06uw$*GqL!C{`9Y3ugW -k$i3UP5fsF{Fu;r!7RJeQJP7)s;4O`%y3)tA(Ja@<|Mt;ZdcWaxc8-{0QfL6UH -3k8ZN8RRT5bCs8rDT`CZf|L^ae;LC=&4tbTP`34?%GIky14-14-W&%p2{2Dg9}QQH(3#Ri-;>jb#Rwa -|vj|l1-BeO~mqwPaa4a@xW!r9CI~QPgDlq^2)=;TrAW2<@e8B|3X;Ih)RS@GsXWH0|H!~0u1}`=LDRDm^M!7)An&}29CjIfIm -uGvf8j6yUuvd!iGOk1#Pn#=p1z8)Lf$=LwZGc)=TiBVtNUL<85VPwrr8j(DM^~fO)3*;?B^ziV7g<2w -7K~;52cfOcC42DN#(g0ZVd#P<#NhK3&b>%#}HfoNEf_T3il -OFpR4hCHt3nLYPxdDnt?6aN;ki!5NRFVWxG7${{1t@Uya;_!TSJt3JE8%Cuvvxhi{dNJ#=aFer8zU@> -iAdBEv`A|EGJED#o2K&GA*&r=0sN8Nb_Os#YzK>ox9#!C;~94~IDV*oY{Wov34){y9%RqMD1cbA1ml0>;GkJzX*Bxt)0 -7COmb+Sw#ALmgPUBKo?dFo;8l7(3&U8rU*aLKMY1B -OnXOy=Xw9vE~Nkxtpg&HZ{B167Mu}v`Q1ByVMvbiKZaNBizVN7mhm@wik;N(M%6i9$z#NuTF>O?qvx3@(Ri}D`yexccnc{aa(?v$CL1tv3%8yRTA2}WA->2=5RlNGB&15I~{dX1D%c~_Eyb)M%b -Wl;@X9YS_T> -^QTqSLuf1cNjKF#!@-n%~w{LWD^|M}HDBtGQIu7wLpv}=quWWA*a>Q -wsxjH%D#k4*Yf8_IuVXODP?mYY$4$>Y&hRH*qf -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 -WCUW7iw{$DWK@*=6Q6cHU|P? -l~V|CAg_TrL*eNcsaaUunId%w=Qhn+m4OLT(}vsu;mW^f>Jt3xL2hZ-5UZa|#14EIB7CjppZZq^`zom -H`EY7p7Ye1HuZk9!>0nZgjGkHl(tG856X)(1N)HfwgV8vB_5g)AhB$y3t^$B6e&BQ?`oOw-zMk^6@Op -qcAEkie$c;P2wWaD!h@~*;`8Zo_Q*!6CH)b{bdUhNq&t3yjRb6(%OS;;=D>J^CNXJDWBYQkqaMOk7|2 -+8wV;oRyGUG)|6UKhOzmZi(u<8lJ)MklM5`SLDSDLm2phjKDn0-j3rr{S==aig%{6*KGHVJeNmwUjF8 -X=M|sy7K~%Zd8ydAV0-cQYgz~cXgoX`G**1V$Yto9C&@nCXOR{hg=!%r2ZLOGYD@x%cy$((bn~}N)N_ -)}U@2H$zo9!kO3wBk-qeFKqEa;fZ6hFa3yA&@&fKLS?jPIgL{V3!CvbXGuZgmqLfS5=E8URRmhfaNad --P4RE8B63;&5ZFj!Ap!Z7n0Ov2m9YEnr_vls9(D<~V&@F|0n0?PZu2b|5t45(nhU8%n^`IJ6q9TD%jM -kDTVp+ZppP`Qp8U6GakiRWZC^yaFJ)%!`EtV5|n?a%DJDaPN{)0}=(UsaA{6QA02{?oGG3SMJYohC3b -EV)+MKlTGdlQyk3z=@_}9j3>?Vq-{~qHg0_zS+`E>MU?2Cf+7~aV{OBF^W%=uTT`fMqdQ3mM6Z4eWI| -;!`pgiMET7LB7K1=(*&dw~`Q)?gpGnIQa%LfEFuituT}IV6o@uG^=L|%#(iqahwct~_S1A==XHvA48l -ybr1AWJ*E=ZvQZ_v@~mx*ip32oR+k -h1~TVvq$e@7I_NPN`U2RJ6BjFGh3JcngrKXc3>sI2j~!p -vX499gL(C^}-`#w-bKp?}%xi{h@UNq}SuvR0GiYAta5nBq-4jUbfI%;!w&G|h@%KZZ9k2`hSElVfdrpKI{@Sc#*h=M -e5=%eh1h$*HSzrG8&1bQ#3VmialnmLobRX6)Y=a~=nE}7FFObvvZfvSIOU!G))A6s|;!lN^JA)wNFhe -I(Is%K0uJ@a=VIijBh0AKT5oQPlFO1yo?08bU&sM@iv(To3KxaOYckA~tfIUo4BuEd{9zS~IJbKiqYi -7pwU4KVZYqI0Cur3Nk7RFSv1&dczzayFfYDKjHQ*G5mW?7nG}*>5*DI5vF -8{^Hw@WeFH(MpW4S53fBDJG}%=+f-_aj!4q;uFtF8Th|MB&uW#*aT(15hqzVH!GT@In9Z^yJl}z3}zH -NOc~5bI`Q|DV6cjo~Ia3Xl$5f-cb_QQ>RXf*(>~~LQ{quCB_ -7q-wtkAJ$(JkV`?>ws&MILoBK$QIejaP~ -X|$~H%tr6CP6lvtLFUhJR9HD+i5mgEBE4+t` -Iai^WPi6^d9?`N6e)%470VHr5dKIxtU6Xxm}@!-fD8cC{v@17wyFJ3l_HMn=$dh0QfE!?Q6i;`^B8ur -A@R(F7P>$17|0kWu)A9QLvRm6&J$|m1UsvgDqkri}lNc5` -q4z=6Iw2oXZE}g(j?y4bA$0ki`5Aq~;bSKdm>^cffw+KwK=7HL1ZY|Ru8iYUdu9})mQ3w{m -gvJs*Z-jUvnmfy=A;bN3X9?6e@T^y8k`utRETr>C-S(@YcO!B-@gfEJ@NR6VDtvJE@2F=}$$b|Gu9CexMHf{knGQVB -YDqLOncak#Ng{(UOi3ax9D_Xx9yDmUthr17E_jp5!qmRsw9rlWh-rsTc2QM;HEPYBpy%F<|CvaO1bMn -tDt>3=QtqkBvGT4O~^!>oaTD(sA7Z@o3fv+tdy}@6aWAK2mt$eR#Tgz-!<0&000>R001HY00 -3}la4%nWWo~3|axZdab8l>RWo&6;FJE72ZfSI1UoLQYZIC|?f-n%p_kN0oNfSaGT^#&3xEbPROjF=Yn -v@>iRX)E}5U|*-@4dU+`zfWZRZ4E;RmkuXrCK01=#)y*PTCgiNtgai*qRC`)^lLA?WpfGLk#bR$D%@j3M`r>@0+DqpYLHu;T&VsYdXJ3XCLEX4~@O9K -QH000080Q-4XQy*o!4>Sh=0Pq$703!eZ0B~t=FJE?LZe(wAFLGsbZ)|pDY-wUIaB^>UX=G(`b1ras)m -Y1p+cp&4=PQWW#4~XG0aFA6qfOdbG$@*&Sz44t*{mf|C8=@z_dS=C_>k=)lU)Y`!5W&v?3;w%SqSX+O)9LObQsZr+65Uk=hR{EtS -|Wc#dtDNStl8+JMfSPt_~Dt&8$G;>g6TeAay5#*!P%nYLPst2yEVV%%>QEyV~!*|W45$uz)&)v{E$)f -(CwbC&>dtrBj7LCxNuf!@Urf7v<(c$>ag?qG8^(HLAK#+iNurOf`(_{pj+JZHeKJ$T07c;6Ji$R(#w{N?}uZWBKpSQz`P -NDD;+8qNb%yJNrkyet4=P_&!HlG7wzu8D4`ca+0i5-`BMl><(N_nda3{J_X!95aKA=VtiA510*yNZ_w -Pv*}(GtiIj3wiz4oEz1C0w|Xa?*teFV)zrtX+Jjk!aXzu0jI9`$czU69-8yichK+kWGUL@+y)CCzP`3 -+S}k+IaKf}32-@zNlP_4)6_-M8t3Egr_b4M*GaN!k*Y?q^UyT!vjQcf;vRBC)u|2*nt4|DJ%-ihN1}49 -25zc#>OouN7>qUC=}8{<=2dTMJ00YN44pYscBe;DV>bu<+Q%!1o*j<8?kYVms%p>!QdlJwGRxz2jI(y -_aAv{(0%tm%+^3#>fJP}CqkP}$rMIi8>((Vnau^5v#XDg#bD;O$>lL-E>TaokWeD-;NAZ#juImR%OK< -0{G9xyV9b&-o07nEqX~iee#VkXTTI=YlekudEz$2QgkX`^g=4<|E>_`HX2ycTyoP@DdBzfFF131VrfC -?t-nMOXAr~7TDAW&>UGa_P3Gr@XY)h)WJbO)5VfC4`;(U{)FPs#Xtm7@S=OG=%AK}nnKYw0>;G>y+2) -M2e=!Hd}ktht3cUbXG_3c!sEUD|csJs-)F=uyl#vVv`4KWBO_`)3t-?>HO{$rq?Ff45f)~V1P~NB(2P+hGMh&(OSKa(@ -P2y$cJl4H9OVQTm2F2PEmI0otnJ(X4=7f^Ae37rSc|^IK5R;T0J|$K-euD(@?__)A;wtDu-@I?0ZXv?2w=%(UIB6Q*L=ae5ew$8QQb7HE*lJ>|L|};alKz~sxSN~E;5!7r!^8IfdiwB<1jtM -EK4B(99f4Y&>ar>4kl%v#}wyVE7qQOrm&hcD7XHQN`!i14Rj!Mhi)8TVW){VUCAuqah|!cVLYCLFoct -h)UTGDz105+hQ>f|2H^VhL-g-f&zlZIDRP^l@d@6aWAK2mt$eR#VFXU)|~f002}4001KZ003}la4%nWWo~3|axZdab8l>RWo&6;FL -GsYZ*p{Ha&s^TXk;cW8M1%EPHLjJK2g2gZd8@H#{2TBLQO7!+5p#otw -ptQ6U6A#BWQEWR&Ofp~CTG7-92y#XA$#_SsQjh)12#EKg--GX+9?)AdPRsAVDF~|r4H4wqQfrb}&7sf -8Wx$b%^eVxBYT%cyO7=Ayar$m9+YC>)ZmiAJjHLcgwwF$fDeEgftYr!HH<^ZMwW_#;qHZ!dt! -$<0GuzcViltDh<0>pC@aY47(iNPbU^ZM-_6`;eDPs60E9#U03!eZ0B~t=FJE?LZe(wAFLGsbZ)|pDY-wUIa%FRGY<6XGb1raswLI -I7+sKvg`idI)!IB|!Gn;*CK*4&)lc15!?!YmS%rqJjC6-#1rbsS}Y*}mg-}jtT_Y2kCvlBiTYO?Cosd -K+nS)97IAK1RFYrY>u+sK(vuiB3H<iop>|cHix!9m53Kx`$!f|dE(VDb~Rtj -7(Da*toe%F&3pF={+@SbkH79`OIU1Qmr^pbg&)7{d57~B_Sf=8!Jp4Ruw5xRQ!`)%R@OD+W}IYwDI~A -h?!tznknGe}W6g^hP5;pi0}_fSZ=8ZBkL_4j1aimOv23au#)u|>#Xd9_0=FA?yBf&HI-ENkIqUTf3?K -k9^t=SZvr@7WW;_&TBi^H#C5C$NyUj2rB{WdR@Xe{g$q6tVv@sCzCi=KD=qF)kwdnA8|O -hIz$$~o^BghR0=u3kRO1Tq5@95imqnTH49@QnsUV(An4T`fsdi -xnxZ{FbUc!50^;}g=nt+x!n7~OjxwLe}KVpCg2k+z$MI7CaFEJu9Z^kYOR1Hy@sD-4T1-sOVH5$WBFk -0z-QFvV_R3KwLRyo9EM)(#(~QlHR2uH3TMBzOrZ8U%ZEU>d0V%IwL~uqy`vw?{gK`%5Z<;?Bh;#BLWW -k`A+zfh13IHo(SRJ!hGRJ}?FWq6u}}+#{=PkRfYMIXVh~(rH-VP!v1!i@^8fwNzCXA9qgzYr#Q{4ovG -rJSM(%|qzCs^@3^hx-Y3W+H*%EXCwu)<7hPgQ)!PSV?Nv}x@;qJypBO4H`_l*mZN^K+AwqeBWV2pCZ( -L#f1=BIABthDoB=Ob@uJ*Q;2iN*;B5ln;AZf2V|uv@kOxj!ETFn^EkN(=<7K9?7%)?I2@ED9t&zj5mN -+4&>*AsR3&sI#Q74uY -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 -m}Vi8X(gE!8G-3yn9h$@O*5~L(bChPbcsoD -Wlwml=Oz5hetR9^7jh`!?~l-yb+1Ia=gy~D|14nm#D1?xn+X#iw&i5NhDkY4-tv;!SC7A4>kp}$oj|2 -Dj&>TV6I$63GNE?f41_6pI^qCZ7!so1_(*J;&OY{FaQ$)SUFpvC7@r$~ESNHkzX4K9#NDsMI6X@uZ^S -a>K^Vr9=JjZ&SSBZ`ncDb;y6P^AEjTU#CGxU?hXsY8F!=Sgq{RdU{8biD6*DRU&*I-dX>=pROR0~;T| -lkc&A?cZ*rM8}=JQvN#R~vpHbt?s8{p`M-B|SzZ=6+e&geSse2|^UUn5Sy^+TfGHG;^FnSmncyXa2`}rI-g -o4L7X>DY@k&PVKG|$GNfAp4zcN9uG5uY{b?))~C;H`IY=A!7?e=H5)9*nl(Il!72q&g;NUacHdWSM3# -*L9kF@q>#6;n~5FR3gPUsTBrBR)6AZ8NbV_Y=&n-E-hTdNoEnN)H+hkmam)IMtO&a -}o2(QePQ&yWw7X$W6n$&Yw01Mdx$l3*P*V`NA{KwU2h!bKP5uF}pphRIOp;DXl|xm8*2pcqVe}@k3v=?A~m}E~$jLN~b;z*0S4$g?0Uu*f$*=)QNW((p4DJ -yBS#D5#}{1kLwyMso|=NIP`K%Q#>vN#%Vi76;3Tpk*=WP>Sb8}6Wb2y>MVAtWE$QJhH0`4M>a{l8Q2D -D^T}uM6F`!)A{AJjA2F`G144R1m#jZ6!y+|BnEBC*JT=27hlta)^O$NXMWy5mRk_XW4=4B^#1bnm{-S -zQ-M!gKAKLd;_u_~SpzIg|h!d);NSIXBJPLB>1ipjK7 -UGY=P^HgBOU`7|N7gIGGE5ZTj}*a-WAf2fi+s^>PT`<_dTs%XZ_Vg&rKc$?5MTMqNw84e<*kqQFeN@d -aJps8*tNTjyxboQZ{-!?>^EeS)8xdEbwl9Ar1ao -SD0NlOmd3cbBjm2Lg2;|QV<@k4xaD(&#!_drwMrp-l5w#7TGZifIAj#v@djYXgF=@`SsHJBb(Cu;mch -bx73~kzB49t5VvAI*37u)P|JxTA-u}j@nzJXy33xy`mb}MwR$)gH=Kb+ -gcyeHhE`mOF)Q)tlAaqWJ*0QoQa4 -IMrx%3cQUAiStN4hz37@NIrS%n|VBT!j-Mx%Ugk%oEC69z`FE7gPp#FC4vv5c4*;KBQ<<$!SCDt?lA% -Wh$IbS;ySXFE`}X?5hPWTKu8`?6tv$-*sZnk?s*9Ih87qcJh&WqzAg$~GoN+3&V;`@P$tLI#MDMh>ef -#?>7z!8t@#L(Uo?Y*6U{f?vX1(y`1JVfcXOKd34ul{nc126x;n;t?p}>KG9)bzDyCR^axnjQQFygPh2 -^Ehn(^Ui?p;{zpMmp#P)h>@6aWAK2mt$eR#N}~0006200000001cf003}la4%nWWo~3|axZdab8l>RW -o&6;FJo_QaA9;WUtei%X>?y-E^v7R08mQ<1QY-O00;p4c~(;!*Dhyc0001-0000m0001RX>c!Jc4cm4 -Z*nhkWpi(Ac4cg7VlQKFZE#_9FJo_PY-M9~X>V?GUtwZnE^v8^k5A0WiH}#XRftydO)MzL%u83&QBVp -_Ei6sVOHNga<>D$Sss)?-d -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 -k@tv9Of$kypJ?p^t0SPp<<0{r+ZapF{3 -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 -Mc;x~z!F@6aWA -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 --LZLgD@9p@W$CLo31)L;n?XZ8 -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 -b!mmdvNAp) -2o+-a$=&bpsSZTNzeE_gRCiJrk^q{AeubunDQ5I@%mnEdwV(H$8?e65v{SEl{0ofJ-)krqVu?;YdSrH -1kXh#%weGH|!Ivj^&=gahVPeZb ->x^%L|o=DRvvmUU=!v+Xx`Jk@)KU9AmaEidJh0Eb=q@;rM@trA{m<=;R=>LMZyi%p35cs5#1 -goLB|W(_nN5I_Q@s(Dt713JlkfFX15!VFPq=7G --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 -mS2Xxe5U$TV&b~R70XKeAB?dH&X3natbN -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 -3z~X5GF`hUsm?&iA8Y%W+>=jUte+V~4%Ev5O -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 -gW7lnwliR|`o~h%x?hin(ZjLm^ja8|C -P9-5;U$I2hgW21|+nov0vRF5RkKzsh3>vqtQ{nNizoqGk-XvCx-We0=CRWb6$ehqB~cmNmlyAv|J;mZ -yy3&rg0C`O=c=N{R(+@s4#PG6I)EfRAntmd2QJF0a>!?x>NEmpj7BGnPB*9y#_@##>NK7w#>@Jvvk(i -MKInn5)b&P$$irm?t9;lEGwgfbXSdZC*CzR+i@}HIA`IeYUmBHvPaz9hlvET;2Y%`ko`sHpDvasiOB9 -(5j(_@U2(awbiy^-G6Xv_~_1B+%Bdo(d!;KQ<~d8FL%g!SUk)wZ<;bYwZWKM0cC<=_A0sO#t{}UOkUo -hO9uQ6{P8x);(RNZr_{et_2SR{7hhTVGn?L2$7ot^>#a`Kx|aD9I-!wud-bqRr_VYRbSSJZU4U=`X_| -B+;b`QD9CN5Qq&voQ-7{sWoRou29c(*Bi(xYf_*fK)SZg_`zh` -VKbdSXb@*|@PI$#%5ve0Mqf}*BWtz(bmsHS}LZp -)tZOD?30s^Sl!_TU*%OIi#`e1V@<}k8XZ>?D@sE2*fo+vM3(F?#ICBao=)aF?{|WqSt_R9uydFS -!Bq}W+J*iuL@|FVC1VcQKCQ(wQvSc`09lgZ7#cZCJaZlf5O~_x;5`61f -$x3eHPPq%4vwEI|o@M{ -jIhc7s&qz1qG?r)=l#8lz~$3$pms7y5tTx6o24%Rol25tH?ptZYYQgDm~#oy -U9_t&?2!=6|-u4z@1BQ4B`Qh34ob_{id4N1{sZ>u!98LX0xcz+WyYzQKlS3bz#7|xC!J3hVF)jA5jSS ->2Jl)(73}%`V;(b)MzxgC-!-4|%uMfo0toky+mq;<*?T(;Vv|?E$(_{vq$0*4yu4OYXA$&VGl} -A=nmN23 -j){6(k1fc%tRRHVPdZiA`GLAAUNrpdmQ5j$;&qnSAUc`*F(#GZnD*c88UH4T#rO;*<`~cN{e#}Y#1b|2-Jg2}8*6B=d{EFGjr%wUG4@$9gM>;Koe*;iU0|XQR000O8`*~JVo&cQ-MkoLP(~ -(^baA|NaUv_0~WN&gWb#iQMX<{=kV{dM5Wn*+{Z*FjJZ)`4bdF?%GZyU*x-}NizC>SDr)X1_o*^O}Az -_GQvLj2HKa*SZJAV-`gIj7+aGY?Trko)adkA8PkmXl4eID{}Pa;B%cySlnwT~)Ja>UA=mF8Z!-#B`bz ->rLHsNp{oLW#5S@|2{a7*G1D*wfa%k%Vkk5)z7w`--=HE+O_KIHft(q*B&geGj5g`fOV5(ZE{7I&+%u -hU019{-FK$tHD5U3#_7DSDlzYhx>8RJ4-)wNE^Ecr)f<94<||EoE2_(4Bdm+B`}KPFO2gobKU`m5#;% ->;&&9^Qbmh1EgJ_CHA@b-=9N+Y2H*M!Du@>Wlk(`83fLnJGugba=`DI;~){xKFn{MY_`$1&6XfCs+1$ -^7r>$ZEf%BrQvt*aMRE9S85AH<(~(RQDBnfTtDdY<7({*aZuji|TT{Rh!CJL}Rru{9I4u3t8FSI?dC! -KHiq(GM_QxscXXcfH*E3RRnIew!_YMpRqedv{W}l&blL -e>)}ReKSz^S&%G%Y^?zH~pcdZ5TG34xMJcA6x-GB^x2-$DtZe~X(-qO*bi)MbJ^(~xa0Wl@yJ~mK`)Y -4nE&8TZcRx1eIGKuSUgu)kWzACTYdNjzW}TJAUj!{KG7tC4p637}-5w`66ETHb2M6~J?w@3mXu3-I(! -l{RLbxv3VG2*bfv=(&PNzsu)9L8oVES4tvcBw~U48REux3NJcyO5D29{UagLw&fOw4A&n?t@iI-|`n7 -z{L9OSt(0`Da;Lv;h32F57X^_VX3YnI1T!#%HtCWop>NEXk_eZE)qaNDl}|ngaehamHgo#^f|Z6xJJ2 -4M#~{3&eG2@SVUgH=>z~sypVjOL*;)j~^~t1#r76V9Z6E^sQL*upo*d1 -D6)+2as)Y$h`{J&x?YU=hXs-tel^`rcX-keyTPl)v3&r -Mz@w!J$s)C`_%nA0j(K@ZV)6EKyfOcgMV3S+#>%bug+TD-$D~{ejVwW@OxhHwU$IY1?1}+O)V+MQ-ig -mNsKvwAoDwgINh`l19O7V8apnyv;zJ7i -xZsyNU-V`?{tZ+g5!}9sBo*ztaO1J7Gh9_%`%WXAd#@ab&rZ97zYkdNf_M29%SVOdNo8H#k7CIgsgR^r2h(U -`Cb&lFWxYfy?gB36Nb3ZMv&k67SD5WV_IWkv7B1OZCISjs$5w_3ut>WN83Fr`S{Y^-6S&?vZ5ny19JyOwRuyq*096)ve@&=X?u(jhGs2spe9B&Rx -Y)&kGcXTIQ?9t#IT+l!aCSY)KJl9PR!>MW-5l?kClbC~n$A(6nDC^AbrR7bNPb=seAT1gpT#*Ejv%9P -)Q7Dah@lkp)KpdHd)q|`o0iwmUfi`$njVMJ%%c2BUru4kF0DVv_7VroXz9r(o2AlU~*6dv=AZ-ETL-w -h)&TYo*6QH{*Mfj1}ai$0=B&%WY)TR-4$bVt0);VhQu8<)cr?un^U%)4eYF_p*lPwICt+u(A2^!y8c8R&`(IihzlPa~3a}-lqX(UqTnOK3TKlsqlbg -!-ofD?Q%1?FBsIH{`B-IQpognYmF$+$rc7yujL9aBXtf7!22HeP`e6xv&4fZ;>LT?<~?JXW;4zI_u4l -Dl&Y*!ndjKdaR(sXF>KYF2HI&6nnI3NZH7D( -H*3D~;2lRb(@iSRX5o%g8lal`1k(_fxbjrli8CXGacviC^UHh!oETmNzHNywn!{j>@s3>09Ti0)rwr0I1 -uR#!@V`?}`&|BD9!WX$B$gk226LrTavU~PW>G`|sq4AY&`i=~ckv*8}l|U=CK@r*lM`eu^$Pq_ER;sw -^$>o)7`ep+rZJWB&uKM-V7P6D%S=c~Jr{J#KMsP!ID4Wy_V%=FCCg&9y&+oEjDUML}T-FT;k2OsIp8% ->EBd^ZxYOq+)6%4x6qREzP5c?!bQxD`PvVCy(+*b8gnMT0#N%c*#9CZ>UbmhrK&CY@`uHjZc--u0_vE -JwPQ5I;=1`8zTo`Sw7I~WeQhpFsgFiw8_$yz%9(H{qdrom5YU$`{Ar@16g?F`NDI!m6kXHNisAuhZQBQ0D(r!SKi4d`s} -%`*$GwK@n@q12Jd?&!&_wx=VKs=7D#o)V-sFI76kRPlRv*esAO+2P)Q`qZWlYf9>y^nzgni!RWKuIXZ -|$K^Rp?Unu>HcjT3(4Pv@p(XJI&NAGG(ru#66U$d{f%0y@A;WHO<4|V&^RMg_aJblm=g&?bSU9%gzLhK%o -|O~*`8e}saE^TiPRd8_1dIOas}lTYiCB_8d3-~2%c5)vI8;%!J^{&hQNZK+U-3ci%<5}Nsw=VvNK`Z>YIv_DlXB753>oOj$B@W -?pQ$J`xOYGvK+(zM|fJqW=cl9(IROid0Pp%SQr-$8p$mPDYT8vBjW@{4oV2%3J7`$A}%jldE53{veSk -p6+Pq};w4W*F`Rt>mWb$xo6Gjr5Ri`#tmB1IK$U^lUk(WfGy`IzzJXM-zN@Ergayz}N67?Kl5o`zE%y -a$6S<2)P}j82W8HV`2HHj7*e*|bWDDvYVn`|r+LM(Gfg^y{fO;RAyb)k958UeH!`;!U6Bj_CJqnuJ+d -0O^QG)T|krwavK@I4OHgES<&gL7Jz01>+n+$0MUD)Pu!)Kk?+{L@)DQ8cic8|{cra^+_171L#Jdf6#* -+=~u80{2RZYsvCbr6N$2Q!M~x7Z -Xz&3|5=FszQ47N`tQlDpgGQ!iP196cCOffD5zJoROfzftXt|oCuIzMor$`udDEl`363`~YU4F?txtD! -LoN#x?JA7W}2Ted*)h>-6BrX$;H%*zO@BlVeaD7`hGG<%(nb%2LGa>Ks@$pkdh6w8%XZ!&$308?Irl{ -`)|eT<;uyjtZlf9SANk7@&U6l@Q+q~oJHgoS5P0&L_paSYE2unblYKC%I+pSTLFJMAAtxuJOg&zDCWO -w1@89z2n;=Y|@aI$vrN6)F-qPt+N>QR_>f?4j#{dAxT=LiB;@Z2{ZV-w7Yl;FX7Xeak0M-3j@Fj8h-+ -jARIO51Q`0UT?spzA4J0+l@VV`m_e)Y4W6m~YKw$kyxKNGc`Os7b*qTO{Cy170*r*GrS3hYf2E& -=K1I}S3SAydVvKv?fTrPBi;4hdSpY%JYrOAakVMQUf%B;hyLpqG(K$bZnagL#NsJ5UG=~hj>bq+ZRL(aUz9`&^Dk5g8{2V@I9s9I2=DX;g^NdS2XEO-L?hVnKq=Bkx)?}>(v`Z0G~FZ!e@JhaC_iUBkTcUTdSNp`83_VPbZf_g6m8p!(`V1Wer}_GpfNC@qi$Y_5jiySf=^m-N0$PO -2SU+EH+1j;PZ>`ES#XkQ{wsN&ew}{x3t2DX8|&;=C^w{ctdX$hvL*+-%Sa}QPon|USGOXUM-a)=r-*Z -4`3h*{EWpu`MFTu@ZHdVm-5qo=n6U7$wKJ&zTfshcZ=IMvQloa1a)we-907*g`LTqLbZ$HtDuLuH=Vx -LgC?>)IvM)Y@bW%F=Mx6n}bX7mJxX!6Dek|2O&PK8zeYv4S!61<0LAQ$|!pG653MZPvSFlWE1&1}Nu! -@I0pV~!EZcn3hOX>!KP+k{DZ_y$$@<7_6b5xn(me?mPFgSyuM?5d0b5;O+2^NbY=EQy1$Y>xwmfqT|% -1kwGGkE$Y>2$uTK6K>i*a`9Roq%N@`n6h{*OZ5)fJE{Em*5%SL$g)!baw>E_wMU4t4N``KLRA277)ypZ7X_-uCg2_jvtze-mb1g#+a2EI!mYrX+- -D8qi}&X$IVZB8qy~sQb)C)MU!A-us{Y<`yng-u(>q!UAOE${DaYLW^27V<_t$@fr(wbsy -GiPbyYf20)o($=5lu0_?NV5sRezHf^$F;R%aiBNPQN-iJv}-7%}JJHSWgeAJT9tZeR_GE9qS<9G8z-Cx3l@IlVp}yl(;3vE8@8QGo3@WI@JR-NZ5z{n%D)*b` -NBB4B~Wra8jL6E_q4~N&X_5T2q7c$N=vr$B3nq`K0cWMAQ@hSiK!TU#z=3bmv109Qn>@fo1H8BibWZk -jhdzUy!~7rn;p*a#uG-5mCjSriwCLvLrM$_V!|rdE{=z4Q -nLq_VpEAgpyJ~}{jewPd;gAeOkh(O~@FwU5_3*?J<$4#DNzV9Q1h=B|dF-gdqYot|LHRp3TQ87tW?&! -4J4LA16ELe`w(ooeD!;9P#pjm{IZ6~UPa))HTbISHNUnCkHP+|%MK?Mi1JM(gJa>3NiOUI+#>|~EBGs*lFNru|@~ -*8g(+NbN)1pkuO! -NS`2s`1~d6gG#45KOP$61Ea7Zj5ea+-Or~*wWx_ly0=HcL9CBA%Vp*C&!Tmcb*nIE?jzn0G6ctdc#$7 -cM9?M#GNtTS)$_k9w@nB5S!Z~$;|q%`yq18o`5Ytdts^kqs4nv#5+)>$;+Jr4GZwHy -8rh6c`346Q^jxu&u!OpKF&Yw7M&D*Oh;>GxZYO9F3VSyP!gfsYi{Qo2TC%Sn$3c$iXcVS+`d_I17L+YNSTg&;8V8p)<1GkV16J1cAaQvuGWWVPn~E;aj}wk@xc@CIrcB0-|CpGJtt+>8h4OJM?*J>bm)%i+pg -9v)szhBVw}hE%O(2294HryW8^2tZ_%B_xW|)g;xWt^?94_^!G-&33we?1lwYDAP^Zd3?|m^t0xCZuC(8*4 -kye!*VXSNbr6nVHsu+XEfRDm|2aDB@-@>FzXEOh`ep^5ns>pfQa=;|0d!VlC4&V1Z)>Nv(cuYp5zyv_ -=6Rhm(QIV=k}pgXBeUOYP;!++1+dA@k|0sHXYPl?kqIPlSvzgr^uxJ&xqOx)PBo9ud^@+m@0XIoTs3l2 -N0pX+eQPh!iAzS$i<}5e4j@6b{k5z?%Qws@%cMR>$IS3EtDilWWa8i>zbsi#{W-^zsUBy!huZ&j%k5D -;P1@}m+_LPRKvHZOl3NS)S-?BRr -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 -NLez*<8^E!u!9Ra*v|Zu{PlJBOZhGvX{5j| -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 -OreF$N9x25SEvF2&b -T2f}f`dY*RqX{4#VL#%C_cs5z}4c?5^&Oov84#`ODwROw+TrLN60uvv{faCp5tIm=^ON$s^Ow@2x0tx0rDez&aC5a)4jp4c*4~+)#7Wy6d?Um5{>ipN94a$>D=5+`h*}k -PC%{U{6fdBko4ii>#)B(71U{_j>&};k8zdZvBvMBp|(Ty8bvE-7D*nW}uT95n|tE^v9 -ZJpFImMzX)_uh^%eu#~5;O;X(JGoS)_xwH+Mq(PkC;gAccmAJCDrbr!^w5=ZczrXp|Pm;2o-kpKi5_f -lYcD`qJm~B(Khbvc=A5q -6eGYaICZYNM*QJpN)-@uIF;sVmjk`J1|`c=`dLx)ncu`0(z9A9)8O3wn0Ze~7d5^)WY_o!To4Q{8UVA -;Dla5%BZ6-bj4>7g=`dWz*Enf}X#Tx9?Tk9HU3?n)>#5K|p`0uB!T`8hLKI1435?kNxugb#KMoB6N_O -D|x9rI!532MlyJm?dxm!)HP+P*D%r;+c%17*CfL23l>@FgAvWZW1bnZtw0B+q48f=DgJ$0_pWjjZ5c&2%BAM6~ETp_L;6V5OC%>{7L_;iGD1d0xpqNa}ntxwv?p|Mc>2fBpIWpD$o8vvhi;0Fo}%lzA}SC=iIj130kp4`hzd{C -8u@{8}{z2-BOA7rKdO0R>xi+qy@%^+6hQQ#ZvFnLYVJJTFT@%oi|<141^J-5$i>h;}ED3td@=wGu#tY -h9>9v^9*tcNG6U6KNrIYXBNPGYe5SG^48u)s#mdP{d1mbIi0A9yg#MhVl(B(qu20$wm>&zZOBl*Yn|XxHK_S@epjGJ2x*rZ6YO61K!lnd%#N6FnsWz|KwD -XiwgB^q;&q{TzN-+bh=b$i$7R`qHdX4s;z921tj`O)_yF;lW-~`>Iz*7VP7vu+O1p*K9XMv&<>`&gpL -)9E$Y)q423h=)N5oe$&IS0K~0-qOzD^p`c$pMlPTWceNVIbAd5Dwa;O=ns#;4oLPz8hh7WQugPDZ4_^ -YHJ>J1>eA+;B1LjWUni|@AlaQ4f{{k->rUBkqwuER^H+xxv^fxooJ|y-+SWoK^oS!rErc?U+qEWI{k(Cu-<4_ -ugfsRH2FD5v1ot_Ike4+&k^C>jg+^y{qr|>zk;{m5INSmvnJY!$4FZPv6g-GGn_b^$CExzZDjI^w7vH%E9)E3)!qsUS(5qu#Lu?agG7NCp-z!zc*G$jAxWqdTUj7g+s -MiQ8-R}J6Ker+>u;ces%7ti974qb#?7X|zfA`oZ`yM)^B@;`>p(aHArkHkF!TxIyQ*%*+qzO=U3_+$w -G7zJ;(<)JYHw!qm;fU6r-Z>?9$9P1uR2PYQ>2g<8VMMSO#o^F8!(^;OP~60W*rfo|mnD -1i~NjjXbbf0AF%xT$qgtVLe>pT_3YG!~G#!H3ozj(ln593oT4Fs6=P20JG;kCd6J1*&#A5XZcyymklJ -SV@Zbg1(-2%LbNSY@!!I7(=9mE2XssRY=36Y#TFHb1xCwmX250{gSb%278w(d5+RL9c}tdA*O-VTPeX -PD!5o)8nu+)#DvTqWps>U2(Ie+XCluYpwvcVpULMdQS!<$Z@WUgU30lGae1q)lSja4EVUI{XcYw3 -}|&jXK8xi94RNPzI9Wf?&Oro1KW#SF&Tq88UF~Bqb5%hQ#XV_UAyO18@lKvaHu|f}=neO?jNI3JpH8q -o*}Xp$px!N#8dDMgPfm7h|C5d8oc%d7#y@oSg__7S3$X7Xg6zqY!?dOpt`kNo7(59d2Va8^7&H@dkiS -*sDkMo~6Z%Ucd#BY<=AIldJ+RsfiA6)g0oS@37D=96dyEla|Qe(FhRq6-j6{;q?3juRkzP -+I7t87ZodZU0D%~Q!1xHHNV#~c&}JiTs0U9=C9N}LHQ~}23oeK3@z&OTgpGgX2e^xi+{xyEKzT|mL|1 -{GG6H-XAZcNc>|a9ISb1RT_E74LhW*oB|NeK0@XVBK0V>kcnf*>&M!OXsA -=W0(X{rLrQ)&jdSNVbFK^`@?C<&c#6$ehkTyH};pK<3g*ZcLJ^$*f% -euY@+1pIYhZ_p3jUbw~HX| -%O_tiob)Vl$wG|DMdZg)?ag|CEvSGtKF)>!+41!rri)nBmgA-6!2AVj-=W5w671fxl>2o7-s4;xvP}t -(eME#mUr>P<4KsvKp(baI9z{dsL%`&-Vny;s4k&_DLt@`(POEM9kl;RQO2-G%#Un1F|H#GC5r2V3sb0n2uf6++otGa+8lfgq(&RpApat3m~&`skIpf~9|>8=P#Ep;mv;F5IDLANkLxIMnSC9I+W@TK!4 -OQ}!!SKvsq4B^|8%FuLIj&}= -FMM$*YBrc0UlE7n*bsD1wKm8aSlVM^Bt^E)Z+(w_w}C8 -8@PkJbt-hF4opW--o56n8EIB!w#mfKaQNLIi3>TZmp7D((_vQZ3V)YOU$rv+2T*)ei3 -@aczp&F6eQ<#U+fzB(k8l=rP8U`_4liMvW(ADE@sV{f&)gO4ob=Pvr;^(hbbi&768`@(2gsNJq(+U?{ -vA6!=%so4Rv~qEPC!38bn2Af~;6hk6LOHz4e*d7bLia}vHwt~sKcuwvpFL}g<79#{>!$#kOfZoT1Rg> -fcvggTJE|&n#@!0w&*~JC=`HqZmAQ>Q9ccex=z6YTubNB1z}GchFUvCw>^%s=kXNU9@Za7~Y}1ZUrmw_ -_fHtUq5@dD3M~0#Yp=OYnCl%*hw`#(nz|~0$JBljKueYw492c)?1C;RfKz1r#UAqqYKXO6@oN+p?aCC -Pt}bR?Lml-^2HzLLlGUq8PmIdyUGBb1LQK39?TQcbD!)vP&4;o7_&cIEIlxm%hMqLqi)1U;-M@V393U -1glGI-ii?{RwppRYMj^acSx1*vd{YF0+4JaL3hh71Efu3vQD6A`tTZF>4vkB`sj;BXM{|=NPH -*`3ipp0#M7Rj7T-8`$Zk(*XU-gse;!64-vGoX|DI+leF23 -G<}}=8H*2t8yJ=fXmj*jN(cAclsIo|dB&xH?IERFV?=!JdvPkV;pQf@2r$pjX>0y+y9ZHd -FCY-^)`GcGZB^8N;c3G5NbXi=Ofv-*hVsVQ;dq{EdY#3Ft+`1Cq2~mB6%YeMmT@hFgUl*j9gYO$oRS| -mWonw#K*M;65PZm#6aG7Stau-nuWPe^AxsGXrBac#qDPJkrz^LAaOU9U^Hq#G@k4;13UY9Y-pzn<7d4 -D0ri*_L3NS=>=pD~7$xq0s9duaXvn4LXV>~TMBa~vOF`6vgcq5JmH6;xQHTmK23CVv|fdtS+t=>BOaLe~}2-udusIowA6aoWE05Kx9 -LfOxDG65{Yo5}L%@98OE;>431g4n`flwli~#EYNbzJ2-PL$I{e={zo#SW6=B#y!*H1;Bn)I(mw`-_v5 -9TB@+yBKmCaXks#Fy?cPzI027!0kpQvw6;~PsKtlZ7sw -|36UA=84-e|yvE9|xQw#>ll60X}9t&~UJzgrTQ%mmOdpJXEGpoD&_^+kEVjkhtWE@uZ2Nvtw?A_m=rX=-C0Q~d1q#y1G` -hS)^r!KkV*alrDpYjl+0He`2fL{H_p)hhA3_Qu?eh3oE6F0dvNl(^gMSa-nw$OACs&}dL75AHK_OxIE -ZC$x7Uy8Z0vi8FwQmS3&gbw07BYIEIt_?GK}Z3&E(5*w7!qjQYwVB*_7cVceebebh5#ZO4?)Ua#%f1W;jvJe-xJpHHao -9vtTIR5AcTuo>2n&{cr&(ibQOHgnIKfL@fP5e6bgDydqxPj5Vv@)QWHm)orENW%z7IA{d}+kv@8I{e7YvPAuDeeampl57U;x!S1J_9zy=gg%+LL;Hl>dkCYyjVX#TbU( -LvW!1!XD64r4R$>k_6Bn{U -O6fWs9w$*jS&gnO&I`$?Pj@tT$8OjtOm!-FEJUqaKGE2HZr}qaGf}@obn<{J|_SG?NN$usdM1rpzZ4i -6_M}R1Ft+69myl@u=RKi075@H^f>|iOFWoO%mB#GXyN=eJ~#B?m`dU0h+z+SFPdff-6rm3DKQ3#_65T -)SE`XnJqak@WEn%UlVDz;MKH^*uxwd6tW3AqLH%XzvWmCQKWyoVbL%^p~eS2p;@Qn__vwJ9~y+*U`aq -ZezPR7Svq_lgGsbE%u!Vb95n`^luR=c&$*wGKI2F9kgg2OKLZG_T=fegC5=|m@Scde}4b()BD!qpS7;Cy15 -+d-{rNv?%gNp9?$OF!`!_|^Ub}hTQF1*%baR49!2q4ufaHJ8%~nVMXGyf(!-;qwb(yRT8w+k?;49tTW -|c7nOTxS3G>FbQgQ+Mk4p{-kKO*+D4Nck!C@Fc_<~a4yzRSE5aZuF)n+) -H7fFjbRhi>{`jB2k1dEq(=UpijVe2M~z?@385UmW#w;4ZA>?A!Ity1J4r$4$%s$Ceb9IZmQrCE2t!tiYHZ -jKcjH}BtZ1>oxb=r?B<^YL2$!q?jS753~w1#jz8?z@V+=l0DPulr3;-{Y$Y(XCg=cw9yR0t@>p5X= -dxP+ei+tFhCh*HPu{5ckCw5mQRm4;#nifQ_vOwB?eo;hmwO!5aG2Q8zr{r>|{O9KQH000080Q-4XQ~Z -za0;dN60AUvZ03HAU0B~t=FJE?LZe(wAFLiQkY-wUMFLGsZb!BsOE^v9pSW9o?I1;}5R}{1d+Zna`ur -~w2BIwR;0!%u=vuQoTf(e#WoLq^a6r$CR>R -gSDE|Y_{2mG=&_O^dV -IkvlI-IvzA%p2|$-(3Vyic&)Y&L>DsOQs!8PR95?gV<$>lxxm_fA$RFQ$7ekRPsnW?<%G|N?B+i;}rn?AI8R;0+2V;We?LpFyK -YULMHYc0%KiBJ!ZmzFBhV=3J`|I`fGWx~acRD`(vFk6G#Rc1ipBmd#B{OQ_OW=My9^WyF-?gBujoix4 -B0SuGh5`>i^1rL~hnqLVZ$nZI;z0H+XB2_<2L(pB|lr-ha>)}@DAVusJ$$xI@1!k@CeOkp5vRjx1@;B}Xsmf -D{DAl;sG(6ZZ^r#S$Ei?I2q-w8hdWzb}bw7HT+ce5t(zl?7N)S>2{O}heh`;Y2*0=YcJ_oNe9}fvqnu -3Cx&7zcP5;js?euBl8oj$x7H4E+r~z3nc=? -sS#M!i>3ITs$4!!*WIR54zfw)`-1r_l-~h%qMMco3tn6Yh$$O)N)Sv;4`Fru|Cd**%p%2`QPWxV0aup -SBCU4r)`@}n{*m=w9C%A9yi`mP2y!E`jD;_9hwd5dhVO2W6z;Y_UF$Xz>m~EjP-G%ckp%bGZk(+X}N8 -;wKULXCrTt1T_=|9wiX%dh{)JS7k}0VL_hunvU#r-Icgk57_(@~z}U$1+jF8?gm3J*T;Aapu_j@h+64N@o=UNhjy)Ak`q*3ZvO!o?6w)k~vwkpolza&qL9iBl# -Tk5egeqy6!K5xPOr)XJ0PIGhY4XJ91{lUwC+ra0^;d^(|2+iwfYY*rL|V$N}zYukpL=H9rc5v0t?*^;p*y|Y -`;E(28E_xOm0K7KDPzKP_cb63WE1>VXMto6|_Kx?V)fB{mPcJSSG2Vv55QM*@jP?QZrx(%@%Ksu1vTc -4l496X)M`6qHznr(X+eu?Uh6S7>u(%BQ?oNnl@;Q>sNgU0zWREw>IlsY -GEl$+Qslq!_ix>~3y&x98@#1RbR-gYRG7O?2|};oY6N?%ENct^KagY09iCgFN}VCLHAz>5!XnW -#=UvBubr&U3%)EEfVQREpG5pQtEEls*KCM))C#H*gZ?*27pYQXa_b=jk7yO>M_TiZQ-r8zQ3yMp(p-l -)ID?uJQDiyF48{a+6JE+izj({|18pqTLc5%C0%{`*s|hXR?t@H@FKdR_Mz@&7W|xpTSyFqknV2puAMo -TRFlD0W{8HCgjqE5Mf7j3{V#%B$<=B$$Vz^gL*6?pS@T5e`HJQ{Vdmk)F2a&U1j&L)`FCVr>^!8ocfO -mb{nmUr!2_O1TOPQl$HV2lj$ny0E8@j+iVG_o_t^aG*z8o^vhiyzmvbf~mMRmE^8HVRe4La!*X$U7UCi0Ac;0beLH79Z$Q8TVH;~feE8dAv>4gREEy8e -H{WuJEM;!QhX2Um|$exJ)@v)x4s4d<2+!N6=wh$yR1g@548o}t>KBeFBfju?#46B&j`Y!Q5KYV+zTJF -v4o2NLEq-bCuj`m45D6V)YJ|iLM^}g6-+NTLd$~RPBa;Q^ag?xz8o{{g5NQ3`u%QRM&-2q(sEwY`Z52 -P_9ow6Uw^gbz;eL02skfx#ddWD=z3;l?@dgqtx`>WeAJd)UaQ}^?2vJh4&427m#pV}DT0sfsdF}?n|4 -n88YK$Om@j8zsmJB;Tw=ZLdrrM%0x-;z!%)b!tNe{$~L30eD3=YzqUvQ3L{)(1{!rr=i#Zg75ZWY -z{pUusD3T*g1%plN{i)xoYG-@sm9wbyf`_D(mAvR_!9(f(p5{kvM>{{v7<0|XQR000O8`*~JVDK8iZV -;ukhD{cS)9{>OVaA|NaUv_0~WN&gWb#iQMX<{=ka%FRHZ*FsCE^vA6J!^9t$#LKLD<)962%MljlDgbE -v}s$SC?9P}ydrs#Q?d$J00VH%#V))LLhy(D_Vi=kI}1{=9y#hLg}7V?0HN+$C%&* -i+y%A(eAe!RW?=|`C^Wc4PSH>x2JRB$`p>~&hpo11qu!k9W;(8$n4^^y9!0X-y4UYCpAhqC#xX*R&J^ -K!jOn{1ZLF}-~w=jB2&Sb9L9?bpj<4n1<&$ZJ4SKYK{?Y;k_q7WeODv1;zT=S&Sh`U%)j0@V&ggrm6X -J*_u@x&%xKu2f?gnh!>b&P_VMhiNuc*4Y>Jrfsr3sT3gnI;$tk&19aJHGsr#y1}F=CwG9$!2w7bOq7z -hWF?z-@TaVXlSz@TL1sq>2M6;!t!v=cGEW=HOsj_F>@P4&BcAbvT!NfuMb=Cv!&>G`^&CI=HJL24s%| -DANHE%M)ym-YB5hK5GaC$o7cdMw#|4lqBu)pAP{YF^AZAAfywj$s)Zk-*xH~9R5*A<~RI8zZtQuNKb_ -fLs@p1rVOHq^!J+%*=5%9%9*g9Z9w4*`Ep>jxrAH+EI{EUavuR35Y6uAKY?E&xh%9dV7A7p$k-==vjd -)lc+^>PM0fsZT

ZFh-d%m1ym@u|3Lc)shWOgG+sn% -1@5OV`$4>5BBRSAJq%d)5`rlZ}000Ff1<1Y#<&f&s)l&t%cmQE2^hXbn#qGBp+=uKh&Zf><`?1~5#E`JLAnn=fIZ2Ezdg4OmaQ12<79nBMyoWXMSdpS-cWtcIO -E_AaeMNB8vrY>Qt9Xj7GQgyIpt%-i}dMs*Jz)ILT+$IxNOqo~FNGf?ka@7uhIi_< -_Ei*L~YzJ7cD!^z8IJMQIDfM}v}UZeJsV9PH+3)F!1gA_9<3z2TXD**ebDMboHBS&EUr|3tFQO=yLtX -PoROB>ti&IS7Z=bKyDlnPnlLQKUudSylP;zG4Bpzg>)0PQLB1p~~p8WsbBn{y|jfr56z$7O{*;0x>;^ -E+$v;X>F-TnCI&Vz?6D*T=w#g{(W<`+}K^!te>VU;SXXPU@!0Hp9_~7RDNJAP`9n12)6Kcrf~G2aAe| -H|=_)Pe-54^gnsSMidr^0_+X|LoS9s&_L#t@?G@rmY&UiKkNg+QIU|M#e=Y8BH^g^Mr?Z4SIT;R*U -+@+<4bwn}KFOr7=#Yci?;{ytO9ofKxGR8xnBiwoa!raM-6)Af15cB`lT&5zn)-fHnPMA=mh1dnb8)Os -6=7&1_l74U~R(IQ0)$DaSGf&oo3~fAiRz$+;tQB9unFSh2G=G^|4f4J -0>g;yO{K07>Z7byJM<|5#Gc66iPATo&P1DZ?9Iv{?$irVHCdh`iK;gcxo9i2s{jn>iIze(i8l~W+MaO -;ChQ?{4JIq7VeUY~)0nfgraAYt@u7nELaX0HrB(gG%PL!;CuV{kcKnYlI+?!$#I^!WimJ9Ys(c|Rypl -eF#|Apj_E=;Munus2kOj6z`oMz8TScJ8j}@DowJWh{t4&#heifI{*8)Tnbqm&|U<1;p6Bc=u$=XtPOO -rXF?1qf&i9sk)@If(a59XSHmBe}+(Q!ADD*@7tJpnA4hH2&Jky7BRl1rRbfvpvZvI%d(j;P+;i< -M#sWuK7L2o+wzSR2B?311nEfpy|cB1+>>`x=W-$<6Jn+n;YPXrz}%$7AvG=mdY -f#2-K4k6(U{Y~ew)+-89mIinTr*pB -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 -WV?@1G#m -O>f9EHMXs5ZR*d1(6j?`FgL=wH7%rL*2os%F?&DAZ)y+MSgp0bCrt9a+mCA&B0VmcHxPi_ZOGRmz&Gv -=F^*>E|)jSkIUuthpQjRIXP+SmYqb=>zG`vk-x&9LL0g5z(wBW?12O0=+P!zn;B{2Yf%=gY>3l4jwVoHB9)7iWZgDk(j>77m#(D9+lD3jdlZ ->*F6>h-HXDpI^YxzAC47~JV;r-Gl$o$JBvWN+wCH7Bi+b+9{-WpVfsgdzGi&O-M{`|^Mx-M5hHh>?c; -A-1U|d`9FTY-oma&Z2uu=|gE@i`LM#S1A29w(fD_YQ#WvWO60Oz{&Nl0X?-(W80GRH -Q^&5}URmqJqnlL4d13}x`-haA#kXzQ@Q#nPzAzOB(i4nIRaUZGkL-aOAqO*s~$a#$||FWy26)@9Z2{G -zGKJl#>AV-AWvBAZj2h82~vhfaVn2ecZ=uXD7`x&^UxVerzpY%86t1yO%FW! -8UZYZV~f|aM)rAC9)&2fJ!W -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 -v{&{Oqo;k`oX#7VEUtR2haFLnsQe6&$ -Jc5{1ucYSgFE`UfBT)dl3uI_rhAb>~*UNvT)`W^APKV*IJrBJYy28g%%{<+tChM^Q9;g;`m%CmC6_x5 -}i;MXy`Q>8$d_dL@wGBYt5yRoSgZvPnss|83Y;|k(eA=C-vYOquKy})`I1h<^>%Q5>037vRz`5C!>kN --WqdL6u<+`YYiGo%?E%s)B@-PN7;nJeUijt7aqi*Zl!)NEXCF1)v0BkoApQ? -FryhgCv^r8HCd2!{RwcktY7^C`|_D>!>o5+H^H>Jx~S_-S%Gz5JO5hE3S(o4e`t)%m+l_?yY(wgrDX`Hv6N+sT`Q@C5#C*XZT>^j$E$1S$w -HKzwMS6+gh5VY1>3(uTtG9)|3GK=t1JkUiiAaZnmQq^q0U8w}9RtwU3)B~whnAcqi84E=?YSBm02fCx -U`PVXkckMFK8{xX~XZ2~KwegFE0?|MC{;#Gj29n?{T9NoM+0D$KLvQLOi76!@k>(8J?2&yn>LaV#95v16tJ ->ngLXAos=8WeUecbsWAH$DR!y6x>B?^W0YCCy9U -}ow<0hgva6$>L0-96P~-ZlG~ikos4LXtsJOrc2p3hvZ$f$>aev7NRJ}ywt)RZq-eA|#^W>pZ2o(i%O7 -~0aNOhpN+uE1ur@*;NqBu>^UW%~sWQ-98wL;wj!pYRJp|hWdECYC$3V`Ty3gy@ECO|x6Rhx;O` -e)zKmdFu9<{R_}ASCMId?zC0}TLFryVPF*GYuR78Mvy5GO(auXHa1BE}X*u*^P*1dc_uy3c19(rikCwuRn6nRemnT0k4;j~D*Wohl2-3SKMDs+_Z7Y*M$T%{)5ZE8Y`HkPnlu{_&~ozwjZ9lzq -Sdxd~|E4$|g>BY{g|uIA~vgMC>1BlV5xpHGQMEifB>?gTd$p`nb(ygtvvIEI4dTjoeotGCxN`!JB{O% -OmRmGGx;uFhKxQ3nxqAxvO@BT=_GHmIgI58vaqjIBmW50ordy7P|sm|j}>Hc2UU1b);@7 -)k9Ju*E{-`(fRolsHTPG~eaE31X1(EhRRdyw^i^>L|%kjIVv!>zBpyQ>wkt@t6v;9-s~SgzwayjKC;xIil>y1||q&InCd>pF?S#_^_vhlMBrA3ML -9=49pAVy?7eE8mI<*h66-auN0&dC`JnRzdqMSleocOtYT!OnWf=zYyUX0xNaT_2EIL^yvvwH)N19*!V -_gYCKdjCjmLglz@zrLoQKuY|Jr5E$0>HgDBukGv4Cc;l%BZdaZ`*(BYHviYi}%-D-;zfasTy1d%Rg=U -1#s2Yq&)-#3FNz$TJThamKMaq&Di$d)`u{Xk?(0YYXdma1<**gq}N#RgGTC$Ij2Y?SgMzdKVH_|yb*4 -@U^P%5tw{aVbkYIIE?9CBaU?s;aey)=5wZVv`j+NGtNfZ#(Cw&+bBX-bG8kDfQu!sMa1<0A0=DVRO0z -@@J6>#S`T3ZkGuc4ejU@=k(VBW-SdnJ-1s6c@GsZOB6tTsQCAi(mp(Su?}dg2UVVz8{wc!C0T_|$iVz -f5?qTWkt=`8RSGI^C89MYm_T79ETNuM!Xd>ya5UQ@RAMO}5w~(u+f<;fajCO*ZL>!*NaqN#g^ugPCwF -~QPLkzgF`(P-NcuNW5H+SdDj9Ux$i9b}u`1KV(?O0q)|YszaSL8gwQ@_N#CS3N66am -I`tyMdyP$M54B#0NWY8e+C?Qj8p_&r2b2#Y)9%JdluaB@lKpL+Ps62i6Q>PNJd!5(=jF>{tmf) -3pU#4!#K$(~dGT;-!Zpsn7`7&bFp-=#sB^W{w!4B!^7C3wYkH8nD?E))Z+BKo@;A+8FT5h3C5?BC6AO -vQ+Y}ZkK1dwmVMS!Qg$#RJGm!WBBPRxdMP>$jW6kn}G6bQM@$Jpg_k`1~vQ%}ZCuXsO!d2fPR8tPACcy{V>S&~R**O)!UtmkFSuHPy}fvGzr`nqz`Z&Qb`g%+Y3kl -5WNkF^ry+2VD|RI-J9!=SN|O>9V7oE>> -sy+N8Q9@bf(_*mP72oXsQ6#bKGUv@Eu^z9NK$>E^Uy2Ekys0?&0tVFI6agi3!Id>|pc -z#Kqynct5c8U|o>!W-PuEltX89-sL&KwcdA+yz7;prVfX}jqYTS8wk_(t{FH_WZ|_n5Ki6!=-wSLs;f ->mAk=Ov_<4#{U&wd-{LPXP?Z_{~M1zV2%Hy+_rMayq1;rNb`0)pKxYbcqc2vwp{>aWSj(eBh(MC|Y~R!ddoXzGG)_zL82OKct9q%fpyB8ms<$ -NR)<+==o&&9L9UguK4 -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 -iwOe)uM2#i1@N3`tDpbc>AVut>oJI0u -JL0#&jsLu&DvtKGXTJAWq4d*Ys; -bl13E%88X9G35Q1U_l}dIK@FAF$uj3eiEm9D(;FkeE$I-on-)pl;JAnDRyt|8~i;9B)0s~t)EYOTqmI -DzWku>L9xwKIr#i+4k>mOG_i~-VJDRb~sU%{_mDkEK>-32x@8J2A8klaC!5`jhuZo!;K0c(4aCa(+B= -MfDGu#m|4pSG~cLAxp>HO+H&``C^|`jXLpmqJT0qgnrh%14yo=LE=BXpd-q{XfCjVNv9n7oNINnr$c(CR%W+- -_`OImu1dFU-cN%R{WsETK(LCX0)7*HMri(KXEpb6sLA{JF_2K<%Fj6z{Dx#46KOi`uzD)ocdC)RL2@@ -b?w6Tl!xYzN+ipzY^Gdb)+rEqbUVhyKS7PI{AhUbafJV -$4t%tv{K07Izx(cc|HJT;57r?}y#Ji8C+~jHT>;ABuD358^Y*198pwh*4gbA+>)%YSem*&U``z{&i9`+TRr0N{qQBLYIJ=zH>iHk#n$NhOfGur9QSp6Cht9Ue)~E;i(?)i>?JdM;k;fltEaR6)4BZx`| -1UIs+u6Q{NWjGpNae9WPia*J(nT&uPRyj?}J=f6IvlrpjDocCa%0lP^J&>PEiMgF`1ygCJQbQXo93m) -Tekfz#%n(XAh`JtK))8fCVmL1`ZDzjxxp|*B0aKD*hc5Iz;MKu+Wz9-eE{sAJ?GL)u$^bZ@nMB>G)tA -7ipNrKDagOC;{i^E2zl-O~fEYZaTteqGuE69vyrT=t1pN8m@F?|MKst!-abU$mhv;r@rvye;CdPu_X!!YdG`k&KPj4_NtHL%mi2d!DrfuONm -#Ci;@NL!xWl_Y>cj9bWCgIs{Ez9!TW3+6G937(M;r2D18X>E_6g|+gm1dU#=(ML0mm4y5ee|Y%?cw(f -pF3Lg#UdyIe_k$^cz#cRfQNcltb97vm6@{)t_x|!%W8)lpKwrZI@5>RGA~#M<0u4dj&UAT^-={q=wz7 -VXx6AI#O14as1douaB8)S8_nE^gm)(kn*c{uXN-;5{b3$BP7NP_7-2_X$Sebv7$=l>5LfNzK`v`#Z0M -c}*L7;ZL?URw3jjS@mgASVzt&xfXe}{5(0~ooRPTEA1SueH(vC+(-bl*?p}aLfn{*}Gt}qI0H2<-IvE -4z@RO6_KiHB+W(Vt<&W*^lLqg=$~+aLnTkY>cjJ%glu4d|#H2u&}D@`|O106jWq@kWCWgYh+}8-zhom -hMrf0Vb1cb;5hzT%Tiu8jYU3qfis6WSbv6yF>g@2K%4T$RbapGW_Y -fQ3OeFO%T|3Cf<{_BBc^6sQ*bn{xwZ+fq|tvTAW~Woxhp)^7h!8^&oa7au@r$YmL~LYTu4_cibHaY@h -Qgd+?nJ80@XB0WBivt9!3{9eVIiJ#FI;irE%7bb5tC)U25iM95wYc=M11s#tvZ%5P74EK`z@@9N9YA5 -NVwlI0CEzbsz+iN7k{4*=oC4E)9FWKY&w&2_Q^NCIA#-#vn67(Ou(=(em6GxLh`8ukt -7-#1`lsq|TD7NWZff|WgUi2IW78P_T9xhgR{DpjVro5jw4h~a_Mz6bc9W(E(?9AcXN9SJm<5XmE-|QW -N;cBRlzenEe!V7I$K4|92}pqOaT -f-$u>Z+yQ74^8hstZLI(I6q}U~a4+Qs86D3Xx0o@3u6PcRK>Eof#FP6tdXRMdOvl_H-o%ID>jjG@I@? -sKxH4&IxRCN7f&%ktit1=T5a{=FYO~4;1|XQPiO1z@xlS>w6%($-RwYt+o>#8mfAe7BiJxV#>QmR(Ic -HI%89aUu?>e@_tk++MQh+EJ7Py5rmw-agZhh2o^!=XhU$7DPx7%Z!DU1VlC28UFU^h70$ZnZ9e(RCq84S%V>=0Bx>?nZc2^!eV1mU`l`Y5zPVPU()Q{+{q%3NsmQ~qB0s~N%Ly; -Pw{vorpwhI?1#&veUma7^7EzAc)D9a3RTTq6DPBL0a1DG07CHHVTNh|}Ev#;?%G?XQYWeu5TIKL<-P(;Wvq4@rc&n_rz -t-8EnlaZw0vjT$Ii6IE4+bwAH3-pQc)g`*TkB*|p@8t@u)Ja}Ib99&s9`q~xl{qiA{FC5O(>97L2@fu -#CU0D3t{^@^=B0!6NrlaIg7u7<>Ex^7N(g>Hrs%tfE7}yP4ylMmW5;)u?)hZUIOCC02dKY12HWIla02 -4PVffa$PGBTSMQH87R+EWR5ds*y3CFEq2L|vwbFObHn;bm{JM#wFhb~@xD`@7-^_d4MUpfN?RVJ -9h7w-&cR87N+WYSk`117raNd*mc#vEqnP~IQY#DEtWWF#DHgT$WPUVFe;=O#g%Cw*IxRYW8z_iU4z4f -v|7A@gD5_?)|xA7cNh1nZ$BlcFk&Qr$p9@2cuDuH3;@(Mylwua+<5M^m?N2O!vz*+lweBgVyITKg)3N6^fgNTmSL-2^>1$ -Sc8?J_ayTf6H%WCEm@5OK`uhnyHMqLDt~+Q=rI2o?t#WUxPHpym^{8rv$>J0MY0Dw+)hO@x-S;g4PT| -HPK}~dPczbK^rLbhNvbt^kQr4P6&bwt=Ox+~dzNd+;j -1kgHI%XXz`I**K?NAGK~b%<9#M{N(LuD>A -qPFsXwXE&#rv9PZc+A85h|?Hi2Y|>jHd%$S|U^LeJbh|YHCyk3Mm)!zW6(iod>%!0G-C3%c@N5Rz9uu -U>SneM1;)~bNq4BAY?d0Y;TEOJ}0WWKrZ@E?*;4{O0&lSwr38hm|rO)43^+NqPIPZi`>wK1pwHhnrSm -=;A~!gL(}PZI -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 -3I|x*L)?KrmD$Dfz-XIuwhaJ(&LNC$Nz0n -hzxNsE%!__593Y8qsqBf&V(A-6os7zjGVV_#&M8RScGg(2J@6jVgf$87o`-rjEm?(~-CbF1(j+*KUJq -$x~2c;^B&Tyrw#-onnWFnPOE-syZOQ8>E?4dA$YN$|ssGkImk=1w5 --Cv5&_EudAl&L@?XW30l^;Pgs)M{S3zldUxdTHf&Y=8sb94QoU0vz#ZMQH+tb{10k*{%QM1U59*$A?5 -R!pteioJzXhwtecMa28aOMXWQ*iaz)|29hw3R8Fyq30Y(#cK(4%qC8QEN0IF?y(Jfi)A1Pg0#S+X!1) -Z+oG@k(%{kQq~9M|RbU!kE6m*5D$KLKE;{yGviqF7>}8Td)Kbzf8SL?_=Vh*vWFev0uh<2z?z3fW=M5k!+TbVHVWJfUKL{MoMNd!xL>a-TxNvkcRSjC)7hVg45Op~4SHYXUo%piU>tS#O@Y59L$$* -E{NMWV(mULP$DwrI>&)sduL8CsTT~$s?)|#$geKvzL0@V~UPCSFo9g6eGAyu*IQXBm6K&fXp*Z!0hW% -HH%HMJ%mmB^VqIJPE&BZ4{*0-bw&caJC9#yrJTm4ohdwfS5(lCl7!!ZPA8%)&e)ZlX~`7XK0S;WoY_p3B~mp+JqSGc;0`VV#Uq7Hectq-MnFgf=V -a<<$`lr0YF*Eh%Q0noKEtK%nRcyvzJQC}^JrJQXE`=rA>kp#$L+xX(VK;@IfjL9&SAFL#He?O@@q#tL -eyz7KTpKJBHoD5TsrX13kCH{*wegtk6-S>Lmos^WDEzD6dCGk50Hacf4o9}TNzK08gPs;^Bgu?PuP2) -`THw{bPVmRsR}(|=>z=>4utz0vdeeny?td_E^!bvK~0SyQnB6pWhY2fo%Q*~%#d=rNFZv1j@3+lyr7; -Rq4_;QLy7cKQP&8A$wXq$O&XhvC0``Ei}0kPF@x>Srr$FcOy=GT$sK{0vg>!I#b}hEIYo~$E}vOB7!~ -ozZ%qdz#+}>%z$P53c>NxC+}JP1OBTCZd{I!RIbb>_yq%!DeLU&mgwmt0D~Fv27T#dkF~<8CS3@a -f|lr`4Fta2V}kNV+!FwY_LTYk(uYwg}^}Uki|V0XXE)?viab?Avi=aMG)b^R4ES9 -X%s=&J~G~ATyxB{{T=+0|XQR000O8`*~JV000000ssI200000H~;_uaA|NaUv_0~WN&gWb#iQMX<{=k -V{dMBa%o~OUvp(+b#i5Na$##jSF -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 -5*f{?~Y6UA-eErd)2jpt@Dn~ahxnAEjH>x)w* -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 -2(y;?<&4V@bbnaui>+<}iVBXGeQZ -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 -noHc%w38KC~5ns;N#@vxE#pub;k4-D$ -dL7ihBfOE-=xnIKEuy#pgzIH&mJxwz;<3DJJQp7VzSgjG8r17ty*T3}cfpbdi8;QQi$Ca4E=t3IW_DfDO6_;Ej5}0Ux0DG>u*GZ)CPg -;)kFmVA+5@QQ5oH{G5dLF2L6HU!d|NT1kwDji|tjGm8g!?%0Pj9{ojZZP;FA`mD-mQAWU>g>fnu5CZK=0QcrR1-CtvF3{VI -phZ^OS^=Z)Xa#nyP`S;vU^2HbhGK2*>)C^~-d8q>B4c~2m>?1c4#j?{t(F; -EG_8Y$EtRdHu$#UW_>C&Zs+j=gnFLkLW`N;nh@)zEVk7-b| -lpBU8DY;#YY135)*1q=4G~GXVr5=8LZ{QO4Fs9Zge{>aY-u?Y6{IlShQ7IS+32@M*6xL|i? --U9fUO=IzXeyHnSAb>>2R}o!Svkts2x|?#J}`*(erlQ?&DWNr36w>IIcPoYO~7lu9i=UfXcqMC}_q^j%yUHsdUF-OrEygs@!L -pdW#>^4Wsuj>I=fUwk8|)}tZH!~THX#V^QQ{9FJJa&qa(b^BHxY8B_<6XtOyL1OrmCzA8ki$P}y{9=l -;;i;VG^Ylo{`TDI4a}&l>FH|7v1tE&h`pQ)Zmh{+E1IWV1whGwqO*JqtZB-~GwA8_V$6ng#gXiDj))5 -!?@8g+W^a=e#)>F-Y2Cb*s-`IT>We{X_^A9oy=?t*p@d4;o@oY9`UKn~)`u<%%cCnyg@MHFR*aPLH!&zKbNdtXW2g2%3u-B@~=G-ZQXljKKb_&19j8BqOi2_f_q>rT4`D1I{CI$pM3)EQ -Zy{YnsBx->=ijNM401)~h(>Si7jiuaGI|f$~F?0cz8*4RYb-ytA^7?!bVm&Im$1=d6%pj~vE2ryI=KF(Zi#%CxS|1OaCSS?$4tTd?KfY -is_z6H&dXou%~8-uuRN1$wgueh_#twQohibi={n=rQBB#&wlMzHXUYZ10_|rO|$}znL9}hfGd4k7mNvmq?;@W%Cyo} -v_CVqUs}oc8vW07(9q?j4sTQ&^l=|j^x~C0Jj)v~I3Ew*Tws&{|8pJGF+W+7L(srUQMQMh<@hvCNOms -iXVCxYZGY(C;8~RwYCL>Jls&+;wFV`xJnc(;o~SP$Xe*u#2QUa~q^be%*=93{nMM5#FoML6ext0nh^l -kci(wb=@zKRo!;`qS+A&)f?!Qm&?PK;7_VS -eQq-l}K;pT$*pol-_AA3(AtZ(FhU2$uR)kKL@t`Yg9kadPV=9N*yC)|nUiUI-DKn1Z^c5V|ckD7ZROc -91%WT!r3D4wC-g|K1-7{DFrd?FJCAG~Mo8X744Pa7H3#H_PX#)#!;zRo9mtSmmZ}fdI&F3Q# -w4mkl&w<9XEx>$OkGtRStnY#O;2J-^T#XQYXxoUfhN`6$mZ1~NmeXMsH0xkl#p4uyYh{0gq8m>|B^dV^e8QD`$%mbj$ -2qh$hfjjy-ckC(~GmaPlOUK0E+t8v(%X2NOxNRfBA$I`)ysrqy%6YV%`IBp>(LdIX?84K!O>r#{)JFg -V0bCK6p29;?o$YGu=g^POorjgj!&qyle{P0PeBP?J6nB0OTKOjoledXOhpF#|sLIDR{>|Dk+$2ip?^eC+he;=x1>*n%)Pm%GK8t4j9~->p@$!1(@CT -)>HWf9eA&j!BBy+i%HRN18GB6F=|bx%9$8|Jm`T%OlwsPr{&8|Nh7!eThLo6b4ig|SxT_IMujPKLbsS -QyIRmg2i)V44#PKHY+}K5Lf$}?OsC~>T;0kjt1L~17vRkdm$bX!xuVZouu&-;YQ2KKFzfU%1kvZIr6E -d5p%{%Y3~)xxrIqXl?*@tF{fjhPhmtXh`5%me%qf_!=@iU8=lhaf%~!xH6LwbdicX@!Wfg3?H1p#;D; -;kK;EkHg&bc@X4cW(QB;>&NGH+P(j_Av*PY)4jB#L_j8cwlC5FSi0$r8XbG3K-Akb6A*4aZP4y1`2b_ -(|&Mj!y>-GN;PapjN2G;vDP%sSj&Fbf#&wE~>0%==sXg%xTA7bF>X2pp~%H(kt!*Kv%RJ5Ss9Izduio+0y%~M$)W^uL)LhbW0kxV-Qn{^a4xn -GV=0tMKnj@!M)Eo-oYB#METI7NV9Bts^^bhV>7J!oTQNef{6MDbFuuK+tw<}o*8<3skh9Zwz>2+ZOOq -VdX5vcx~CepjL8Et10{CgNg)|DrN{@Gb0w?>6|KXwk}C#^e -h3;O80of<5wvRJWhC*m-hBiOicEe6m?_JJlYvT+%t^pTSCsOjI+07Q)^f*spt|HobkTPI3U0fzf1QVU -x~O)?me8;GqeSmRiktR|SeA-fF;Bh&?K;sjAydhRwozSxE1-l7qTsgN_E -458f;b*VI|ld8GHa8wcOsVA%5JdbQqvQ_NT7D<9YK?h|jdlF79qX964|S=>uyPW9%iQ1+cYJ|k#z>dbN~EBZDQg*0gM4#)s+%ZulV8kS@2(n9A1Eb;AY*Viyy4Q}8Ks3 -Y0yYpx)@CsNQAZn$|aQq(j%zG25Py62XKGq4n#5=NB-zevz*92AoNG&#JuIG(tNmzU#T&hVDB%K~0;* -#Q3W;@#oZu>!4}P_B2>>C(!YR~t0rU-gm<&3WaL4E)>TjuiZx#n1pztygQhEg&rwnzY=$Vb@H_rA+Fk -iOf)`@}RM1Bk_-a4u{nSbS!NJnW*Pt;K36j3RvwADeHuFH%FUI)ogsqT$ZE9La8ojjCL2UQbXG$DhRBhb!)0X~7hrjbsuo6GE4#B{aCfCed^FsN -qKro3swr%mgdi(;IAS%qFicTc?`H#EhMokZYE5B)y`NK5Y)OQB;I6)i)b^cn-SP~ORq?gLeOT+{`T>Y -`2BJ4tinDcMK(4yl^^?5K{bl9MC+$z^7^cT -SI`RWT)@-gDy0Q@i1B+&lDHm|L{^*(3Wij6_R$Acw&ZLdvK4QdkM0p&WyS`Ty$6qK8e&f8&sScEp_y} -&y_SgdsH%~@`ZYhL%RyP>gMUCIXw08lASmSZWsga*MUOmc?%C&iCx@Em!%E@jYh_m&DV^tR(@1a2Gr>2 -g2Q!3#1at@6c}gLGu3yAJw!Hh=N&eLW*{pl5U}4UHw<4)FYW -`uw@xrEJ}xsqSrfZ_OTR8N3kJYJ_<$Ui_pc*61pSYbfQqfWkL~;N&X3{Wo<;n-(oxTGaTR=vi61N35v -m$i5ApfrbQ~F`nRzR|4Xlq~Cs%ev2ELdGp_JSZ+j|)ZDEF)8|I^|39r(Ay7 -0F`oCHsMGSKeWg{93!gqoH@!Hz$&~oeTLI1Xrp5btE7P)1YGRrN!&`mpM7wp(VK -H=(txhXKe`6Reldmm0JW}UMk}4@+Mq5^&pn>Q?4A?rFAj)X6|c -K7rIqFY>uHn=T^C-i%$!jK|Lp}-eN1rgnxs_D3-@-Dh4c;-MY6*=9;W|o$S0y;NBvN*d;L@fTq@0Ui!fPHz~l(ygZfmT$GZHVt6ogP&G8Wl^6v7ikL?O743 -Xr(ui0Ml7)XDLT@?sJ-P_!jlRLiPyrV7rb6~0C9gxI=)(1*7|&hJq`JQtPx~}?B&j>r)mV);2Eb5^X9 -;okIG(F}r1;7ybU!P+Tq7X31gM%=$9)jyU$K00arqWAh3)*f#*FzYl6>s8aG(%Jh -|5q;w=<9)JLVYa;mIxLKuDap2MhxnMh`x@LYr%@PN@BoYrjBX}F9KQ%3_>FsH~=Aa+RZPItd;1U#JMYANN!@XzRG&DMhml8m|6dHf1rmc=TIje(b0}Wlo6By41&wLC%XpwBy%mggdytcY$0d2SQ -Cq$DX!KoR5>WX&L@gC-r_OB(K*S=zJ-*0OhvK+k3VpFr2-vNZGT|CARYGp -2T)4`1QY-O00;p4c~(;Z00002000000000o0001RX>c!Jc4cm4Z*nhna%^mAVlyvrVPk7yXJvCQVqs% -zaBp&Sb1z?CX>MtBUtcb8c>@4YO9KQH000080Q-4XQ{K2^vq%B}0Eqblf}u6E?$Ex7o~)jtK{nuv5qk0s~sv -d

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 -kxz{P&%R+RYK0kc!S=xmZ+>wV2K_% -FU{$Xn+-07>OL^_@d6PT#zpU0l9ByEs32&FB1!H>bb7J9~5ba@ZV*)L%^gAB^UomW6iJ;`4-`4{oomx4m_^_x9DIDx6)#0^*yiRzqzw-&q~D2y?9RTX{! -t}61S7KI!0CEvxhReW2(NCcsgH16HKKBaxZ^VWWcu_q2FkV${V++W3wj5JniZiE4Qn=m@`GPnl~xUAP -b-QwT_xIDJ4>4ATI`uI2Pbu_x|S=fKn+bhp=CG%5bQ;6Os-IGEA4s+VV6g -++TH=e;o!hY&e6NUZ8R9F)*6YrqZkq5y#*s53V4!vTE#s^Bm~;ciyDd0EIdtIv9%O&p4m)BF@xa@LgJ -JoPC@B3XR7Lnq<@_Y;5mRw~#R0>pfm#y@;W%xDv|r@3ypW+`aqG -_2^#z>E^a@>1lfurm;shp+}SsUzvEO599JuG%4TCCq3%%IKHOy@Ah^V!-jjmje85~HnvCR4x&!AikX_&0RRAl1ON -ae0001RX>c!Jc4cm4Z*nhna%^mAVlyvwbZKlaUtei%X>?y-E^v8mlfiD=Fbsz8ehMMGY=F@>=poBIG- -!|*c6!)}P;9ypVp|#|C+pi!jzvdl>XZ2W6eazn8`7NsXa+YB0tnR^O-{&z)$QOArZ`EyiQk&UK~|@Wq -}qx~cSbsOP_1$wsW7C^s>ZP03U`!F3>ItQv^bzRBH>fgjE6l{y6>@aO80!4vT%b?lQstHkWKh^KvxT*z-(>E7W#H^tIgBnOS^-;oTdK5+ -je-JTJuQS}bldpzFy><#d4PQnN-Bn?rjWZ{dZM!z2NaZR=<7Ias|2zAmOmEMjLP_Q_jTZeB8p{bqLGP -Nvrp;2@a7p?^FtAKSx9>>sl)r#uqpp=8Dmb3FUZARcyR52Nu}h=zlusB1I2pBTn>9ejY-KF044wc0B~t=FJE?LZe(wAFLiQkY-wUMFLiWjY%gP -PZf<2`bZKvHE^v9BSlezRHxhlhhK2*A-o>MV%Krt}1Qv#z-la-{ -t&E$!jOHvPsluT{2pDV#e}2W3-9a^Sso`8L>i -?wt7CEO=%p1rAF;S}gtvC^h6oK>UIsNyRSwm<_EFUh*1G8(m487)m_02lBinZqmFZ*9#>u@dD7@Mk<8 -`_JDvMhv~2n}&l}f!lEC#i4MB5`n1->_#`Tf+e1j+VUzNc_Fi!8MgKJW`4mhmXcXJTWp-G^HK_~T>i^ -^{`9evz5u=a3;&E#9<<{#K$oG8CR>o=mBVda-}!h>(LA(hFQfz}_pG&Gy#26Pd}7k_#R8o6!X$Uzmm~ -@{XullzW_a16U1|-^FlEsPG*PyI$Dy(LAWQ@p_yB9}m~+NQcM4-#9Z$ilp7?VZL4Opj^sDDHFza3F!W -A9yw~j?UXcZ*&u|xj}18qzMWVR_cz$`3Vpok-iC;72(O0r*brtK4TaRLI8%$5@4z268FyWxhzmNTjbc -ZoVUR|yx#ItCEU$OFup*mB*t4{!1}AxxpXzK-<&Gi;;L1v_^-mAasZMQc9p6HwxmA}A5x(Q$ZgQVV;o9 -7@M2YD@9_-qbIK -+*~ELBrPFu8dZFJF0TiHY=nc@aZ$zxL%j+n-zJ4_wQ4jGSWLEZN&Q2Ia-;>0#JmNT!2 -{eq0O)Jp^u=9M+lbz0F1FEF@ -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 -oQ1 -VEK>E-Kk35543bd|0Xw0{En6fT65lX~+r8UpH7iV($<;4UpG7ajp2jxw -otWv2SS&IH7d&fHBF8zSd#6oWh0Z5^#Jt+bucFM97f78g8a48m;$YT>;QOy8csaBGVMq_^KL#aJ%m|v -!=xk0cVdkF3v?j3f9~UdZ*sg-LhPN@bl;2@VaJa(X8%F@Jn(tq+*}grBQ?AYeelBV -igwT)0E_Nk@;xpGg@s`G&d*$Nj1GiI@grf;wJ@IzS_+JL5=3OWUv?YOM`>iyqG&w)hDf0f&mQb);Kir -Hm6_Wg$FiccJlW+)9~Dq7?npuyE_yuLS2>)x*`_C_GTu53B8^X*C{-Z$**Nwg03qk%{fg3vy+zE$PPy -|YJ_u`(}qH{hMc8_*^$dlvs`x>Ls8EHPK>s!T4@J7_1id*u{ZdDBb_a;2M@Zfpm9H>{6D^rjN@)Xy<#ThqksgB6H+w^E={>GG -8abDGVhlTPI|qT8B8U?g7!Av)5$0!aj>MZ^ft8bC?2y+-e9$=mH@2Y2`m8|Z~E+nOg5*La8ZMg-&2U3 -zsY0KjiY(NdI+p*;6&G1=k!}gtr;Y(?zDwJwowR=J@6aWAK2mt$eR#P@8)w -Ey*006cP001Na003}la4%nWWo~3|axZmqY;0*_GcR>?X>2cYWpi+EZgXWWaCxm(O>g5i5WV|X5Z*&29{kA={vvE(#QdLrWuzjYN7$%3h=BfA1TTl4U2zqCj=AC2~Ia=FJ<)2Ke3u$*BjsL) -Z<6?50W%MJgZ%7to@1=E0udX>}w|6b`C%Gb*IrTMb%!YcK%KP;quN7$=!G+gaEk~nKL8VY`QSz -#BR7}kBujzaw@Qlaf@cXW!{K<)JMZO{{q*$p9X;G@0PHXi<0w%~9ZbbBvje^}AhnEenMt}S~RP@}?8< -BI2tPdOhc)QyhzKLmwN99tY(?@u+4lpV$a_LCi?|fzSg(wU;ed2{9FCOFW$86x~=NobBTub83_uswkz -gt43HRqJ=V}#XM0&(TNRZG-9V7^anEuDKMqt -mrA3ar9n-#3AKG*no9bCyA^qd3~K`1g)kaD -+XIZy#&t&|Ymdl!$J_UKQ4UW^ZG)75$98VL|;f$)1k*`09>me--u)cwg5d?G%;VkXRDBz_o -c!&XsLl?*uz!`m_14jsQHe^+~>+A!fPPDd$7F-OJCVMV1`-4oa^;6Yf*_zZXrQ$7Qs>ka -de-UR5U=U}gpp|vyLCr2ct1HO)Q;f7(ub`jD8Tf=MRfaPa2NlBxZN4DwA?XZK*PAew{$<9b0+s76Hm)i -hpDP?DV{TQ6rel4Fm{11#l`5^=d6X9g~(xf%m9o>{{m1;0|XQR000O8`*~JVmFNkb&=vpyk5d2uApig -XaA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFLGsca(OOrdF4E7bKAyt-}x(c=w<{a6imrU({L`EC=wlOt{r -(K`lcP%1wnEtq6PsL4^fK8{p~&H?0W%FPDb}bZ}~w2f!(uv9(&&4y2!UIiq>^i7d(nsvfbrH#o|Sor* -*|6@x9lR_xUzXQuQUxZ*P4rxD;}@7y!9r&zvfl(V2yfPYhh^?J|PMO?+KtcrS -xjA84%0Q&9*(iW1u~|J7s&$mT?A~+2(f~Hk -p)6pu$rYXDh;GnZ{+w_DQv2E5sGR@fFJjr^8-!!PhaOHDUSUzhK1xml2a9;^i9v5>yX6J{S{9KkbTq2 -h#LF-&TChE~YQf-o2hhZ>FhIkR^+eD^!CDQ+h77V-K9Wg-ALIP3j -yKweNnz99#Bv~P|?oPu|$ljIQ2Y>eZ_0@0F3%SeNL@}h0-@q1dHel7-3 -9+4TPL@5K|2cti>L8&Bdi>YfsxxCI2}lwWx;`XU;#6}7eJ-m5GJPHt%$a?H(+O(6ErZa4`ixx#7OTLn;A)o|Ix*P%m12zmeRKh|lX`s+r$7}~q%y`_hrI%(-B!)}jtxUcf!lQUKImQ3NmQKDiMBY@@ -4(7ZCXm8>yMpr>Xn#lk50TGS}jK^uA^a_HbU#7F8Av?mG-;Z?yvA05jXEC7! -S$O@uW6v>L0gvOaxg@MuR~+sgp8Zq(JM%1KWY1;E9^gU&d+4=kurA1p$;hAcw -AG>a6XHc()U0ZA`K{p8pRxMrpEmVfmz?<66#Hou#}id$tI~g+XVC-~_doamk6W%eW@=G*G>TB2J7fj( -2epgU6hKlbly^YIM;Izp|{}E_fl_L$Pp`vp7xjeaL3uRhJA;IbZQ?$%)l0ht&%>Wp!4Sp`qvv#}$Cuz -vdYb8?S9o4kQ^|)fo4Inra>6UcvWeuyKNDG)5=nkpR1f_yVUP71l{nR?2^E&XAnPFOrtDI{4afX6JLF -RFr3tK2&l-lX&tZ8uGo$_iP(y59$eU08U0q`hp9YAQF&H$*j%IHsGNXd*v{}FW?-U8Npk1!lVa`fW+- ->4%({Z#hnUK@FN5Ryl0TMddt}c6it@HtRlgv0YKwqF$+O~Td@Rbvse1#)JBOR#-cCXBo}I{7FuD{=g@ -@)N;_~!-w` -JUzG0KORg%hOQg?{pLeCSN373lJ^~@urB8Y)Qqpg~(rsq@Wxa{5>cp?vQR9S4)_w!OZ94X#|X1@)QDN -z2&EqCsL4Kgs@P~jq`M6-L_Q^WdiU!>I?>`N|S^L>dsP!ObWT{z{y8F^&O27m}}F5z0+or>izA^z(yq)rL4Q&&Y|DQY6=umIdVl%{Nejj7 -bhm)^OnFb_Z4oxEgE$S_1Bp$uyDfAy&s=mn@j(b^ayABMy%^6A2IDQKEwu5kGGWewXBR3DhCxYZOSBDL)2_33^39u1czaA+f*#RhQtHdqEpUWGCV2KZhd{ -lCFTQT|#`tGqwyuDTD-+2L~5GNQkWneF-5TDicVOVql4TuyGXKMC^O*9Be6jGz7@uiSB%^fgzG&qBKs -nYE{z@DpXr;i6cl*<@J+9DG$`8+B(evVe~lj^*UO|%PKEyg2^XLJRvFCp=Da|YR`G5!p724YbA$M0r( -UT8cwjyfgHBq!pl4*R&GI5ka!650qwD#P20|EP{bO}3LMD?I0Q0s5Q0&robQlRzL0)$H~*P^&4N?*HT -?Z#+((W9fha#_gnZIh -T#WIP`MLWQO4pKNBoq@1|Kcmn)>10h+3(Hg27ga>SPYY0q;mP}H`zLVArPsPCy7VX9TJ%9z;9NNBgj@S)&lxbY -R(Tp=H2IOr{76ips7(wXTW<%hRnHZB0XzbpEB0wp5qA|ehJkJ6R(gw=;LNl+yEKoo+JzaFE9KklC0x? -DxM12uv-e@HVSVPg|)Kd_q3*kz#C8%GpXY-JN -o8Hy#cU#2ts_x2tAyO_S7&Zc;E^>%i7_3I5@&aThSr~MB!dC=IVYu)|5HqW3)pLKBB+3ovpPasJA_Vk -A#`)>5&Lvv}1k?yM965$c+U}o6bN&l<{S{KRhvWjT-0(v&t)?2OkF*Fi9wX6jIJ2@TobQ(X3(4~nY!; -q4vYopG{W=>8<=8j6pqKh%KWGG%WZpkp#y)F3Q-K3z+yXOhlL{&C@P==)*0M~*~EF7Al_N=IR%X7I|_ -o-C24RpJdl@GjABwA2Z_2CQcF%Olhkgl;F>bu|`w1MR^fsyh@tLRTnG73vh@Yr#9ye1<18%7^w`h&&@ -{K%ru0wjPPd21ZClyvmGBOik!5Y}5^4CLMW)thjr+)b;Hyj+LG -yb)2KsR$YinU*l(xpnx`p{Rj-4yc%%)s;ciiD#05|dMfw%1z^cwd%rfA-HciiC{-BYBU^+LGvKAzo{g -oxVp&gPuDu{zS(20eKJ*e1F_qb${}dkx1aSddmtQ!<-yf?i|lEX%W}q;lnp+7ut?lHiF(wcZTw8Y=QO -Z+3CDIB6dEUPH*XvGcfeU4nMk$4$TZo!2`QZ?jUjk2}VK0kRA>EeiLJ2TRa12-8!Vta58!g;vr1PxXA -TVF9m4Mx}y11wt@&YWWQmtf0?Yf1!O__Xskucz%;K58TevV5<{>Scfjd$5`d^Qw8qN7p3{1=xj=ggEn -rAI_ep7QNMhdb5khc3TKz~^wC{uN6J=l|kdizU51RG7;W)McgMq~5C_-%zMNQ}N5yuKYPv%?^uqSh{@ -be{3)}V1msL1zCdVNryNSD3C4{z9V8>Y{%PLQ6$4n+e{0J_`?U?}^Ar6khzgW&70#b|jVex}KHhx8aH}>f4}yo@aNUbGRWJRf@*!tAZW>EBx(SY_e8s2!i#@I06ufMaraMCPGMx!_k+(r8 -%iE@IWi?V^LyNH9HaEA!HCtD)W|n3CrE-(ru}-a>3ZR -t`)d$eI_4c~J_cV#46BtE2GPHno=S(GSviyI95Se$4K*Q=(hLm4G7ofkR+V0^1-DT+Ly=4Qxs5*y~>= -?4(r`M~29XZK7~EY4xUceWha^QC#uX*e034#7amF-Q_G-5>yI!DwYEv9ZTzuB~QS2)R*NhUV?mJ^IAE -6{+wQs2oqj`>sH)8-&EW5d9hx8fBItdtduQFFT&ICTUSqn)Z-8o{r~)r^6U4b;~zeJ0~i0)?^+x`vVH -{gLM3O9)5Dr7S`0%C8;Pv`M7%r_0zqNpL;~^j_jZta%O0Fo9Y-rtR>pbNBk0SoEtzxB24LL3Vt-`u_d -k5l>my^{K7b*}*lA-g&)Avh*LpWQM?hIlNz30+%~GcXxPZwzu~OA$=2a%)o`&BKnN27uZDIqm%T$8}L -4&ZT1zw_ZL2EYh_mW7DQFX&?KjI@?s>1HsvISf$R`=G$23S1hsg<04L>wB#9Rqd0VZmrPfPamrC!_I) -UWbTz!lkhYd?*w=W7P}%rzfvM_*JyTs!g$^`&sx=l~3~#W>NxLtWZ -1WD%w8i3S`}0#O_a3rc_BWmU}FE@uLbw)lYxrL7)6S)9vOTu$;F8l^6984*ny9W2l1 -Hp(5-zWTIjwtaJ3RdRBRCFNQLiv1gdUf?c8x7oGq)tV9!QvwAZ&R6yzMAypIqSq~?a&4(zhMZlZVl=| -C7{BM5~PsvsH5J}-zo=W|P}unerq++Gd`-96FmKvAFUbnh08YCoz3OZ4t0|L4` -1p8xaICCr7vxK7|+j79?Lbeq)@GRP7||z%ppO)at$oJMeIpK|JvWdjllq;y=1iAPKtC25#IWGH5TU!C?>5dWWZ8=o^_!!52ZWFGT1f@MmBt? -E2Ve-G!fLI=6)hG>pbjiBVFF-iN4lvl+FAEQc-6Yc>TyJjPTn-*kq!u* -Nurjro_tRku0v|j4F)PV*xApEAID?xZW`YLPZuc^;|vLCLzRce?vEkr3frv6*Oi=8cUjsd?77{BsSoX -sEcjXFb7}WTK7k@gZ&l=`x_1{B2~L5irN)lo(rcJ=nh*6GMnjz@u5t@&;Wp@#=R{=I6x_Zvm`Q1@zHO -z=SZ+Flyp~hhE-or>Yrl=g_-C#LQq#n;qMwPB4R#&ji3OR3p<;i7R-KcqWy3k1*fyQn(3TBJX*d;)S| -WK;MmDsH=F$~{TA>|rmbG2WBf#`B;JkFEwLJ1=CP9oLSfp+L3#jtt5wRG1}wL4U->#EYV)#nCX`=l5wp%UfW+2)|hbhs)cMkwc{h%Evfx_YOI&xj{ru -^wSphg^Kkv+Hp-jcNaGxgF-vSM)OZh7PeD&h{V>Vg-|1ApSW2XX4a=*R##-A|9op}b;fHcT`i%kA@En -)Q-(R88K*w}lr>XrK=U5<#q(Z{W=n>*ZL9b4y-)ckAogb({1Rl=bp0Nw-9b0NDl~Kj9$Q9zIX`Y*}5) -biJ^@7ti{iaAjBL_n)&sZIr`hSDG -%j#kN1bkWhw8pD3dn3LuIUBqew}>;r#kCJS#D1UGwZC<&R(0HE|Y0~3-(rc*^W1cZXHOIWZ2D7f5IVoD$=OEbkl`KbP>hJTDl6e1jHN$OG5K4%4r8hzxQR?aqFp4_uY!-rL4*nAUdE9>c|TH+0jNnkCi3f#~WPyHs -|PdS1EMZobwTLMh&9@)6&h`EE1&+`ugg+$)Psx`! -6J06C4eCkgbNSa&&{e-Fv=yJ3YUAsWu&o1_0Y%o_Kq0wqHEXQPaZbbo71Wxxwy>ErAM9!S~RLKsvBZF -&wnEH>nD{UK-o%nnrPQpna~pbM8RX+~IyXi&bq6b1l7cs=ut!TISYQG`gV^ulR8wsa)snvVh!Mo7AY% -9Z3mDe(V?jtd@vN%Z(LIE|w1i^tg|{Dvt(Z(G@}9^(F@mw_iW9-?P9Uri&s!=xPlq`=7ACME%s==Rh# -`lkt{54>eyu*$xlf_kIA0IQ&UFKA)}TkWY@s^H~X{yf40d9vFB7b*f-<6Vv=NB#ET1Jv#*&ffB_xswB -!odJoJ(95d7SX4quXlE?Garbta*TR)~^=it&LE*o8J`1wcA_t_Q2E;SE0v<9q_JwaJxj*f5@oE%7289 -PQ%5Icj>$)c{rF**oSLK9%{Hx4nWyiomH%c!Jc4cm4Z*nhna%^mAVlyvwbZKlab8~E8E^v9xJZp2? -$dTXqD<)Lcnao0vC3&43?{rsOie9rKOFB`$Ehm)&hrp1W2m}}a6vb>dzx}#<9+&|^*;|(nr&3vX^z`) -f^t%UM6#1G((PCR|3lT+3t~Ys6F+MNzbX$o?-FG_r`D)E$bDfvws@&Vl9WOGOEz8aV5zl4D%UDW-b|q -d?x!4D6o9Rmhx8mh0m2(5OPQDLVx#H*a@5T4;I|?ie#Y-$UmCUo!!1g)}ekNHdVpzd_%Bw|QtbbBu1o -ZO0$Y1u;cqP{2b5?FQI8G$$-Sl?yNtPwRdPAnun{iR(MbPP+sW&`+;!9yCF6E1UwJlQf@P~)tXnZrCP -NT{Ff4`50*Vhk!j2^}TgWGp-(dhu@QJz)erD9j?HqQh;c`SHV5(53~<8T@QX>j|2r={pjrqTVEyZiAi -0J{&Tt!ang$$*PrLsmPYqiscK -&St4P56{~W%-sHfQc`9gc*kYT-$ac0V@?;wWpJ1>o^#~;Q2x}YSgU$@qo?y1KN#lw@ZIV3Y1m5T+du0;o`4bo|=Woj1gX*!9wI(gd -H0C2un3X)>~NQT;R}ZzY(mginIB?5@krB4+3-n1njZc3MLoCB|DT?@hV_EOBAD74sVN{EJf&1T8MI+; -%^?L2QN4Iy;=8r -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 -<2eu`;1d#OE!&N-iL!uoQ -2oCxi&xm|oRrG`-iV0E$QJh_x^qRk7MP!yNU&7}mV9>W_HL_gXk@lki#~UpaBv9cJv~+7x&jn7(b+PvDm3^#9jtNdqKC{t< -}fU!c^XKZO?OYnD3rumT{;D_@SD5VBb*JK=0xW|?fB+D@#yBO$2O0AtE+_5$dpk8zrBQkAE-vuNS5*Q -=F}G_%AD)UWgTlC9u{uXl3G)IoBmZIj?UY6Q-6a-=l-63v4+Lwz1L(WOIy~W&alx>b<)Y0%;$G#7~t= -FV?0BhHJ2uoD81LBR3-I4qMM#vmyLwBD(p-H_R-9V9rzaxk%92qnFH@z+oR?@c>SAO(}9#P;r*8j-H) ->8VCwX~dGHRL%JnT9o`PPBR3GldFnu^3O(s+@1j(PLPAu(MerwYbLB|KX^u2r6+q8L}r#<}vcu0r#>b -*j)ANgp!32M&X9gl}1Cm?dRU*fmj82udNg1Ukk8Wz%y<`C;yY!E6GT?(QX&L^e#?J^#H0JpU8qlOc9l -jlzaabM)|=_B=+-v#5_(ZiSf$MLlX4U~25n=Yb5v@IVOC`Q&5fBit0uN7NfQ7f;a^O{OK-({dQwj`Qb -%G_MD$pQ79_HO;I -D@I^Hjdd{nZ!(1-5HntyxKefd^Qv7mko(V>q$H~8Hl-q3aH{k7e{ufY70|XQR000O8`*~JV`M_-R4F~ -`L6B_^kC;$KeaA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFLQHjbaG*Cb8v5RbS`jt%~@@4A{Qg+;;|GhIqN+vBS8}!4q{*blBnTPZKqLsT!8^@#`N -&n62;J!<9w)A4d#6;T@#gN`+s8=x -7XYu;2#EcG@}=7w&2d@!|f4=}H(<#b&KJ>C5jds#xqmIK84LE3szzo|kgPoyZ|La?rzxXP2+F$z5r4s -qmjkbTsu&iXDZY2QDpIejsE}W0bQpdM{V-J3?dyS8p6!3bf9uMFbJZ6p0xcQ)mUoG~yq%G~Q?u?7rpB -8zaIF7g{=agAtT3rGC?bd20kyCFUZhteMh|ZMo&nnb-_7Hg3Sikx2p*#C>-E=XdFI{1`OTfQ=>{YLe$ -m?R;TsL68H9^3e!Lwosx@wT -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 -PP%rm1%~=TUBqI-NE456Gbkj2=Z#Pfc#0C}U -$*&^X-Um|qg4~DM>XP5490Ai&a0h1u)W@@Zodz;gv&DUF-zl^?Ye>1F(kiP}=gL&ux^yj0^KSuH!ur@ -YFKRaLh7GPctPa0?)1|NZIwjkSpZ8_GVA>jb5-#4#6+}!jJ*Wi`*=K2GTXplFDFt`MD=&YGGj{L>wi7 -{?pot?ZoKOV5J_~ZEe^62!m*MVg?dH?&a`~K(>B}U9^Qo^&|&bJ%e`m*z_`fYUH4*n@Y4IBoIf!^@tF -1$Rw*x6QZfzFp1fA}W;e*F?8_q*RL!=9oM<|YgtNLoFH?+%m{R>#eXF>Zk`20$4OE8&bV+u4Qz#phec -z4y|<6MGK!49#?nJ@VN%+3eXqGaQ3i49O7+O1q9SS(#&f+5fg~;wvkYx?4rjaDdESJ4MH-(cEj&yk4S -)R7Qg4?o(1)hfDN#+vxxNOsAvK-&sjtCwjPw*-k67?G?ijXoT|@boObJ+njH4NhXahSVzAdT6`lTmJN;GU_H}YV2hNkLv*F-kF#LURT>6Tmm#>Zouiv~q`P -+Y;zB@aA|M%g=<<;;1_~*a=@WvCLPw!^IzdwfaC{F%Nv%I+f^sxB6^=x}*cW?hcp8sOIKizEpw}I|wU --KFU+hE35!~S<1MCSDaTW7)_fa72fa3nEp&yOqw24DEc`PsldIo8OmZ|{9wx6qp*!6o4VY4ml;{58Ld0$%NuNeHaeF_!#9JrQ|W`*m+rp -q=Fz=_cL(RL-1joB@j;5ct0s)FG>w&@`wdW`iboJCT}iZ;!=qP&)4|1s`-23ox1G)Hks_mX+rTtiHGe -ZAO}KB*FD~8p=fg{>ec)uSXJ6?*R3V(bq;}jdbT(RGUgB)l*G=pjrM&W&CM}+u8nw@%F9KKdu_47vWKMycu0U=J#UZ0mR4Z*&$WkO%!fOd>;9 -hl+U3a?HGVP0V5*9XAAUk!%;Dj{DFkKVk)U`!2YbP(+jFgl${=w%RrrlDB{+@=A)foK^nTb3gFDwX&XgSj&nmD%!M({3hytUcO7y@S -qMO7s1MLwHR7z@nGAe)E+)B=Zz)EMz5UdA_r2t>((93orhO(t>bqgW714`07x7^&LjeCWY-voOvxNLa -w8EincL0OJw=&uv{Jd1XH3zDvRQre% -M*=rFn)4h6#3}K`XZC()+ma@rO=JEm76uww>593M8yv}(3DNaAKP^RnJ)Wp69Yo(l9o0b -wA$Lo?xL5y{XC4B=Am{RS%3a4p=Ppu6scZ=-h^Q$$z{>EIQcdkb2+F=zb2o8;j__(%^uBzZyt3! -9KKq+bgC6Z?XE;`;H?7h1pOC2)LfY06o>*0M7DBd;&u37d2dRFH -hl;Q_zseq@O}63`$a9_GV+w!hJtMVEL?l(mzYKw!pI=!sW%QeRxgPl4q%*s -ycoDy^i#wV0!nbMQgR7sv!Rd_jvtAVU?@b?5p(Y4V0XTTA%r=Zk`jqz4z2i-&+H%i>$PN4l@HWV;93oJC5b60?dk{plc$Sbedm2Q(>DM+HzWSAI)s1`-V+ -K<2TKJ=+fHgyocpM94`Jzsuk{SLs-YC(1!J(bH8mft>HVq_evO?eK=;POpL>qy5Un>$D>=|=lq~t9(o -6Dm~>~lAW;LHwucQS(`)Wjeb2uE-dcp(Pk@r`24Th&0l|(9;f23oNsWwO5vW@PD6AqaRo2i^2_ZC8Ask>I$A;7nyGuEL$g3GkQ(gm#stdy(f{gO -fW!c7yoJ&hxW0&A{VJaEtC>(BTZSEBJE8U^IE2b-HWicL^6HrU`5rRByyL8vP4A%f6Hth9`$x~psBrr -v52DqkDAZ-aH4_EQW5`5KRv9n?vRVh^H&BwkcJeniE>^lfJ6LSmVkpvSktEXueaiq>5q<2XrK)+fxA) -hJj1*{g7p2m0JxG{J!#9jCvT=!;8 -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 -dVhcayP)<# -%QDp@KC48u!m7YH&svamo$Xd!NDO$4ZCJ+Tss36JZBfwn3S?pCrD#NLgdmAE2Xh?`#mcxY4vyuf|&gu -eIs9VhCD -#5qpidu`59ZzKZpRqm0m+S&}Zz(JX|0+}{!1%OuoZRlep-UG^tUF|6ftnY%nzI-}5&~;ozMg)e2NGQ8 -1Raq2bDrRGQ-zRABWL8KP1S$lp@_tI8VMrpH>NH{Q9F_VV0-S76iow1LG%r9-_btXs9F{B;;25XLtOV -bC6Sk-Lb_U|>D8v_DWP*9Y0fmM*XIEHs@@WHTDhNTzM22Bsuyn3-Pc6JF?<0Y4GMBPXZK0i8oYU)#a< -P@W}rjaZwo)29+TTi`vY;P0ucD7F_NEIj!3e -B4-j@NLRqS+Ab7S7)s3O-#d*@Kpg7cuvz4ySFm44GF_o5r-ZaAcdmpH8R^1#gD!Q>a=k+FlkH)$KGJJ -k(R<;Z|%BMzpyRuS+tfBCROi`*+F*~N}VZ*Gia3I=Exvn-QWpMUOe|}*;kz$oVQu2xKZDMm;7o^pCI;tge$1r3%?Lz~r+-5lj(dov6fXLN(Ht5qu-%lE_Qx9)2e0 -ZYx1(jtk$}8(2UW -xB_9AYpeYpD5Pt_{KqG+Aah80_{+sHl9Wc3z*DM*snv0b6fU)19mSK|*0yvV*+@qkoWA529;P_wkS-;!mX&@AX!OTGw644b(J$UBvpA9BfIK)V3}8Y0Da7sm~hwl9C?l1^(80~H4yz-33t -VIeWq|%Ev#&z53LEyD&}CQF@GC27;6Pk&GYI7RQK($He;wWc6Cn3nO;%>8!Fcsk8<5ar@Dp8+qJQbHN -|_yIH)Q!~973HDNU}fGE-=^(l#&R9Y?E3n~6gQO$|6Hh( ->t>z=e4ew8`gbWyd|%K=0?wd&`CS{nWYZSWhy!r<_W!@~X$@>x-#M`s{yY1`KYOS<1L -RXfwB1|x&kY-Vsrgv=+4F)*gw_pls6I4SX9fNUi-Q8sYb;f*Sgll4#La7EVH>X+>r#q^j(3Av2*LUqcsCKoDYz^ICn|X>Q+If&>bld)$i6K -ZCH*DlFd8EdNcnIw$3nhcqm`Yj)cMXSCMAmg~n8Arwbt3F;^+pKp7MFwWEs3SWS6$=4q^d;L6KV1n%7auce-0N! -e`4gumjNS(;>ucFI?|{{|it{0|XQR000O8`*~JV$E^s91qJ{B6C(fsA^-pYaA|NaUv_0~WN&gWcV%K_ -Zewp`X>Mn8FKl6AWo&aUaCwcIO>?t05P56$CCXS}KJ1wu`hFZbllGDx-W!9lVk^er0o*w4w7vX%dbKeroxkI(FW2fN+Cs+?5^sS1|GC;5 -EiHc5W|EY$u`ZP?fQpUhbbhiyOEt2*TwdyUU3yV0piWLC-z`||qjiIq~am*2jA|Ks&Ld&>*9lU1zJvR -3`r82&T+@#6>kma{CcnB1xAr&{#qL6z)HrIOXZ<4cvF;z}iPntrmq%w!Q)a@Q<}!;4TWdOFQG -DfUl=PtZ)Q$Z2@;t9B0_rfla_e6!0EQ-SL3CM!*qFfJ4AnWWZM_;49jh6%4poPJ>wC2)MEZe1+mT!^R -Gz1Kxw_YCPa8Bj5-oz#-sC{SZj&gTA?G0KFd6E>jvklo^lq+Jl){S;FJN%y^c}K+v0GV>H*s4x}^FgK --gWVQ_S?Kf~kll=EO2+H5or`$ptTaw`y^ngO=C)~zK#nVjN!$5<=eN4fUtI1(V&ycq}8B-m7s6HG6SO -&kHybD9F9xn6f5{p2DtvoEAPA}|1%u~yFjrg;^uooP^Ca2iBy69H-xGCToFp(Tz1OkrS2Aq0a-ulvl* -u(1Q_6!u_w$s&1)KsX7o->xCOuR~lE2e~ce2i&ebs9&a>2~lQ3+G_%&w`&Mf=+4lHR~`Yag&U&G%vib -u=`*|s|5D{8PG`h%NFjaVhV|KnBT1fT8k*+&=8XufPohI<_@n`Kvjf*R`X3wFq|&o&34_Ec$4C<-$&` -BAqDkoX4BzSQU7%)LSXyI53R_z)@GF6!TUmiRPhn|<5Giausv!EhHN86S*Mv7sVsMNUwvJ8U25WNlbU -jfagh!U$p*?qxxe5!o)dI>vibL8c? -#gQsFY;5Ijjp@POCARBEoJecvR(dhyM!{T&nYY<2FFMfB*{vZ+;^5U?9@f$b|z=xcu?g)wikg?^9B;V -a!N0WwxAd@KoAB9+5iCz5OxE4G|oyDUESvu7i+)YJ388rVd64Tcn^|jyF4E4&}b;|nL?xeX-d|gjy4l -Jtd>UmswQg{qV3pdIS0i`N70dN;UU~CTRYTfnJF|y1!zZM^zG=SU9{ac~kXjPT&wuqK -1sDffc9$IGG_6+km46c!U8SZGZ<1fFm{vZ9&|qYp72c;L`^9zyLU4aec -x0lXAz?sB8xR5m;2ScHHk}w8-9oVeUyenrGS#BI5rGf{56wrXLEr(HNp!~Q=#KF3|{ENUTtKi{G58(hkSuAIUE)RvaBbR2_g^Vmo=QWuSrNm( -A$TlKEYaJIszk=|dE%IFwp@38$qTr1!}osElTa1gO@_k+R}@4Cl`yK+h+=GxRA(MxobkYh1Gbsc*CQf -Mt9g1T>1{s*XUEzBpo2_5r78gHK@R7OeE*u-_C#-OzoYJD3IK@CNi0?<7wC -!2qM5dIdN=!4k6P+)K36ksW>ZNRSlQp$qwO0z2{d-HSc&#fdO%`PWRd~Ab!^u*0#voXef^OHu~d7kBPEXg=8fk)&tNdfGVtSoYm}9$x!l^K`6<{Mg^f -FZ|20z3cif_6hB*Yc!%L3D0FA2m0*iznS4y7@-|pNP?z<`6e)@b1>l4g*qa@6k<}#rLuTWYX!`=L01Q -roykN4AW^C@hUff^m-!g}gKD1tXy83)f+$;_G4I8tnkYG7}K;A?0B{!eL?86GRt4j$v`)EH&1LJEVw_ -GsPf&@8KQ1I$`9t`zEkhFWKQ)p?>wJCN33GHGW;g60up(Cv+EcUnd-CI&I7Pt;OrW}JlyED0e`sn9F8 --u5a5d}SKC2xzTwD!9fCNY?xhhu*_tv495!QaQkyyLW!+$BtXraDm{zZ0}UV9Iu!Vr(TXar0%X{p-)* -h`*Js(&AlwYGq1}ko~13yOBH{^pa!*}7d}N)o);nruPND+eAMG1;yam^B7DUB9p{qt4!0F{=vYI5o==*JC#wW(*s&BSy%v5({j4qf96 -gGj2aXh(pD$a1M~AYeeM%BuLpvT?{~kR_0z;rJ3-Qkfpk2a`a88z|9QpOp-%8H34#~+Ma?vJ)6wiuF8 -bP^l7rueA3^ZjK8jO@>!?-L<(zcE7Do6W%B0J~w_~xb)tgsrZ;;>`xuq~9?-kh5$*cj}20Z>Z=1QY-O -00;p4c~(c!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2ZbY*jNb1raswO31T+eQ$ -+>sKri1f&87o52y%s3Y-g|TVOZrPX`y!Vw>tRQ*Oma -EjH;mWu5nw@`~T+Anudr3H^Ap>e@^eTz(k -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 -<6ay3h000O8`*~JV&BwqszyJUM9svLV3;+NC0000000000q=CN!003}la4&FqE_8WtWn@rG0Rj{Q6aW -AK2mt$eR#TR>aG_BF002D#000>P0000000000005+csRRH3aA|NaUukZ1WpZv|Y%gD5X>MtBUtcb8c~ -DCM0u%!j000080Q-4XQ(rU*uN({j0Ny4502%-Q00000000000HlF21^@tXX>c!JX>N37a&BR4FJg6RY --C?$Zgwtkc~DCM0u%!j000080Q-4XQw7u2l@$vB0O2G602TlM00000000000HlG15&!^jX>c!JX>N37 -a&BR4FJob2Xk{*Nc~DCM0u%!j000080Q-4XQ=-7pTFMUq0AVu#03HAU00000000000HlG=9RL7uX>c! -JX>N37a&BR4FJo_RW@%@2a$$67Z*DGdc~DCM0u%!j000080Q-4XQ{;JOYzzc!JX>N37a&BR4FJ*XRWpH$9Z*FrgaCuNm0Rj{Q6aWAK2mt$eR#PVcqo?Qq002}000 -0#L0000000000005+c89o32aA|NaUukZ1WpZv|Y%gtLX>KlXc~DCM0u%!j000080Q-4XQ#;j?=0FJm0 -52Q>02%-Q00000000000HlF5KL7x5X>c!JX>N37a&BR4FK~Hqa&Ky7V{|TXc~DCM0u%!j000080Q-4X -Q;uiz4&(>`0QndI03-ka00000000000HlGeNB{tEX>c!JX>N37a&BR4FLPyVW?yf0bYx+4Wn^DtXk}w --E^v8JO928D0~7!N00;p4c~(=hw_o*?3;+PvF8}}@00000000000001_fznX`0B~t=FJEbHbY*gGVQe -pVXk}$=Ut)D>Y-D9}E^v8JO928D0~7!N00;p4c~(=-cj`1~0001l0000T00000000000001_fuddj0B -~t=FJEbHbY*gGVQepBY-ulFUukY>bYEXCaCuNm0Rj{Q6aWAK2mt$eR#VjdaF^N#0093O001KZ000000 -0000005+cMPC2_aA|NaUukZ1WpZv|Y%gPMX)j@QbZ=vCZE$R5bZKvHE^v8JO928D0~7!N00;p4c~(<) -AAbh82><|Y9smF#00000000000001_fna9<0B~t=FJEbHbY*gGVQepBY-ulIVRL0)V{dJ3VQyqDaCuN -m0Rj{Q6aWAK2mt$eR#VV?GE^v8JO928D0~7!N00;p4c~(;(W`@cs0RRB_0ssIc00000000000001_f&GsF0B~t=FJ -EbHbY*gGVQepBY-ulJZ*6U1Ze(9$Z*FvDcyumsc~DCM0u%!j000080Q-4XQ)*dce2@eH0H_H702u%P0 -0000000000HlFvkpKX2X>c!JX>N37a&BR4FJo+JFKuCIZZ2?nP)h*<6ay3h000O8`*~JVm>Usq2Lu2B -HVOa$AOHXW0000000000q=7G%003}la4%nJZggdGZeeUMV{Bc!JX>N37a&BR4FJo+JFLQ8dZf<3Ab1rasP)h*<6ay3h000O8`*~JV{dxd -PSO5S3bN~PVApigX0000000000q=DJX003}la4%nJZggdGZeeUMV{B6he1Pj1ONb-4gdfm00000000000001_fpE+K0B~t=FJEbHbY*gGVQepBZ*6U1Ze -(*WUtei%X>?y-E^v8JO928D0~7!N00;p4c~(<6gYW%*2mkOV00000 -00000q=Dht003}la4%nJZggdGZeeUMV{dJ3VQyq|FJowBV{0yOc~DCM0u%!j000080Q-4XQ|fU;s?`G -k0FDa)03-ka00000000000HlFW+yDS@X>c!JX>N37a&BR4FJo_QZDDR?b1!3WZE$R5bZKvHE^v8JO92 -8D0~7!N00;p4c~(=T(w7#`2><}_A^-p<00000000000001_fo9+U0B~t=FJEbHbY*gGVQepBZ*6U1Ze -(*WV{dL|X=inEVRUJ4ZZ2?nP)h*<6ay3h000O8`*~JVT3!3Cp$Gr~OV0000000000q=9c!JX>N37a&BR4FJo_QZDDR?b1!6XcW!KNVPr0Fc~DCM0u%!j000080Q-4 -XQ~$H#<-r300EY_z03ZMW00000000000HlE_`2YZLX>c!JX>N37a&BR4FJo_QZDDR?b1!CcWo3G0E^v -8JO928D0~7!N00;p4c~(=u$ot^q0ssJ~1^@sa00000000000001_fhhd|0B~t=FJEbHbY*gGVQepBZ* -6U1Ze(*WXkl|`E^v8JO928D0~7!N00;p4c~(=2!<0Pg0RRAO1ONaY00000000000001_fkyxV0B~t=F -JEbHbY*gGVQepBZ*6U1Ze(*WXk~10E^v8JO928D0~7!N00;p4c~(;`3unU81pok=5&!@n0000000000 -0001_fo%c-0B~t=FJEbHbY*gGVQepBZ*6U1Ze(*WX>Md?crI{xP)h*<6ay3h000O8`*~JV9AJnmU>g7 -c%WMDuApigX0000000000q=9@00RV7ma4%nJZggdGZeeUMV{dJ3VQyq|FKKRbbYX04E^v8JO928D0~7 -!N00;p4c~(>7Sb)bq3;+PDF8}}@00000000000001_fg2c!JX>N37a&BR4FJo_QZDDR?b1!pfZ+9+mc~DCM0u%!j000080Q-4XQ!gWy@Rc!JX>N37a&BR4FJo_QZDDR?b1!vnX>N0LVQg$JaCuNm0Rj{Q6aWAK2mt -$eR#OXid4LQD000;m0018V0000000000005+cK1TrnaA|NaUukZ1WpZv|Y%gPPZEaz0WOFZfXk}$=E^ -v8JO928D0~7!N00;p4c~(=?bqy{%0RRA60{{Rg00000000000001_frm~30B~t=FJEbHbY*gGVQepCX ->)XPX<~JBX>V?GFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVXbO;_`~d&}lmq|(BLDyZ0000000000 -q=5%e0RV7ma4%nJZggdGZeeUMWNCABa%p09bZKvHb1!0Hb7d}Yc~DCM0u%!j000080Q-4XQ;*`ja$Nx -c0RI9204M+e00000000000HlFLQUL&PX>c!JX>N37a&BR4FJx(RbaH88b#!TOZgVebZgX^DY;0v@E^v -8JO928D0~7!N00;p4c~(;vfl9b{1^@uw6#xJv00000000000001_f#*{J0B~t=FJEbHbY*gGVQepCX> -)XPX<~JBX>V?GFLPvRb963nc~DCM0u%!j000080Q-4XQvgu2Xxae)09ynA03-ka00000000000HlGSS -^)rXX>c!JX>N37a&BR4FJx(RbaH88b#!TOZgVepXk}$=E^v8JO928D0~7!N00;p4c~(Md?crRmbY;0v?bZ> -GlaCuNm0Rj{Q6aWAK2mt$eR#Ts`S07&@008)n001Qb0000000000005+cA9Dc!aA|NaUukZ1WpZv|Y% -ghUWMz0SaA9L>VP|DuW@&C@WpXZXc~DCM0u%!j000080Q-4XQ`;-Z>hA>r0G$~C03HAU00000000000 -HlGzl>q>7X>c!JX>N37a&BR4FKKRMWq2=hZ*_8GWpgfYc~DCM0u%!j000080Q-4XQ^k>Mqx%p50Bkq_ -03!eZ00000000000HlHJn*jiDX>c!JX>N37a&BR4FKlmPVRUJ4ZgVeRUukY>bYEXCaCuNm0Rj{Q6aWA -K2mt$eR#Wqj+MRm{008e6001Qb0000000000005+cD6IhiaA|NaUukZ1WpZv|Y%gqYV_|e@Z*FrhUu0 -=>baixTY;!Jfc~DCM0u%!j000080Q-4XQ#cE&dK(G=0PY?D03`qb00000000000HlHDwE+NdX>c!JX> -N37a&BR4FKlmPVRUJ4ZgVeRb9r-PZ*FF3XD)DgP)h*<6ay3h000O8`*~JVv-DZ*7XttQD+T}n9{>OV0 -000000000q=7`h0RV7ma4%nJZggdGZeeUMY;R*>bZKvHb1!0Hb7d}Yc~DCM0u%!j000080Q-4XQw(z& -$U*`D0DJ}j03rYY00000000000HlGK!vO$rX>c!JX>N37a&BR4FKuOXVPs)+VJ}}_X>MtBUtcb8c~DC -M0u%!j000080Q-4XQ!azP_Xi9B0ADKr03HAU00000000000HlE$#sL6uX>c!JX>N37a&BR4FKuOXVPs -)+VJ~7~b7d}Yc~DCM0u%!j000080Q-4XQwc9KIy?pd0O1n=04D$d00000000000HlFk(g6T)X>c!JX> -N37a&BR4FKuOXVPs)+VJ~oNXJ2wPD002J#001BW0 -000000000005+c-q-;EaA|NaUukZ1WpZv|Y%gtZWMyn~FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV -6)>Prc>w?b-U9#tApigX0000000000q=8r20RV7ma4%nJZggdGZeeUMZEs{{Y;!MTVQyq;WMOn=E^v8 -JO928D0~7!N00;p4c~(<{l00000000000001_fe+#V0B -~t=FJEbHbY*gGVQepLZ)9a`b1!CZa&2LBUt@1>baHQOE^v8JO928D0~7!N00;p4c~(>3m#NLd0RR971 -ONaX00000000000001_fxG1a0B~t=FJEbHbY*gGVQepLZ)9a`b1!LbWMz0RaCuNm0Rj{Q6aWAK2mt$e -R#RFqbX4mM003Dg000~S0000000000005+cxaR=?aA|NaUukZ1WpZv|Y%gtZWMyn~FKlUUYc6nkP)h* -<6ay3h000O8`*~JV8yxcLYykiO;sO8w9smFU0000000000q=DV^0RV7ma4%nJZggdGZeeUMZEs{{Y;! -MjV`ybc!JX>N37a&BR4FK%UYcW-iQFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVt357KN(}%2o-Y6Z9 -RL6T0000000000q=BRg0swGna4%nJZggdGZeeUMZe?_LZ*prdVRdw9E^v8JO928D0~7!N00;p4c~(z?C000000 -00000001_fzm7j0B~t=FJEbHbY*gGVQepMWpsCMa%(ShWpi_BZ*DGdc~DCM0u%!j000080Q-4XQ`3)D -F}no-0NW1$03HAU00000000000HlGgLIMDAX>c!JX>N37a&BR4FK%UYcW-iQFLiWjY;!Jfc~DCM0u%! -j000080Q-4XQ!TH1U%CPS0RIL603QGV00000000000HlGXNCE(GX>c!JX>N37a&BR4FK%UYcW-iQFL- -Tia&TiVaCuNm0Rj{Q6aWAK2mt$eR#N}~0006200000001Na0000000000005+coJ#@#aA|NaUukZ1Wp -Zv|Y%gzcWpZJ3X>V?GFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV>CLabA_f2e^%DR9ApigX000000 -0000q=Dc|0swGna4%nJZggdGZeeUMZ*XODVRUJ4ZgVeVXk}w-E^v8JO928D0~7!N00;p4c~(=zz6u&A -3IG5qCIA2;00000000000001_fk9FN0B~t=FJEbHbY*gGVQepNaAk5~bZKvHb1!CcWo3G0E^v8JO928 -D0~7!N00;p4c~(>WI%Nj382|ttT>tia -P)h*<6ay3h000O8`*~JVz2<6y83F(RnFIg;GXMYp0000000000q=7Jb0swGna4%nJZggdGZeeUMZ*XO -DVRUJ4ZgVeUb!lv5FKuOXVPs)+VP9orX>?&?Y-KKRc~DCM0u%!j000080Q-4XQ>U8^`|<(+0GS5>05J -dn00000000000HlGMdjbG(X>c!JX>N37a&BR4FK=*Va$$67Z*FrhVs&Y3WG`)HbYWy+bYWj?WoKbyc` -k5yP)h*<6ay3h000O8`*~JV;ye;Y=m7u#Cjc!JX>N37a&BR4FK=*Va$$67Z*FrhVs&Y3WG{DUWo2w%Wn^h|VPb4$E^v8JO928D0~7! -N00;p4c~(<_$cJSK1ONc%3jhEv00000000000001_ftP~<0B~t=FJEbHbY*gGVQepNaAk5~bZKvHb1! -0bX>4RKcW7m0Y+r0;XJKP`E^v8JO928D0~7!N00;p4c~(>5-4uH@0000p0000i00000000000001_f$ -WC@0B~t=FJEbHbY*gGVQepNaAk5~bZKvHb1!Lbb97;BY%gD5X>MtBUtcb8c~DCM0u%!j000080Q-4XQ -{h^v-UR{x01^cN05bpp00000000000HlFyhynm`X>c!JX>N37a&BR4FK=*Va$$67Z*FrhX>N0LVQg$K -Wn^h|VPb4$UuV?GFKKRbbYX04FKlIJVPknNaCuNm0Rj{Q6aWAK2mt$eR#T6qSSr -FG000zg001cf0000000000005+ce2@YFaA|NaUukZ1WpZv|Y%gzcWpZJ3X>V?GFKKRbbYX04FL!8VWo -#~Rc~DCM0u%!j000080Q-4XQwXG`bdLi70O<+<0384T00000000000HlG1u>t^aX>c!JX>N37a&BR4F -LGsZFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVWn*>Aln?*_wL1U+ApigX0000000000q=8Sh0swGn -a4%nJZggdGZeeUMa%FKZV{dMAbaHiLbZ>HVE^v8JO928D0~7!N00;p4c~(;=qt)2!6951WL;wIC0000 -0000000001_fg;8N0B~t=FJEbHbY*gGVQepQWpOWZWpQ6-X>4UKaCuNm0Rj{Q6aWAK2mt$eR#R!EM9= -9W000bx001BW0000000000005+cNZJAbaA|NaUukZ1WpZv|Y%g+UaW8UZabIa}b97;BY%XwlP)h*<6a -y3h000O8`*~JVBLu(1a|i$cpdA1J8~^|S0000000000q=9e!0swGna4%nJZggdGZeeUMa%FKZa%FK}b -7gccaCuNm0Rj{Q6aWAK2mt$eR#O+blrnz>000#b001BW0000000000005+c90mgbaA|NaUukZ1WpZv| -Y%g+UaW8UZabI+DVPk7$axQRrP)h*<6ay3h000O8`*~JVbYEXCaCuNm0Rj{Q6a -WAK2mt$eR#W90%c=hW002h<001BW0000000000005+c@+JcSaA|NaUukZ1WpZv|Y%g+Ub8l>QbZKvHF -JfVHWiD`eP)h*<6ay3h000O8`*~JV000000ssI200000D*ylh0000000000q=7Fe0|0Poa4%nJZggdG -ZeeUMa%FRGY;|;LZ*DJaWoKbyc`sjIX>MtBUtcb8c~DCM0u%!j000080Q-4XQ}{OHU%wOp0EkBb04o3 -h00000000000HlF>C<6d+X>c!JX>N37a&BR4FLGsbZ)|mRX>V>XY-ML*V|g!fWpi(Ac4cxdaCuNm0Rj -{Q6aWAK2mt$eR#N}~0006200000001ul0000000000005+cf;|HOaA|NaUukZ1WpZv|Y%g+Ub8l>QbZ -KvHFLGsbZ)|pDY-wUIUtei%X>?y-E^v8JO928D0~7!N00;p4c~(=y03zQ=1pokK6aWA#00000000000 -001_fzdq!0B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3Wb8l>RWo&6;FJfVHWiD`eP)h*<6ay3h000O8 -`*~JVYKm(DsSp4FB1ZrKF#rGn0000000000q=8~X0|0Poa4%nJZggdGZeeUMa%FRGY;|;LZ*DJgWpi( -Ac4cg7VlQK1Ze(d>VRU74E^v8JO928D0~7!N00;p4c~(dXaE2%00000000000001_fm& -1p0B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3Wb8l>RWo&6;FJ@t5bZ>HbE^v8JO928D0~7!N00;p4c~ -(;?)mp#21^@s_761S@00000000000001_fy{3M0B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3Wb8l>RW -o&6;FJ^CbZe(9$VQyq;WMOn=b1rasP)h*<6ay3h000O8`*~JVRpR%rEerqv^&c!JX>N37a&BR4FLGsbZ)|mRX>V>Xa%F -RGY<6XAX<{#OWpHnDbY*fbaCuNm0Rj{Q6aWAK2mt$eR#RNV>E!MN002)F001)p0000000000005+cw} -t}%aA|NaUukZ1WpZv|Y%g+Ub8l>QbZKvHFLGsbZ)|pDY-wUIa%FLKX>w(4Wo~qHE^v8JO928D0~7!N0 -0;p4c~(;z7guBI3jhFYB>(^~00000000000001_f%c070B~t=FJEbHbY*gGVQepQWpi(Ab#!TOZZC3W -b8l>RWo&6;FLGsbZ)|pDaxQRrP)h*<6ay3h000O8`*~JV000000ssI2000009{>OV0000000000q=7A -%0|0Poa4%nJZggdGZeeUMb#!TLb1z?CX>MtBUtcb8c~DCM0u%!j000080Q-4XQ%ZO_Kh^;N0QUm`02= -@R00000000000HlFzm;(TCX>c!JX>N37a&BR4FLiWjY;!MPY;R{SaCuNm0Rj{Q6aWAK2mt$eR#O9VLc -*5<004mo0015U0000000000005+cdzu3PaA|NaUukZ1WpZv|Y%g_mX>4;ZVQ_F{X>xNeaCuNm0Rj{Q6 -aWAK2mt$eR#W6-jcert003ME0012T0000000000005+cPMre)aA|NaUukZ1WpZv|Y%g_mX>4;ZV{dJ6 -VRSBVc~DCM0u%!j000080Q-4XQ?|5;v2z9h009*M04V?f00000000000HlF#p#uPLX>c!JX>N37a&BR -4FLiWjY;!MTZ*6d4bZKH~Y-x0PUvyz-b1rasP)h*<6ay3h000O8`*~JVY!f}Mm;e9(@&Et;9{>OV000 -0000000q=6`?0|0Poa4%nJZggdGZeeUMb#!TLb1!6JbY*mDZDlTSc~DCM0u%!j000080Q-4XQ_!}>j! -Xpr04ojv03rYY00000000000HlHar~?3SX>c!JX>N37a&BR4FLiWjY;!MUWpHw3V_|e@Z*DGdc~DCM0 -u%!j000080Q-4XQznk}UF`z^0EP?z04V?f00000000000HlG5t^)vYX>c!JX>N37a&BR4FLiWjY;!MU -X>w&_bYFFHY+q<)Y;a|Ab1rasP)h*<6ay3h000O8`*~JVxg+o|3jzQD;RFBxB>(^b0000000000q=CJ -%0|0Poa4%nJZggdGZeeUMb#!TLb1!6Rb98ldX>4;}VRC14E^v8JO928D0~7!N00;p4c~(OV0000000000q=AOG0|0Poa4%nJZggdGZeeUMb#!TLb1!9XV{c?>Z -f7oVc~DCM0u%!j000080Q-4XQ{g5JlS2Xk03QSZ03rYY00000000000HlG@x&r`kX>c!JX>N37a&BR4 -FLiWjY;!MVZgg^aaBpdDbaO6nc~DCM0u%!j000080Q-4XQ^z}7-{%Mb00kES03iSX00000000000HlF -by#oMnX>c!JX>N37a&BR4FLiWjY;!MWX>4V4d2@7SZ7y(mP)h*<6ay3h000O8`*~JVqt#C>SOEY4%mM -%aAOHXW0000000000q=94;ZXKZO=V=i!cP -)h*<6ay3h000O8`*~JV5m0CS*#-ar%Mt(p9RL6T0000000000q=9+O0|0Poa4%nJZggdGZeeUMb#!TL -b1!INb7*CAE^v8JO928D0~7!N00;p4c~(=`e-Wdi0RR9S0{{Rm00000000000001_fsNDy0B~t=FJEb -HbY*gGVQepTbZKmJFKKRSWn*+-b7f<7a%FUKVQzD9Z*p`laCuNm0Rj{Q6aWAK2mt$eR#O=t$&+0T000 -av0015U0000000000005+cde#E~aA|NaUukZ1WpZv|Y%g_mX>4;ZY;R|0X>MmOaCuNm0Rj{Q6aWAK2m -t$eR#V=;&!#;a007WW000{R0000000000005+c6XXK`aA|NaUukZ1WpZv|Y%g_mX>4;ZZE163E^v8JO -928D0~7!N00;p4c~(<9v<$F!0RRB01ONaX00000000000001_fr4;ZaA9L>VP|P>XD)DgP)h*<6ay3h000O8`*~JV$Uak^jsySzd<*~p9{>OV00000000 -00q=EMZ1ORYpa4%nJZggdGZeeUMb#!TLb1!gVa$#(2Wo#~Rc~DCM0u%!j000080Q-4XQc!JX>N37a&BR4FLiWjY;!MgYiD0_Wpi(Ja${w4E^v8JO928D0 -~7!N00;p4c~(<7l-3zA1pok95&!@v00000000000001_ftL&f0B~t=FJEbHbY*gGVQepTbZKmJFLPyd -b#QcVZ)|g4Vs&Y3WG--dP)h*<6ay3h000O8`*~JV$(R6($qWDhN+$pSApigX0000000000q=5_)1ORY -pa4%nJZggdGZeeUMb#!TLb1!psVsLVAV`X!5E^v8JO928D0~7!N00;p4c~(=8MVb@a2><}@9RL6y000 -00000000001_ffOGE0B~t=FJEbHbY*gGVQepTbZKmJFLY&Xa9?C;axQRrP)h*<6ay3h000O8`*~JVJg -Ie^3<>}M$|3*&AOHXW0000000000q=76c1ORYpa4%nJZggdGZeeUMb#!TLb1!vnaA9L>X>MmOaCuNm0 -Rj{Q6aWAK2mt$eR#SeVju$xt007?x000{R0000000000005+cb~6M3aA|NaUukZ1WpZv|Y%g_mX>4;Z -b#iQTE^v8JO928D0~7!N00;p4c~(4;ZcW7m0Y%XwlP)h*<6ay3h000O8`*~JVFRk9iF984mR00419R -L6T0000000000q=9lo1ORYpa4%nJZggdGZeeUMc4KodUtei%X>?y-E^v8JO928D0~7!N00;p4c~($_h7T@?TTj70zd7ytkO0000000000q=ENI1ORYpa4%nJZggdGZeeUMc4KodXK8dUaCuN -m0Rj{Q6aWAK2mt$eR#TU6$*GwI002=F0015U0000000000005+cieCf(aA|NaUukZ1WpZv|Y%g|Wb1! -XWa$|LJX<=+GaCuNm0Rj{Q6aWAK2mt$eR#R%uV*EA^002xa0018V0000000000005+cUu6UUaA|NaUu -kZ1WpZv|Y%g|Wb1!psVs>S6b7^mGE^v8JO928D0~7!N00;p4c~(<%F(Ir$7ytl{R{#Jb00000000000 -001_fzopX0B~t=FJEbHbY*gGVQepUV{MtBUtcb8c~DCM0u%!j000080Q-4 -XQ>83)(Elp{03N*n02KfL00000000000HlGik^}&7X>c!Jc4cm4Z*nhWX>)XPZ!U0oP)h*<6ay3h000 -O8`*~JV1`i|L^ZNh*@+$-Y7ytkO0000000000q=DkT1ORYpa4%nWWo~3|axZXsaA9(DX>MmOaCuNm0R -j{Q6aWAK2mt$eR#VdT6U!aA|NaUv_0~WN&gWV_{=xWn*t{baHQOFJWY1aCBvIE^v8JO928D0~7!N0 -0;p4c~(=#V!TZ<0RR9c0{{Ra00000000000001_fq&)&0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%gPB -V`ybAaCuNm0Rj{Q6aWAK2mt$eR#SzRWD^nr006fF001HY0000000000005+c@aF{paA|NaUv_0~WN&g -WV_{=xWn*t{baHQOFJo_QaA9;VaCuNm0Rj{Q6aWAK2mt$eR#TS#Z*33|002cd001Tc0000000000005 -+cLg@tnaA|NaUv_0~WN&gWV_{=xWn*t{baHQOFJo_RbaHQOY-MsTaCuNm0Rj{Q6aWAK2mt$eR#WrQ{u -=rN0089)001Wd0000000000005+cmiYw$aA|NaUv_0~WN&gWV_{=xWn*t{baHQOFJ@_MWp{F6aByXEE -^v8JO928D0~7!N00;p4c~(=DC2d}>1pol%4*&or00000000000001_fz|y50B~t=FJE?LZe(wAFJob2 -Xk}w>Zgg^QY%geKb#iHQbZKLAE^v8JO928D0~7!N00;p4c~(=m$h}$^2><}I8vp<$00000000000001 -_fye^}0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%g|z0B~t=FJE?LZe(wAFJob2Xk}w>Z -gg^QY%gPBV`yb_FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV{LU}Q2?hWFIS>EZgg^QY%gPBV`y -b_FLGsMX>(s=VPj}zE^v8JO928D0~7!N00;p4c~(=ukX5ii0000!0000V00000000000001_f!P)Y0B -~t=FJE?LZe(wAFJonLbZKU3FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVFCeYqo&W#<{{R309{>OV0 -000000000q=8l!1^{qra4%nWWo~3|axY_La&&2CX)j-2ZDDC{Utcb8c~DCM0u%!j000080Q-4XQx_bf -z0f5B0EzVj03HAU00000000000HlF27zO}vX>c!Jc4cm4Z*nhVWpZ?BW@#^DVPj=-bS`jZZBR=A0u%! -j000080Q-4XQc!Jc4cm4Z*nhVWpZ?BW@#^DZ*p -ZWaCuNm0Rj{Q6aWAK2mt$eR#T(rX47{B0074f0018V0000000000005+c8bb&GaA|NaUv_0~WN&gWV` -yP=WMy?y-E^v8JO928D0~7!N00;p4c~(<7zl0F)IRF3_dH?_)00000000000001_fzC$=0 -B~t=FJE?LZe(wAFJow7a%5$6FJftDHD+>UaV~IqP)h*<6ay3h000O8`*~JV%jaiiJOcm#-39;vApigX -0000000000q=EW@2mo+ta4%nWWo~3|axY_OVRB?;bT49QXEktgZ(?O~E^v8JO928D0~7!N00;p4c~(> -4Stva{2><}YBme*>00000000000001_fpvul0B~t=FJE?LZe(wAFJow7a%5$6FJow7a%5?9baH88b#! -TOZZ2?nP)h*<6ay3h000O8`*~JV#p&<`cLV?c{|*2EDF6Tf0000000000q=EO22mo+ta4%nWWo~3|ax -Y_OVRB?;bT4CQVRCb2bZ2sJb#QQUZ(?O~E^v8JO928D0~7!N00;p4c~(>4mtk7J2LJ%}6951t000000 -00000001_fwhwe0B~t=FJE?LZe(wAFJow7a%5$6FJow7a&u*LaB^>AWpXZXc~DCM0u%!j000080Q-4X -Q(XHuNTdY-00s^K04V?f00000000000HlGon+O1KX>c!Jc4cm4Z*nhVXkl_>WppoNZ)9n1XLEF6bY*Q -}V`yn^WiD`eP)h*<6ay3h000O8`*~JV*bE&BS^@w7umk`A9RL6T0000000000q=BKK2mo+ta4%nWWo~ -3|axY_OVRB?;bT4CXZE#_9E^v8JO928D0~7!N00;p4c~(=sLYgsX0{{R&2LJ#f00000000000001_fi -|QF0B~t=FJE?LZe(wAFJow7a%5$6FJo{yG&yi`Z(?O~E^v8JO928D0~7!N00;p4c~(c!Jc4cm4Z*nhVXkl_>WppoPb7OFFZ(?O~E^v8 -JO928D0~7!N00;p4c~(=w3SgoX1^@sKDF6T*00000000000001_flIIm0B~t=FJE?LZe(wAFJow7a%5 -$6FJ*IMb8Rkgc~DCM0u%!j000080Q-4XQy@3Q&?*H00HqE903rYY00000000000HlGLwg>=lX>c!Jc4 -cm4Z*nhVXkl_>WppoPbz^F9aB^>AWpXZXc~DCM0u%!j000080Q-4XQ}jUtd0`j;0O~XV03ZMW000000 -00000HlEfya)hrX>c!Jc4cm4Z*nhVXkl_>WppoPbz^ICW^!e5E^v8JO928D0~7!N00;p4c~(;<1(*0Z -0{{Tj1^@se00000000000001_fuht10B~t=FJE?LZe(wAFJow7a%5$6FJ*OOYjSXMZ(?O~E^v8JO928 -D0~7!N00;p4c~(=*+ey_kIsgELdjJ3+00000000000001_fg0Ed0B~t=FJE?LZe(wAFJow7a%5$6FJ* -OOba!TQWpOTWc~DCM0u%!j000080Q-4XQ=mbO7&rp}0MiBl03rYY00000000000HlG75(xlsX>c!Jc4 -cm4Z*nhVXkl_>WppoPbz^jQaB^>AWpXZXc~DCM0u%!j000080Q-4XQyE3l*JCFD0P9cy03iSX000000 -00000HlEf76|}wX>c!Jc4cm4Z*nhVXkl_>WppoRVlp!^GG=mRaV~IqP)h*<6ay3h000O8`*~JVeyjeE -HUj_v+6DjsBLDyZ0000000000q=BV92>@_ua4%nWWo~3|axY_OVRB?;bT4OOGBYtUaB^>AWpXZXc~DC -M0u%!j000080Q-4XQw=H8vY8S901h?)03!eZ00000000000HlE&K?wkGX>c!Jc4cm4Z*nhVXkl_>Wpp -oSWnyw=cW`oVVr6nJaCuNm0Rj{Q6aWAK2mt$eR#Uc!Jc4cm4Z*nhVXkl_>WppoWVQyz)b!=y0a%o|1ZEs{{Y%Xw -lP)h*<6ay3h000O8`*~JVq|tlo_ZI*F^MnBaB>(^b0000000000q=C(~2>@_ua4%nWWo~3|axY_OVRB -?;bT4dSZf9q5Wo2t^Z)9a`E^v8JO928D0~7!N00;p4c~(>R2%Q%i7XSd*fdK#}00000000000001_fd -|eB0B~t=FJE?LZe(wAFJow7a%5$6FKl6MXJ}<&a%FdIZ)9a`E^v8JO928D0~7!N00;p4c~(>Ngpv<-8 -vp=ekO2TG00000000000001_fo0_h0B~t=FJE?LZe(wAFJow7a%5$6FKl6MXJ~b9XJK+_VQy`2WMynF -aCuNm0Rj{Q6aWAK2mt$eR#V+O6}2NM003+N0stof0000000000005+cA^{2jaA|NaUv_0~WN&gWV`yP -=WMycaX<=?{Z)9a`E^v8JO928D0~7!N00;p4c~(<35Ym-B8vp>1lK}uE0000000000000 -1_fr=>#0B~t=FJE?LZe(wAFJow7a%5$6FKl6MXLM*`X>D(0Wo#~Rc~DCM0u%!j000080Q-4XQyVYVGZ --8I0Lpd&04D$d00000000000HlElMG63LX>c!Jc4cm4Z*nhVXkl_>WppoWVQy!1b#iNIb7*aEWMynFa -CuNm0Rj{Q6aWAK2mt$eR#W8c@T8Xp0082daA|NaUv_0~WN&gWV`yP= -WMyJaA|NaUv_0~ -WN&gWV`yP=WMyAWp -XZXc~DCM0u%!j000080Q-4XQ^$+OgLe%80M{@804M+e00000000000HlGBkqQ8CX>c!Jc4cm4Z*nhVX -kl_>WppofZfSO9a&uv9WMy<^V{~tFE^v8JO928D0~7!N00;p4c~(>Pzq~y~1ONce3IG5h0000000000 -0001_flQwY0B~t=FJE?LZe(wAFJow7a%5$6FLiWgIB;@rVr6nJaCuNm0Rj{Q6aWAK2mt$eR#S&;{S~< -Y008m;0015U0000000000005+c(4z_faA|NaUv_0~WN&gWV`yP=WMyV>WaCuNm0Rj{Q6aW -AK2mt$eR#TG(*D?bD00031001KZ0000000000005+c#iR-VaA|NaUv_0~WN&gWV`yP=WMy?y-E^v8JO928D0~7!N00;p4c~(MtBUtcb8c~DCM0u%!j000080Q-4XQ&QQjhKd6K0NM!v02}}S000000 -00000HlE#z6tc!Jc4cm4Z*nhVZ)|UJVQpbAVQzD2E^v8JO928D0~7!N00;p4c~(=)#1YeR3jhEW -DF6T?00000000000001_f!)Ch0B~t=FJE?LZe(wAFJo_PZ*pO6VJ~5Bb7^#McWG`jGA?j=P)h*<6ay3 -h000O8`*~JV8*fE&g8~2mdj|jjA^-pY0000000000q=Apk3IK3va4%nWWo~3|axY_VY;SU5ZDB8IZfS -IBVQgu0WiD`eP)h*<6ay3h000O8`*~JV2pbF$Pz3-092Ecn9RL6T0000000000q=8b<3IK3va4%nWWo -~3|axY_VY;SU5ZDB8WX>KzzE^v8JO928D0~7!N00;p4c~(=RDAwlg1pojh82|tu00000000000001_f -!);#0B~t=FJE?LZe(wAFJo_PZ*pO6VJ~-SZggdGZ7y(mP)h*<6ay3h000O8`*~JVU2hWoT>$_9MFIc- -9{>OV0000000000q=5+B3IK3va4%nWWo~3|axY|Qb98KJVlQ7`X>MtBUtcb8c~DCM0u%!j000080Q-4 -XQ!LZH4qz()02iVF0384T00000000000HlGU-3kD3X>c!Jc4cm4Z*nhWX>)XJX<{#9Z*6d4bS`jtP)h -*<6ay3h000O8`*~JVhCpNd_%8qebH@Mx9{>OV0000000000q=7vN3jlCwa4%nWWo~3|axY|Qb98KJVl -QN2bYWs)b7d}Yc~DCM0u%!j000080Q-4XQ!?pD%+?eD00U6~02}}S00000000000HlF(IST-AX>c!Jc -4cm4Z*nhWX>)XJX<{#FZe(S6E^v8JO928D0~7!N00;p4c~(>1NpBz`GXMbn$^ZZ#00000000000001_ -fr3s80B~t=FJE?LZe(wAFJx(RbZlv2FKlmPVRUbDb1rasP)h*<6ay3h000O8`*~JVc&0UHY!Cnd+c^L -L9{>OV0000000000q=Dgq3jlCwa4%nWWo~3|axY|Qb98KJVlQoBZfRy^b963nc~DCM0u%!j000080Q- -4XQ}nBO_}2yi0DThx03HAU00000000000HlG6k_!NEX>c!Jc4cm4Z*nhWX>)XJX<{#JVRCC_a&sc!Jc4cm4Z*nhWX>)XJX -<{#JWprU=VRT_GaCuNm0Rj{Q6aWAK2mt$eR#Tn5)qt=I002ZP001BW0000000000005+cy}kc!Jc4cm4Z*nhWX>)XJX<{#QHZ(0^a&0bUc -x6ya0Rj{Q6aWAK2mt$eR#SN)bMBJK0001<0RS5S0000000000005+c>eLMYaA|NaUv_0~WN&gWWNCAB -Y-wUIbT%|DWq4&!O928D0~7!N00;p4c~(=jFEei;A&*;@br9smFU0000000000q=B -mE4ghdza4%nWWo~3|axY|Qb98KJVlQ@Oa&u{KZZ2?nP)h*<6ay3h000O8`*~JV92gG9H?RNz0AK+C8v -pt03QGV00000000000HlG`u@3-nX>c!Jc4cm4Z*nhWX>)XJX<{#THZ(0^a&0bUcx6ya0Rj{Q -6aWAK2mt$eR#W>LvixVj0001n0RS5S0000000000005+cSlbW)aA|NaUv_0~WN&gWWNCABY-wUIcQ!O -GWq4&!O928D0~7!N00;p4c~(=j{uZzcDF6V!rvLyP00000000000001_f%uyd0B~t=FJE?LZe(wAFJx -(RbZlv2FL!8VWo#~Rc~DCM0u%!j000080Q-4XQzP$Z{KEhM01^QJ04V?f00000000000HlFE#Ss8-X> -c!Jc4cm4Z*nhWX>)XJX<{#5Vqs%zaBp&SFJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVuZk~a&H(@b% -L4!aB>(^b0000000000q=84q5dd&$a4%nWWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mZE163E^v8JO928D -0~7!N00;p4c~(=#IR?{F8~^}oWB>ps00000000000001_fmp~90B~t=FJE?LZe(wAFJx(RbZlv2FJEF -|V{344a&#|qXmxaHY%XwlP)h*<6ay3h000O8`*~JVZx#Tp_5lC@ISK#(D*ylh0000000000q=D|_5dd -&$a4%nWWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mb9r-PZ*FF3XD(xAXHZK40u%!j000080Q-4XQ~r7Dqp -2PM0On`_04e|g00000000000HlE}=MeyKX>c!Jc4cm4Z*nhWX>)XJX<{#5Vqs%zaBp&SFLQZwV{dL|X -=g5Qc~DCM0u%!j000080Q-4XQ$#`^Ltc!Jc4cm4Z*nhW -X>)XJX<{#5Vqs%zaBp&SFLYsYW@&6?E^v8JO928D0~7!N00;p4c~(=2mv5*<0ssJr1ONaa000000000 -00001_fyQ4F0B~t=FJE?LZe(wAFKBdaY&C3YVlQ7`X>MtBUtcb8c~DCM0u%!j000080Q-4XQ_80;;Vl -#Z09Zi)03iSX00000000000HlFPViEvwX>c!Jc4cm4Z*nhabZu-kY-wUIUukGzbY*yLY%XwlP)h*<6a -y3h000O8`*~JViZ&5F4j%vjVSWGrBme*a0000000000q=B?{5&&>%a4%nWWo~3|axZ9fZEQ7cX<{#5X ->M?JbaQlaWnpbDaCuNm0Rj{Q6aWAK2mt$eR#P<&;B8MG008hT0RSQZ0000000000005+c1eOv2aA|Na -Uv_0~WN&gWXmo9CHEd~OFJE+TYh`X}dS!AhaCuNm0Rj{Q6aWAK2mt$eR#T4CiAmiC002W10015U0000 -000000005+cld}>4aA|NaUv_0~WN&gWXmo9CHEd~OFJE4;YaCuNm0Rj{Q6aWAK2mt$eR#RIt113 -O7000O^0RSNY0000000000005+cthy2aaA|NaUv_0~WN&gWXmo9CHEd~OFJo_Rb97;DbaO6nc~DCM0u -%!j000080Q-4XQ?aYPpWj3P0K&-u03!eZ00000000000HlE{0}}vnX>c!Jc4cm4Z*nhabZu-kY-wUIX -mo9CHE>~ab7gWaaCuNm0Rj{Q6aWAK2mt$eR#S*`+2Ohn0056Y001HY0000000000005+cOGpy{aA|Na -Uv_0~WN&gWXmo9CHEd~OFLPybX<=+>dS!AhaCuNm0Rj{Q6aWAK2mt$eR#VdDT~t~C003bYEXCaCuNm0Rj{Q6aWAK2mt -$eR#O=o=YggH008v^001KZ0000000000005+c<5?2`aA|NaUv_0~WN&gWXmo9CHEd~OFJE+WX=N{8Vq -tS-E^v8JO928D0~7!N00;p4c~( -UK0RtX>c!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bUtei%X>?y-E^v8JO928D0~7!N00;p4c~(<^umlj -o0RRA(0{{Rv00000000000001_fo)zB0B~t=FJE?LZe(wAFKBdaY&C3YVlQTCY;c!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bX>Mv|V{~6_WprU*V`yP= -b7gccaCuNm0Rj{Q6aWAK2mt$eR#TGEWT0#V0027<001Na0000000000005+c!DJHvaA|NaUv_0~WN&g -WXmo9CHEd~OFJ@_MbY*gLFKlUUbS`jtP)h*<6ay3h000O8`*~JV7j>E{4?5a&s?laCB*JZeeV6VP|tLaCuNm0Rj{Q6aWAK2m -t$eR#Q>FVND4b000qb001cf0000000000005+co^KNXaA|NaUv_0~WN&gWXmo9CHEd~OFJ@_MbY*gLF -LPmTX>@6NWpXZXc~DCM0u%!j000080Q-4XQ($uc5A^{60KNnO04e|g00000000000HlHLhZ6vBX>c!J -c4cm4Z*nhabZu-kY-wUIW@&76WpZ;bcW7yJWpi+0V`VOIc~DCM0u%!j000080Q-4XQ=U0-T4wc!Jc4cm4Z*nhabZu-kY-wUIbaG{7VPs)&bY*gLFJE72ZfSI1UoL -QYP)h*<6ay3h000O8`*~JVQj8HPR0041vjzYFD*ylh0000000000q=DUw698~&a4%nWWo~3|axZ9fZE -Q7cX<{#Qa%E*p0PqF?04M+e000000 -00000HlF>juQZIX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7VPs)&bY*gLFLPmdE^v8JO928D0~7!N00;p4 -c~(4R -=a&s?VUukY>bYEXCaCuNm0Rj{Q6aWAK2mt$eR#QGcX#DaH008AU001cf0000000000005+cqLvc?aA| -NaUv_0~WN&gWXmo9CHEd~OFLZKcWny({Y-D9}b1!0Hb7d}Yc~DCM0u%!j000080Q-4XQ&0q_vHu4E0N -o-004M+e00000000000HlH2r4s;fX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7Vs&Y3WMy)5FJy0RE^v8JO -928D0~7!N00;p4c~(4R=a&s?bbaG{7E^v8JO928D0~7!N00;p4c~(=g8MlBL4gdhIIRF4J00000000000001 -_fx5U80B~t=FJE?LZe(wAFKBdaY&C3YVlQ-ZWo2S@X>4R=a&s?bbaG{7Uu<}7Y%XwlP)h*<6ay3h000 -O8`*~JV+jeS&o(2E_R~7&OEC2ui0000000000q=6vE698~&a4%nWWo~3|axZ9fZEQ7cX<{#Qa%E+AVQ -gzbYEXCaCuNm0Rj{Q6aWAK2mt$eR#PD@+Cmox001-{001Ze0000000000005+c2+k7#a -A|NaUv_0~WN&gWXmo9CHEd~OFLZKcWp`n0Yh`kCFJfVHWiD`eP)h*<6ay3h000O8`*~JVWr}a9_W=L^ -g#`crCjbBd0000000000q=9AC698~&a4%nWWo~3|axZ9fZEQ7cX<{#Qa%E+AVQgzc!Jc4cm4Z*nhabZu-kY-w -UIbaG{7cVTR6WpZ;bWpr|7WiD`eP)h*<6ay3h000O8`*~JVFatz%c?JLg)ffN(E&u=k0000000000q= -D1i698~&a4%nWWo~3|axZ9fZEQ7cX<{#Qa%E+AVQgzbYEXCaCuNm0Rj{Q6aWAK2mt$eR#RmaV{t13004ar000>P0000000000005 -+c{^t__aA|NaUv_0~WN&gWX=H9;FJo_HWn(UIc~DCM0u%!j000080Q-4XQ!S&#-OB&~0B8XK02%-Q00 -000000000HlFn>k|NQX>c!Jc4cm4Z*nhbWNu+EV{dJ6VRSBVc~DCM0u%!j000080Q-4XQ&gZqI-Lsu0 -2(p?02lxO00000000000HlFq>=OWRX>c!Jc4cm4Z*nhbWNu+EV{dY0E^v8JO928D0~7!N00;p4c~(<) -7#cEfBLDzyr2qgN00000000000001_fj0OP0B~t=FJE?LZe(wAFKJ|MVJ~T9Zee6$bYU)Vc~DCM0u%! -j000080Q-4XQ!L0`TPFhm0F4I#0384T00000000000HlH68x#O=X>c!Jc4cm4Z*nhbWNu+EX>N3KVQy -z-b1rasP)h*<6ay3h000O8`*~JV>7zsD7XSbN6#xJLAOHXW0000000000q=7*n6aa8(a4%nWWo~3|ax -ZCQZecHQVPk7yXJubxVRT_GaCuNm0Rj{Q6aWAK2mt$eR#Uvk5`-O?004Ou0{|TW0000000000005+cm -LC)VaA|NaUv_0~WN&gWX=H9;FLiWtG&W>mbYU)Vc~DCM0u%!j000080Q-4XQ;jEASl|Hw0A2(D03QGV -00000000000HlHLw-f+yX>c!Jc4cm4Z*nhfb7yd2V{0#8UukY>bYEXCaCuNm0Rj{Q6aWAK2mt$eR#P$ -fug4Yu000yK0018V0000000000005+c3%V2laA|NaUv_0~WN&gWZF6UEVPk7AUv_13b7^mGE^v8JO92 -8D0~7!N00;p4c~(<9JDt<50RR9w1ONab00000000000001_fnK^40B~t=FJE?LZe(wAFKu&YaA9L>FJ -*XRWpH$9Z*FrgaCuNm0Rj{Q6aWAK2mt$eR#S=pt2_V)0077r000^Q0000000000005+cO1u;RaA|NaU -v_0~WN&gWZF6UEVPk7AWq5QhaCuNm0Rj{Q6aWAK2mt$eR#U62=1n9W004@V0018V0000000000005+c -g2NO5aA|NaUv_0~WN&gWZF6UEVPk7AW?^h>Vqs%zE^v8JO928D0~7!N00;p4c~(;<3Bjgi0RRA%0ssI -a00000000000001_f#cv50B~t=FJE?LZe(wAFK}UFYhh<;Zf7rFUtwZzb#z}}E^v8JO928D0~7!N00; -p4c~(;ut&*Vh0002-0RR9Y00000000000001_fr#Q10B~t=FJE?LZe(wAFK}UFYhh<;Zf7rFUukY>bY -EXCaCuNm0Rj{Q6aWAK2mt$eR#O`)I%9|q007`D001KZ0000000000005+cyWVP|P>XD?rEVQzVBX>N6RE^v8JO928D0~7!N00;p4c~(=Z>u=dG2LJ#X5dZ)q00000000000001_ -frRoD0B~t=FJE?LZe(wAFK}UFYhh<;Zf7rFZFO^OY-w(FcrI{xP)h*<6ay3h000O8`*~JVaRPJMmPUvqSFbz^jOa%FQaaCuNm0Rj -{Q6aWAK2mt$eR#R>K>OXr5001W;001BW0000000000005+cp!*a6aA|NaUv_0~WN&gWaA9L>VP|P>XD -@AGa%*LBb1rasP)h*<6ay3h000O8`*~JVZj(V;@&*6^L=pf1B>(^b0000000000q=8um6##H)a4%nWW -o~3|axZXUV{2h&X>MmPa%FLKX>w(4Wo~qHE^v8JO928D0~7!N00;p4c~(<2WSxN$8vp?GcmMz+00000 -000000001_fsPFo0B~t=FJE?LZe(wAFK}UFYhh<;Zf7rZaAjj@W@%+|b1rasP)h*<6ay3h000O8`*~J -V5aMk*l@R~{Vm$x=9RL6T0000000000q=Dfm6##H)a4%nWWo~3|axZXUV{2h&X>MmPbYW+6E^v8JO92 -8D0~7!N00;p4c~(URJD0D=Gj03HAU0000000000 -0HlHFP!#}hX>c!Jc4cm4Z*nhiWpFhyH!ojbX>MtBUtcb8c~DCM0u%!j000080Q-4XQ~N##QYZxg0D%n -v02=@R00000000000HlGNQ567iX>c!Jc4cm4Z*nhiWpFhyH!os!X>4RJaCuNm0Rj{Q6aWAK2mt$eR#S -7On+w7P006`n000{R0000000000005+c{8kkJaA|NaUv_0~WN&gWaAj~cF*h$`Xk}w-E^v8JO928D0~ -7!N00;p4c~(;mu~yKE1^@s85C8xk00000000000001_f%jY$0B~t=FJE?LZe(wAFK}gWH8D3YV{dG4a -%^vBE^v8JO928D0~7!N00;p4c~(=83#T>70RRBy1ONaW00000000000001_fxTlD0B~t=FJE?LZe(wA -FK}gWH8D3YV{dJ6VRSBVc~DCM0u%!j000080Q-4XQ#;SNM$Z8N0BHmO03HAU00000000000HlGyWfcH -$X>c!Jc4cm4Z*nhiWpFhyH!oyqa&&KRY;!Jfc~DCM0u%!j000080Q-4XQxxc!Jc4cm4Z*nhiWpFhyH!o#wc4BpDY-BEQc~DCM0u%!j000080Q-4XQX>c!Jc4cm4Z*nhiWpFhyH!p2vbYU)Vc~DCM0u%!j000080 -Q-4XQx;WthTRMR0Ch9~03HAU00000000000HlE^bQJ(_X>c!Jc4cm4Z*nhiWpFhyH!pW`VQ_F|a&sc!Jc4cm4Z*nhiWpFh -yH!o>!UvP47V`X!5FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVv=buUNDBY}!7Bg&EC2ui00000000 -00q=Br06##H)a4%nWWo~3|axZXYa5XVEFKKRHaB^>BWpi^cUukY%aB^>BWpi^baCuNm0Rj{Q6aWAK2m -t$eR#U)|DV@Y~0094{0RSZc0000000000005+cK8_UtaA|NaUv_0~WN&gWaBF8@a%FRGb#h~6b1z?CX ->MtBUtcb8c~DCM0u%!j000080Q-4XQ(+_n)=L2Z05Spq04D$d00000000000HlFM0u}&pX>c!Jc4cm4 -Z*nhiYiD0_Wpi(Ja${w4FK~G?F=KCSaA9;VaCuNm0Rj{Q6aWAK2mt$eR#WeURQTcq0028O001Na0000 -000000005+c)dLm)aA|NaUv_0~WN&gWaBN|8W^ZzBWNC79FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~ -JVR)sLnTmb+8bOZnZBme*a0000000000q=ESe765Q*a4%nWWo~3|axZXfVRUA1a&2U3a&s?VUu|J&Ze -L$6aCuNm0Rj{Q6aWAK2mt$eR#WZ?Ez&*&005c~001KZ0000000000005+cmkJgDaA|NaUv_0~WN&gWa -BN|8W^ZzBWNC79FJW$Ea&Kv5E^v8JO928D0~7!N00;p4c~(4IUfGI1^@v08UO$w00000000000001_f%p~{0B~t=FJE?LZe(wAFK}#ObY^dIZDeV3b1!vnX? -QMhc~DCM0u%!j000080Q-4XQ-k>iYCQk|08jt`03!eZ00000000000HlHO9Tos^X>c!Jc4cm4Z*nhiY -+-a}Z*py9X>xNfc4cyNX>V>WaCuNm0Rj{Q6aWAK2mt$eR#S!>I518J000mf001KZ0000000000005+c -Zypu^aA|NaUv_0~WN&gWaBN|8W^ZzBWNC79FL!BfWN&wKE^v8JO928D0~7!N00;p4c~(;r+t4op2LJ% -B6aWAq00000000000001_f&L{H0B~t=FJE?LZe(wAFK}{iXL4n8b1z?CX>MtBUtcb8c~DCM0u%!j000 -080Q-4XQ|?3ZU&aIg0DcPq02=@R00000000000HlFFEfxT9X>c!Jc4cm4Z*nhia&KpHWpi^cVqtPFaC -uNm0Rj{Q6aWAK2mt$eR#TCk?2lvw003VK0015U0000000000005+cJu(&maA|NaUv_0~WN&gWaB^>Fa -%FRKFJo_PZ*p@kaCuNm0Rj{Q6aWAK2mt$eR#Q%e)_ffU002z}0018V0000000000005+c-8L2gaA|Na -Uv_0~WN&gWaB^>Fa%FRKFJo_YZggdGE^v8JO928D0~7!N00;p4c~(>8*Y@fz0{{TE1poja000000000 -00001_fj2r90B~t=FJE?LZe(wAFK}{iXL4n8b1!pnX>M+1axQRrP)h*<6ay3h000O8`*~JVxf(FQYzF -`U`4a#DAOHXW0000000000q=BM6765Q*a4%nWWo~3|axZdaadl;LbaO9XUukY>bYEXCaCuNm0Rj{Q6a -WAK2mt$eR#VF+@nr@9006lG001KZ0000000000005+cOhpy|aA|NaUv_0~WN&gWa%FLKWpi|MFJE7FW -pZEK~phAOHX -W0000000000q=D>6765Q*a4%nWWo~3|axZdaadl;LbaO9ZWMOc0WpZ;aaCuNm0Rj{Q6aWAK2mt$eR#O -71vL9&%0006R000{R0000000000005+cwp|tgaA|NaUv_0~WN&gWa%FLKWpi|MFJW+LE^v8JO928D0~ -7!N00;p4c~(=qw23Qo3jhG$CjbB(00000000000001_fmmb~0B~t=FJE?LZe(wAFLGsZb!BsOb1z|ab -Z9Pcc~DCM0u%!j000080Q-4XQ-ZRh&n^J~0MP*e0384T00000000000HlEha25b?X>c!Jc4cm4Z*nhk -WpQ<7b98erV`Xx5b1rasP)h*<6ay3h000O8`*~JVuV#XDOV000 -0000000q=Alf765Q*a4%nWWo~3|axZdaadl;LbaO9bZ*Oa9WpgfYc~DCM0u%!j000080Q-4XQ)i?&gh -vDb0J01K03rYY00000000000HlG7h!y~FX>c!Jc4cm4Z*nhkWpQ<7b98erWq4y{aCB*JZgVbhc~DCM0 -u%!j000080Q-4XQ(+b-xsC(?0E7c!Jc4cm4Z*nhkWpQ<7b98er -Xk~10E^v8JO928D0~7!N00;p4c~(;!N^{|P0RRB?0ssIV00000000000001_f!dK40B~t=FJE?LZe(w -AFLGsZb!BsOb1!IbZ)?y-E^v8J -O928D0~7!N00;p4c~(;&WxEeF2LJ%@761Sv00000000000001_fx8hG0B~t=FJE?LZe(wAFLGsbZ)|p -DY-wUIaB^>UX=G(`b1rasP)h*<6ay3h000O8`*~JV%K=~A>Hz=%R0RM4BLDyZ0000000000q=7IQ7XW -Z+a4%nWWo~3|axZdab8l>RWo&6;FLGsYZ*p{Ha&ss60E9#U03 -!eZ00000000000HlFi8y5g@X>c!Jc4cm4Z*nhkWpi(Ac4cg7VlQ%Kb8l>RWpZ;aaCuNm0Rj{Q6aWAK2 -mt$eR#Q~m&~Q@)006oY001EX0000000000005+c&Mg-JaA|NaUv_0~WN&gWa%FRGY<6XAX<{#PbaHiL -baO6nc~DCM0u%!j000080Q-4XQvd(}00IC20000004V?f00000000000HlFnGZz4GX>c!Jc4cm4Z*nh -kWpi(Ac4cg7VlQKFZE#_9FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV8`mypV*mgEoB#j-FaQ7m000 -0000000q=Bh37XWZ+a4%nWWo~3|axZdab8l>RWo&6;FJo_QaA9;WV{dG1Wn*+{Z*Fs6VPa!0aCuNm0R -j{Q6aWAK2mt$eR#WC47Qf{a002=(001BW0000000000005+cS~M2`aA|NaUv_0~WN&gWbY*T~V`+4GF -JE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JV8Qg7btpor7@(cg~AOHXW0000000000q=9`%7XWZ+a4%nW -Wo~3|axZjcZee3-ba^jdVRLzIV`*4;YaCuNm0Rj{Q6aWAK2mt$eR#T|q81C`{007 -tp0012T0000000000005+cAyF3qaA|NaUv_0~WN&gWbY*T~V`+4GFJWeMWpXZXc~DCM0u%!j000080Q --4XQ_y|osl5UK0AK|G03HAU00000000000HlFVR2KknX>c!Jc4cm4Z*nhmWo}_(X>@rnVr6D;a%C=Xc -~DCM0u%!j000080Q-4XQv?g`YhnWc0CWcc03-ka00000000000HlFOR~Gc!Jc4cm4Z*nhmWo}_( -X>@rnVr6D;a%Eq0Y-MF|E^v8JO928D0~7!N00;p4c~(=VY_ikb0ssJK1pojW00000000000001_f$Lf -q0B~t=FJE?LZe(wAFLY&YVPk1@c`t5Za4v9pP)h*<6ay3h000O8`*~JVm{Kjfv;_bF^%(#F9RL6T000 -0000000q=5il7XWZ+a4%nWWo~3|axZjcZee3-ba^jwWpr|RE^v8JO928D0~7!N00;p4c~(>PR?T&(0{ -{T#3IG5c00000000000001_f$w7%0B~t=FJE?LZe(wAFLY&YVPk1@c`tKxZ*VSfc~DCM0u%!j000080 -Q-4XQytzSIjjQ!0AUCK03rYY00000000000HlG^XBPl)X>c!Jc4cm4Z*nhmWo}_(X>@rnbZ>HQVPtQ2 -WnwOHc~DCM0u%!j000080Q-4XQ(9$!S(69=03#Xz02}}S00000000000HlGwYZm};X>c!Jc4cm4Z*nh -mWo}_(X>@rncVTICE^v8JO928D0~7!N00;p4c~(=s(5w5a0002y0000T00000000000001_fs1q(0B~ -t=FJE?LZe(wAFLZBhY-ulFUukY>bYEXCaCuNm0Rj{Q6aWAK2mt$eR#V0R{PsW<0056y000~S0000000 -000005+cadj5}aA|NaUv_0~WN&gWbZ>2JX)j-JVRCb2axQRrP)h*<6ay3h000O8`*~JVg4{8H&I14dc -?tjk7ytkO0000000000q=D;-7XWZ+a4%nWWo~3|axZjmZER^TUvgzGaCuNm0Rj{Q6aWAK2mt$eR#PEK -Z5&Yq007Gh0018V0000000000005+c?~WG$aA|NaUv_0~WN&gWb#iQMX<{=kUtei%X>?y-E^v8JO928 -D0~7!N00;p4c~(;$KNE5S4FCW;DgXc@00000000000001_fqjz~0B~t=FJE?LZe(wAFLiQkY-wUMFJE -JCY;0v?bZKvHb1rasP)h*<6ay3h000O8`*~JVTd^H-KL7v#KL7v#9{>OV0000000000q=CSo7XWZ+a4 -%nWWo~3|axZmqY;0*_GcR9uWpZc!Jc4cm4Z*nhna%^mAVlyveZ*Fd7V{~b6ZZ2?nP)h*<6ay3h000O8`*~JVo&cQ- -MkoLP(~(^b0000000000q=AOG7XWZ+a4%nWWo~3|axZmqY;0*_GcRLrZf<2`bZKvHaBpvHE^v8 -JO928D0~7!N00;p4c~(<{YAOHX%00000000000001_fe+yq0B~t=FJE?LZe(wAFLiQkY-w -UMFJ*XRWpH$9Z*FrgaCuNm0Rj{Q6aWAK2mt$eR#V3)ODrG?004s_0012T0000000000005+cIqMeyaA -|NaUv_0~WN&gWb#iQMX<{=kW@%+?WOFWXc~DCM0u%!j000080Q-4XQ-`a+qkaPb0Eh_y03QGV000000 -00000HlGG^%nqeX>c!Jc4cm4Z*nhna%^mAVlyvhX>4V1Z*z1maCuNm0Rj{Q6aWAK2mt$eR#WJrk&9>+ -001*h001HY0000000000005+cPx%)BaA|NaUv_0~WN&gWb#iQMX<{=kaBpvHZDDR001j)0018V0000000000005+c+7}oAaA|NaUv_0~WN -&gWb#iQMX<{=ka%FRHZ*FsCE^v8JO928D0~7!N00;p4c~(;Z00002000000000d00000000000001_f -qFF<0B~t=FJE?LZe(wAFLiQkY-wUMFJo_RbaH88FJE72ZfSI1UoLQYP)h*<6ay3h000O8`*~JVDud}2 -wE+MCy#oLMF#rGn0000000000q=CUT7yxi-a4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQ7|aByXAXK8L -_UuAA~X>xCFE^v8JO928D0~7!N00;p4c~(<6WeLe=3;+NcD*yl}00000000000001_fyFl%0B~t=FJE -?LZe(wAFLiQkY-wUMFJo_RbaH88FJW+SWo~C_Ze=cTc~DCM0u%!j000080Q-4XQ@F=N{!|740J;$X04 -D$d00000000000HlF(L>K^YX>c!Jc4cm4Z*nhna%^mAVlyveZ*FvQX<{#KbZl*KZ*OcaaCuNm0Rj{Q6 -aWAK2mt$eR#RZPnqGkv000C+001Ze0000000000005+c3riRPaA|NaUv_0~WN&gWb#iQMX<{=kV{dMB -a%o~OaCvWVWo~nGY%XwlP)h*<6ay3h000O8`*~JV!0)=_!6g6yk%j;OE&u=k0000000000q=C|37yxi --a4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQ)LV|8+6baG*Cb8v5RbS`jtP)h*<6ay3h000O8`*~JV%Tm -ZcSqK0Cxf=igBme*a0000000000q=Das7yxi-a4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQ)VV{3CRaC -uNm0Rj{Q6aWAK2mt$eR#N}~0006200000001}u0000000000005+cdX5+XaA|NaUv_0~WN&gWb#iQMX -<{=kV{dMBa%o~OUvp(+b#i5Na$#Md`ZfA2YaCuNm0Rj{Q6aWAK2mt$eR#UDjr@UDa003e(0021v0000000000005+cAfOlkaA| -NaUv_0~WN&gWb#iQMX<{=kV{dMBa%o~OUvp(+b#i5Na$#;0RtWW>|0BisN04M+e00000000000HlG?u^0ewX>c!Jc4cm4Z*nhna%^mAVlyvrVPk7yX -JvCQUtei%X>?y-E^v8JO928D0~7!N00;p4c~(>3X^-9}ApihshX4R000000000000001_fo8H80B~t= -FJE?LZe(wAFLiQkY-wUMFK}UFYhh<)b1!pgcrI{xP)h*<6ay3h000O8`*~JV000000ssI200000G5`P -o0000000000q=C)T7yxi-a4%nWWo~3|axZmqY;0*_GcRyqV{2h&WpgiLVPk7>Z*p{VFJE72ZfSI1UoL -QYP)h*<6ay3h000O8`*~JV-ne74NCE%=i3I=vG5`Po0000000000q=6sQ7yxi-a4%nWWo~3|axZmqY; -0*_GcRyqV{2h&WpgiLVPk7>Z*p{VFKuCKWoBt?WiD`eP)h*<6ay3h000O8`*~JV1rwupqyYc`p925@I -{*Lx0000000000q=C2A7yxi-a4%nWWo~3|axZmqY;0*_GcRyqV{2h&Wpgicb8KI2VRU0?UubW0bZ%j7 -WiMY}X>MtBUtcb8c~DCM0u%!j000080Q-4XQ$B=C*0Bfx0528*073u&00000000000HlGm*cbqCX>c! -Jc4cm4Z*nhna%^mAVlyvrVPk7yXJvCQb8~E8ZDDj{XkTb=b98QDZDlWCX>D+9Wo>0{bYXO9Z*DGdc~D -CM0u%!j000080Q-4XQ|5}9n%DsV0D}Yo03-ka00000000000HlG%;TQmLX>c!Jc4cm4Z*nhna%^mAVl -yvwbZKlaUtei%X>?y-E^v8JO928D0~7!N00;p4c~(<%5t5pw2LJ##6951v00000000000001_f#2g80 -B~t=FJE?LZe(wAFLiQkY-wUMFLiWjY%gPPZf<2`bZKvHE^v8JO928D0~7!N00;p4c~(;j&s+yy0ssI- -1^@sd00000000000001_fywI_0B~t=FJE?LZe(wAFLiQkY-wUMFLiWjY%g$fZ+LkwaCuNm0Rj{Q6aWA -K2mt$eR#Q#`_MiL!008m<001EX0000000000005+cX6_gOaA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFL8 -Bcb!9Gac~DCM0u%!j000080Q-4XQ<7w_RO$r)02>eh03!eZ00000000000HlGT?-&4ZX>c!Jc4cm4Z* -nhna%^mAVlyvwbZKlaa%FLKWpi{caCuNm0Rj{Q6aWAK2mt$eR#P@8)wEy*006cP001Na00000000000 -05+c%=H)maA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFLGsbaBpsNWiD`eP)h*<6ay3h000O8`*~JVmFNkb -&=vpyk5d2uApigX0000000000q=9bx7yxi-a4%nWWo~3|axZmqY;0*_GcR>?X>2cYWpr|RE^v8JO928 -D0~7!N00;p4c~(=TlKaP}761SlLjV9E00000000000001_fqfDg0B~t=FJE?LZe(wAFLiQkY-wUMFLi -WjY%gc!Jc4 -cm4Z*nhna%^mAVlyvwbZKlab8~ETa$#<2c3jhEUCjbB=0 -0000000000001_fweIi0B~t=FJE?LZe(wAFLiQkY-wUMFLiWjY%g?aZDntDbS`jtP)h*<6ay3h000O8 -`*~JV=c|?Zr4j%D-!=dM9{>OV0000000000q=B|Q831r;a4%nWWo~3|axZmqY;0*_GcR>?X>2cba%?V -ec~DCM0u%!j000080Q-4XQ=&${ali)v02~zn03ZMW00000000000HlGKP#FMlX>c!Jc4cm4Z*nhna%^ -mAVlyvwbZKlacVTICE^v8JO928D0~7!N00;p4c~(<+&>wL!3jhF9DF6T@00000000000001_ftFYq0B -~t=FJE?LZe(wAFLz~PWo~0{WNB_^b1z?CX>MtBUtcb8c~DCM0u%!j000080Q-4XQ^&0civc!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2ZY++($Y;!Jfc~DCM0u%!j00008 -0Q-4XQ&4{~A4CEG02u`U03-ka00000000000HlFWY8e1c!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2ZZEI{ -{Vr6V|E^v8JO928D0~7!N00;p4c~(Mn8FL+;db7gX0WMyV)Ze?UHaCuNm1qJ{B005Z*nE_CM0 -06po82|tP -""" - - -if __name__ == "__main__": - main() diff --git a/src/client/common/process/internal/scripts/index.ts b/src/client/common/process/internal/scripts/index.ts index f719844ef80b..2644fd0d00ff 100644 --- a/src/client/common/process/internal/scripts/index.ts +++ b/src/client/common/process/internal/scripts/index.ts @@ -25,9 +25,6 @@ const SCRIPTS_DIR = _SCRIPTS_DIR; // In some cases one or more types related to a script are exported // from the same module in which the script's function is located. // These types typically relate to the return type of "parse()". -// -// ignored scripts: -// * install_debugpy.py (used only for extension development) export * as testingTools from './testing_tools'; // interpreterInfo.py From d63fb1825fa03297581fffd06829dd513ee42651 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 15:42:40 -0700 Subject: [PATCH 0077/1136] Bump brettcannon/check-for-changed-files from 1.1.0 to 1.1.1 (#21607) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [brettcannon/check-for-changed-files](https://github.com/brettcannon/check-for-changed-files) from 1.1.0 to 1.1.1.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=brettcannon/check-for-changed-files&package-manager=github_actions&previous-version=1.1.0&new-version=1.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-file-check.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index c0bf09f2cd24..2d227ea00399 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'package-lock.json matches package.json' - uses: brettcannon/check-for-changed-files@v1.1.0 + uses: brettcannon/check-for-changed-files@v1.1.1 with: prereq-pattern: 'package.json' file-pattern: 'package-lock.json' @@ -25,7 +25,7 @@ jobs: failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - name: 'package.json matches package-lock.json' - uses: brettcannon/check-for-changed-files@v1.1.0 + uses: brettcannon/check-for-changed-files@v1.1.1 with: prereq-pattern: 'package-lock.json' file-pattern: 'package.json' @@ -33,7 +33,7 @@ jobs: failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - name: 'Tests' - uses: brettcannon/check-for-changed-files@v1.1.0 + uses: brettcannon/check-for-changed-files@v1.1.1 with: prereq-pattern: src/**/*.ts file-pattern: | From 0c4a156c6f88710a35142d3fe0463ab84ec0463a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 15:43:06 -0700 Subject: [PATCH 0078/1136] Bump mheap/github-action-required-labels from 4 to 5 (#21606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mheap/github-action-required-labels](https://github.com/mheap/github-action-required-labels) from 4 to 5.
Release notes

Sourced from mheap/github-action-required-labels's releases.

v5

Tag that always points to the latest commit in the v5.x.x series of releases

v4.1.1

  • Fix build step by switching to ubuntu-latest

Full Changelog: https://github.com/mheap/github-action-required-labels/compare/v4.1.0...v4.1.1

v4.1.0

What's Changed

New Contributors

Full Changelog: https://github.com/mheap/github-action-required-labels/compare/v4.0.0...v4.1.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=mheap/github-action-required-labels&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index e953f62d2011..730b8e5c5832 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'PR impact specified' - uses: mheap/github-action-required-labels@v4 + uses: mheap/github-action-required-labels@v5 with: mode: exactly count: 1 From 8099383e6c7905bc6a4ee61e26061a62283a1844 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 15:43:33 -0700 Subject: [PATCH 0079/1136] Bump actions/checkout from 2 to 3 (#21577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
Release notes

Sourced from actions/checkout's releases.

v3.0.0

  • Updated to the node16 runtime by default
    • This requires a minimum Actions Runner version of v2.285.0 to run, which is by default available in GHES 3.4 or later.

v2.7.0

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v2.6.0...v2.7.0

v2.6.0

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v2.5.0...v2.6.0

v2.5.0

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v2...v2.5.0

v2.4.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v2...v2.4.2

v2.4.1

  • Fixed an issue where checkout failed to run in container jobs due to the new git setting safe.directory

v2.4.0

  • Convert SSH URLs like org-<ORG_ID>@github.com: to https://github.com/ - pr

v2.3.5

Update dependencies

v2.3.4

v2.3.3

... (truncated)

Changelog

Sourced from actions/checkout's changelog.

Changelog

v3.5.3

v3.5.2

v3.5.1

v3.5.0

v3.4.0

v3.3.0

v3.2.0

v3.1.0

v3.0.2

v3.0.1

v3.0.0

v2.3.1

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/info-needed-closer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/info-needed-closer.yml b/.github/workflows/info-needed-closer.yml index 07104d6bc4be..c0b130be803b 100644 --- a/.github/workflows/info-needed-closer.yml +++ b/.github/workflows/info-needed-closer.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions From 0cc5b18b7fba25302bcea9ecc68ec9d70cdc792c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 12 Jul 2023 09:01:22 -0700 Subject: [PATCH 0080/1136] handle skipped marker on class level (#21612) fixes https://github.com/microsoft/vscode-python/issues/21579 --- .../tests/pytestadapter/.data/skip_tests.py | 10 +++ .../expected_execution_test_output.py | 17 ++++ .../tests/pytestadapter/test_execution.py | 2 + pythonFiles/vscode_pytest/__init__.py | 82 +++++++++++++------ 4 files changed, 86 insertions(+), 25 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/.data/skip_tests.py b/pythonFiles/tests/pytestadapter/.data/skip_tests.py index 113e3506932a..871b0e7bf5c3 100644 --- a/pythonFiles/tests/pytestadapter/.data/skip_tests.py +++ b/pythonFiles/tests/pytestadapter/.data/skip_tests.py @@ -28,3 +28,13 @@ def test_decorator_thing(): def test_decorator_thing_2(): # Skip this test as well, with a reason. This one uses a decorator with a condition. assert True + + +# With this test, the entire class is skipped. +@pytest.mark.skip(reason="Skip TestClass") +class TestClass: + def test_class_function_a(self): # test_marker--test_class_function_a + assert True + + def test_class_function_b(self): # test_marker--test_class_function_b + assert False diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index 3d24f036fe2a..92b881fdc8b8 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -169,6 +169,9 @@ # └── test_another_thing: skipped # └── test_decorator_thing: skipped # └── test_decorator_thing_2: skipped +# ├── TestClass +# │ └── test_class_function_a: skipped +# │ └── test_class_function_b: skipped skip_tests_execution_expected_output = { "skip_tests.py::test_something": { "test": "skip_tests.py::test_something", @@ -198,6 +201,20 @@ "traceback": None, "subtest": None, }, + "skip_tests.py::TestClass::test_class_function_a": { + "test": "skip_tests.py::TestClass::test_class_function_a", + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, + "skip_tests.py::TestClass::test_class_function_b": { + "test": "skip_tests.py::TestClass::test_class_function_b", + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, } diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index f147a0462f38..8f5fa191e38c 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -62,6 +62,8 @@ def test_bad_id_error_execution(): "skip_tests.py::test_another_thing", "skip_tests.py::test_decorator_thing", "skip_tests.py::test_decorator_thing_2", + "skip_tests.py::TestClass::test_class_function_a", + "skip_tests.py::TestClass::test_class_function_b", ], expected_execution_test_output.skip_tests_execution_expected_output, ), diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index b14a79aef7fd..20b3dba325ef 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -207,32 +207,64 @@ def pytest_report_teststatus(report, config): def pytest_runtest_protocol(item, nextitem): + skipped = check_skipped_wrapper(item) + if skipped: + node_id = str(item.nodeid) + report_value = "skipped" + cwd = pathlib.Path.cwd() + if node_id not in collected_tests_so_far: + collected_tests_so_far.append(node_id) + item_result = create_test_outcome( + node_id, + report_value, + None, + None, + ) + collected_test = testRunResultDict() + collected_test[node_id] = item_result + execution_post( + os.fsdecode(cwd), + "success", + collected_test if collected_test else None, + ) + + +def check_skipped_wrapper(item): + """A function that checks if a test is skipped or not by check its markers and its parent markers. + + Returns True if the test is marked as skipped at any level, False otherwise. + + Keyword arguments: + item -- the pytest item object. + """ if item.own_markers: - for marker in item.own_markers: - # If the test is marked with skip then it will not hit the pytest_report_teststatus hook, - # therefore we need to handle it as skipped here. - skip_condition = False - if marker.name == "skipif": - skip_condition = any(marker.args) - if marker.name == "skip" or skip_condition: - node_id = str(item.nodeid) - report_value = "skipped" - cwd = pathlib.Path.cwd() - if node_id not in collected_tests_so_far: - collected_tests_so_far.append(node_id) - item_result = create_test_outcome( - node_id, - report_value, - None, - None, - ) - collected_test = testRunResultDict() - collected_test[node_id] = item_result - execution_post( - os.fsdecode(cwd), - "success", - collected_test if collected_test else None, - ) + if check_skipped_condition(item): + return True + parent = item.parent + while isinstance(parent, pytest.Class): + if parent.own_markers: + if check_skipped_condition(parent): + return True + parent = parent.parent + return False + + +def check_skipped_condition(item): + """A helper function that checks if a item has a skip or a true skip condition. + + Keyword arguments: + item -- the pytest item object. + """ + + for marker in item.own_markers: + # If the test is marked with skip then it will not hit the pytest_report_teststatus hook, + # therefore we need to handle it as skipped here. + skip_condition = False + if marker.name == "skipif": + skip_condition = any(marker.args) + if marker.name == "skip" or skip_condition: + return True + return False def pytest_sessionfinish(session, exitstatus): From 0beefa093c49e7321ac1efad1d39746d49e3b47a Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 12 Jul 2023 12:34:12 -0700 Subject: [PATCH 0081/1136] Bump semver (#21618) --- package-lock.json | 144 +++++++++++++++++++++++----------------------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index e6042d7638d8..3678852d1f65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1851,9 +1851,9 @@ } }, "node_modules/@vscode/vsce/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -2694,9 +2694,9 @@ } }, "node_modules/async-listener/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "bin": { "semver": "bin/semver" } @@ -3953,9 +3953,9 @@ } }, "node_modules/cls-hooked/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "bin": { "semver": "bin/semver" } @@ -4275,9 +4275,9 @@ } }, "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -4787,9 +4787,9 @@ } }, "node_modules/diagnostic-channel/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "bin": { "semver": "bin/semver" } @@ -4911,9 +4911,9 @@ } }, "node_modules/download/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -5600,9 +5600,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -8526,9 +8526,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -9259,9 +9259,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -10120,9 +10120,9 @@ } }, "node_modules/nock/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -10436,9 +10436,9 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -11145,9 +11145,9 @@ } }, "node_modules/parse-semver/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -16751,9 +16751,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "tmp": { @@ -17421,9 +17421,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" } } }, @@ -18405,9 +18405,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" } } }, @@ -18685,9 +18685,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "which": { @@ -19094,9 +19094,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" } } }, @@ -19201,9 +19201,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -19935,9 +19935,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -21994,9 +21994,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -22584,9 +22584,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -23259,9 +23259,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -23518,9 +23518,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -24062,9 +24062,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } From 5ccb73187ad24f2c29a55f20033c9c813c1ed606 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 12 Jul 2023 15:12:41 -0700 Subject: [PATCH 0082/1136] delete run messages (#21620) remove some printing during test run. --- src/client/testing/testController/common/resultResolver.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 8e08ffbeedf5..d9b0a0bde015 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -139,7 +139,6 @@ export class PythonResultResolver implements ITestResultResolver { if (indiItem.id === grabVSid) { if (indiItem.uri && indiItem.range) { runInstance.passed(grabTestItem); - runInstance.appendOutput('Passed here'); } } }); @@ -152,7 +151,6 @@ export class PythonResultResolver implements ITestResultResolver { if (indiItem.id === grabVSid) { if (indiItem.uri && indiItem.range) { runInstance.skipped(grabTestItem); - runInstance.appendOutput('Skipped here'); } } }); From 0b30aaa2e44478bb69ed5c4c2a7d857106417c35 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 13 Jul 2023 12:56:54 -0700 Subject: [PATCH 0083/1136] display errors and tests on discovery (#21629) fixes https://github.com/microsoft/vscode-python/issues/13301 --- .../testController/common/resultResolver.ts | 21 +++---- .../resultResolver.unit.test.ts | 58 ++++++++++++++++++- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index d9b0a0bde015..320c00bf6ad0 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -65,18 +65,19 @@ export class PythonResultResolver implements ITestResultResolver { } errorNode.error = message; } else { - // Remove the error node if necessary, - // then parse and insert test data. + // remove error node only if no errors exist. this.testController.items.delete(`DiscoveryError:${workspacePath}`); + } + if (rawTestData.tests) { + // if any tests exist, they should be populated in the test tree, regardless of whether there were errors or not. + // parse and insert test data. - if (rawTestData.tests) { - // If the test root for this folder exists: Workspace refresh, update its children. - // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. - populateTestTree(this.testController, rawTestData.tests, undefined, this, token); - } else { - // Delete everything from the test controller. - this.testController.items.replace([]); - } + // If the test root for this folder exists: Workspace refresh, update its children. + // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. + populateTestTree(this.testController, rawTestData.tests, undefined, this, token); + } else { + // Delete everything from the test controller. + this.testController.items.replace([]); } sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts index 0fc7eb3ab353..30b2ccecf513 100644 --- a/src/test/testing/testController/resultResolver.unit.test.ts +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -101,8 +101,7 @@ suite('Result Resolver tests', () => { cancelationToken, // token ); }); - // what about if the error node already exists: this.testController.items.get(`DiscoveryError:${workspacePath}`); - test('resolveDiscovery should create error node on error with correct params', async () => { + test('resolveDiscovery should create error node on error with correct params and no root node with tests in payload', async () => { // test specific constants used expected values testProvider = 'pytest'; workspaceUri = Uri.file('/foo/bar'); @@ -136,6 +135,61 @@ suite('Result Resolver tests', () => { // header of createErrorTestItem is (options: ErrorTestItemOptions, testController: TestController, uri: Uri) sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); }); + test('resolveDiscovery should create error and root node when error and tests exist on payload', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const errorMessage = 'error msg A'; + const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`; + + // create test result node + const tests: DiscoveredTestNode = { + path: 'path', + name: 'name', + type_: 'folder', + id_: 'id', + children: [], + }; + // stub out return values of functions called in resolveDiscovery + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: [errorMessage], + tests, + }; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + + // stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + + // stub out functionality of populateTestTreeStub which is called in resolveDiscovery + const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); + // call resolve discovery + resultResolver.resolveDiscovery(payload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // builds an error node root + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider); + // builds an error item + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + + // also calls populateTestTree with the discovery test results + sinon.assert.calledWithMatch( + populateTestTreeStub, + testController, // testController + tests, // testTreeData + undefined, // testRoot + resultResolver, // resultResolver + cancelationToken, // token + ); + }); }); suite('Test execution result resolver', () => { let resultResolver: ResultResolver.PythonResultResolver; From b454189cad5d24a8c93a4bc220f1d4c070b0c827 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 13 Jul 2023 15:33:00 -0700 Subject: [PATCH 0084/1136] Fix for CodeQL errors (#21613) --- src/test/mocks/vsc/extHostedTypes.ts | 2 +- src/test/mocks/vsc/index.ts | 2 +- .../common/environmentIdentifier.unit.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/mocks/vsc/extHostedTypes.ts b/src/test/mocks/vsc/extHostedTypes.ts index 4921b24629d1..f87b50174150 100644 --- a/src/test/mocks/vsc/extHostedTypes.ts +++ b/src/test/mocks/vsc/extHostedTypes.ts @@ -775,7 +775,7 @@ export class SnippetString { this._tabstop = nested._tabstop; defaultValue = nested.value; } else if (typeof defaultValue === 'string') { - defaultValue = defaultValue.replace(/\$|}/g, '\\$&'); + defaultValue = defaultValue.replace(/\$|}/g, '\\$&'); // CodeQL [SM02383] don't escape backslashes here (by design) } this.value += '${'; diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts index 092fc67da6c6..7678bef4e53c 100644 --- a/src/test/mocks/vsc/index.ts +++ b/src/test/mocks/vsc/index.ts @@ -285,7 +285,7 @@ export class MarkdownString { // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash this.value += (this.supportThemeIcons ? escapeCodicons(value) : value) .replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&') - .replace(/\n/, '\n\n'); + .replace(/\n/g, '\n\n'); return this; } diff --git a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts index 6aeb320a0b11..af719c3e40ed 100644 --- a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts @@ -148,7 +148,7 @@ suite('Environment Identifier', () => { test(`Path using forward slashes (${exe})`, async () => { const interpreterPath = path .join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe) - .replace('\\', '/'); + .replace(/\\/g, '/'); const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); }); From 65a761f36990eb2397453e106f6cff2d075dd8e6 Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Thu, 13 Jul 2023 16:12:22 -0700 Subject: [PATCH 0085/1136] Add deprecation warning for linting and formatting settings (#21585) For https://github.com/microsoft/vscode-python/issues/21390 --- package.json | 194 +++++++++++++++++++++++++++++++++++------------ package.nls.json | 96 +++++++++++++++++++++++ 2 files changed, 241 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index dfd6a640b3c4..0ef0ff4d79b6 100644 --- a/package.json +++ b/package.json @@ -588,13 +588,17 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.formatting.autopep8Args.markdownDeprecationMessage%", + "deprecationMessage": "%python.formatting.autopep8Args.deprecationMessage%" }, "python.formatting.autopep8Path": { "default": "autopep8", "description": "%python.formatting.autopep8Path.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.formatting.autopep8Path.markdownDeprecationMessage%", + "deprecationMessage": "%python.formatting.autopep8Path.deprecationMessage%" }, "python.formatting.blackArgs": { "default": [], @@ -603,13 +607,17 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.formatting.blackArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.formatting.blackArgs.deprecationMessage%" }, "python.formatting.blackPath": { "default": "black", "description": "%python.formatting.blackPath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.formatting.blackPath.markdownDeprecationMessage%", + "deprecationMessage": "%python.formatting.blackPath.deprecationMessage%" }, "python.formatting.provider": { "default": "autopep8", @@ -621,7 +629,9 @@ "yapf" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.formatting.provider.markdownDeprecationMessage%", + "deprecationMessage": "%python.formatting.provider.deprecationMessage%" }, "python.formatting.yapfArgs": { "default": [], @@ -630,13 +640,17 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.formatting.yapfArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.formatting.yapfArgs.deprecationMessage%" }, "python.formatting.yapfPath": { "default": "yapf", "description": "%python.formatting.yapfPath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.formatting.yapfPath.markdownDeprecationMessage%", + "deprecationMessage": "%python.formatting.yapfPath.deprecationMessage%" }, "python.globalModuleInstallation": { "default": false, @@ -669,31 +683,41 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.banditArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.banditArgs.deprecationMessage%" }, "python.linting.banditEnabled": { "default": false, "description": "%python.linting.banditEnabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.banditArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.banditArgs.deprecationMessage%" }, "python.linting.banditPath": { "default": "bandit", "description": "%python.linting.banditPath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.banditPath.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.banditPath.deprecationMessage%" }, "python.linting.cwd": { "default": null, "description": "%python.linting.cwd.description%", "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.cwd.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.cwd.deprecationMessage%" }, "python.linting.enabled": { "default": true, "description": "%python.linting.enabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.enabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.enabled.deprecationMessage%" }, "python.linting.flake8Args": { "default": [], @@ -702,7 +726,9 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.flake8Args.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.flake8Args.deprecationMessage%" }, "python.linting.flake8CategorySeverity.E": { "default": "Error", @@ -714,7 +740,9 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.flake8CategorySeverity.E.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.flake8CategorySeverity.E.deprecationMessage%" }, "python.linting.flake8CategorySeverity.F": { "default": "Error", @@ -726,7 +754,9 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.flake8CategorySeverity.F.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.flake8CategorySeverity.F.deprecationMessage%" }, "python.interpreter.infoVisibility": { "default": "onPythonRelated", @@ -754,19 +784,25 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.flake8CategorySeverity.W.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.flake8CategorySeverity.W.deprecationMessage%" }, "python.linting.flake8Enabled": { "default": false, "description": "%python.linting.flake8Enabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.flake8Enabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.flake8Enabled.deprecationMessage%" }, "python.linting.flake8Path": { "default": "flake8", "description": "%python.linting.flake8Path.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.flake8Path.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.flake8Path.deprecationMessage%" }, "python.linting.ignorePatterns": { "default": [ @@ -779,19 +815,25 @@ }, "scope": "resource", "type": "array", - "uniqueItems": true + "uniqueItems": true, + "markdownDeprecationMessage": "%python.linting.ignorePatterns.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.ignorePatterns.deprecationMessage%" }, "python.linting.lintOnSave": { "default": true, "description": "%python.linting.lintOnSave.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.lintOnSave.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.lintOnSave.deprecationMessage%" }, "python.linting.maxNumberOfProblems": { "default": 100, "description": "%python.linting.maxNumberOfProblems.description%", "scope": "resource", - "type": "number" + "type": "number", + "markdownDeprecationMessage": "%python.linting.maxNumberOfProblems.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.maxNumberOfProblems.deprecationMessage%" }, "python.linting.mypyArgs": { "default": [ @@ -805,7 +847,9 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.mypyArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.mypyArgs.deprecationMessage%" }, "python.linting.mypyCategorySeverity.error": { "default": "Error", @@ -817,11 +861,13 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.mypyCategorySeverity.error.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.mypyCategorySeverity.error.deprecationMessage%" }, "python.linting.mypyCategorySeverity.note": { "default": "Information", - "description": "%python.linting.mypyCategorySeverity.note.description%.", + "description": "%python.linting.mypyCategorySeverity.note.description%", "enum": [ "Error", "Hint", @@ -829,19 +875,25 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.mypyCategorySeverity.note.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.mypyCategorySeverity.note.deprecationMessage%" }, "python.linting.mypyEnabled": { "default": false, "description": "%python.linting.mypyEnabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.mypyEnabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.mypyEnabled.deprecationMessage%" }, "python.linting.mypyPath": { "default": "mypy", "description": "%python.linting.mypyPath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.mypyPath.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.mypyPath.deprecationMessage%" }, "python.linting.prospectorArgs": { "default": [], @@ -850,19 +902,25 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.prospectorArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.prospectorArgs.deprecationMessage%" }, "python.linting.prospectorEnabled": { "default": false, "description": "%python.linting.prospectorEnabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.prospectorEnabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.prospectorEnabled.deprecationMessage%" }, "python.linting.prospectorPath": { "default": "prospector", "description": "%python.linting.prospectorPath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.prospectorPath.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.prospectorPath.deprecationMessage%" }, "python.linting.pycodestyleArgs": { "default": [], @@ -871,7 +929,9 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.pycodestyleArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pycodestyleArgs.deprecationMessage%" }, "python.linting.pycodestyleCategorySeverity.E": { "default": "Error", @@ -883,7 +943,9 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pycodestyleCategorySeverity.E.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pycodestyleCategorySeverity.E.deprecationMessage%" }, "python.linting.pycodestyleCategorySeverity.W": { "default": "Warning", @@ -895,19 +957,25 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pycodestyleCategorySeverity.W.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pycodestyleCategorySeverity.W.deprecationMessage%" }, "python.linting.pycodestyleEnabled": { "default": false, "description": "%python.linting.pycodestyleEnabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.pycodestyleEnabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pycodestyleEnabled.deprecationMessage%" }, "python.linting.pycodestylePath": { "default": "pycodestyle", "description": "%python.linting.pycodestylePath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pycodestylePath.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pycodestylePath.deprecationMessage%" }, "python.linting.pydocstyleArgs": { "default": [], @@ -916,19 +984,25 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.pydocstyleArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pydocstyleArgs.deprecationMessage%" }, "python.linting.pydocstyleEnabled": { "default": false, "description": "%python.linting.pydocstyleEnabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.pydocstyleEnabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pydocstyleEnabled.deprecationMessage%" }, "python.linting.pydocstylePath": { "default": "pydocstyle", "description": "%python.linting.pydocstylePath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pydocstylePath.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pydocstylePath.deprecationMessage%" }, "python.linting.pylamaArgs": { "default": [], @@ -937,19 +1011,25 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.pylamaArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylamaArgs.deprecationMessage%" }, "python.linting.pylamaEnabled": { "default": false, "description": "%python.linting.pylamaEnabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.pylamaEnabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylamaEnabled.deprecationMessage%" }, "python.linting.pylamaPath": { "default": "pylama", "description": "%python.linting.pylamaPath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pylamaPath.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylamaPath.deprecationMessage%" }, "python.linting.pylintArgs": { "default": [], @@ -958,7 +1038,9 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "markdownDeprecationMessage": "%python.linting.pylintArgs.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintArgs.deprecationMessage%" }, "python.linting.pylintCategorySeverity.convention": { "default": "Information", @@ -970,7 +1052,9 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.convention.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintCategorySeverity.convention.deprecationMessage%" }, "python.linting.pylintCategorySeverity.error": { "default": "Error", @@ -982,7 +1066,9 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.error.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintCategorySeverity.error.deprecationMessage%" }, "python.linting.pylintCategorySeverity.fatal": { "default": "Error", @@ -994,7 +1080,9 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.fatal.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintCategorySeverity.fatal.deprecationMessage%" }, "python.linting.pylintCategorySeverity.refactor": { "default": "Hint", @@ -1006,7 +1094,9 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.refactor.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintCategorySeverity.refactor.deprecationMessage%" }, "python.linting.pylintCategorySeverity.warning": { "default": "Warning", @@ -1018,19 +1108,25 @@ "Warning" ], "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.warning.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintCategorySeverity.warning.deprecationMessage%" }, "python.linting.pylintEnabled": { "default": false, "description": "%python.linting.pylintEnabled.description%", "scope": "resource", - "type": "boolean" + "type": "boolean", + "markdownDeprecationMessage": "%python.linting.pylintEnabled.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintEnabled.deprecationMessage%" }, "python.linting.pylintPath": { "default": "pylint", "description": "%python.linting.pylintPath.description%", "scope": "machine-overridable", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.linting.pylintPath.markdownDeprecationMessage%", + "deprecationMessage": "%python.linting.pylintPath.deprecationMessage%" }, "python.logging.level": { "default": "error", diff --git a/package.nls.json b/package.nls.json index e228e6a19552..84c920b7c22e 100644 --- a/package.nls.json +++ b/package.nls.json @@ -44,12 +44,26 @@ "python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.", "python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.", "python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.formatting.autopep8Args.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.autopep8Args.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.autopep8Path.description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", + "python.formatting.autopep8Path.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.autopep8Path.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.blackArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.formatting.blackArgs.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.blackArgs.deprecationMessage": "This setting will soon be deprecated. Please use the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.blackPath.description": "Path to Black, you can use a custom version of Black by modifying this setting to include the full path.", + "python.formatting.blackPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.blackPath.deprecationMessage": "This setting will soon be deprecated. Please use the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.provider.description": "Provider for formatting. Possible options include 'autopep8', 'black', and 'yapf'.", + "python.formatting.provider.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8) or the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.provider.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension or the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.yapfArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.formatting.yapfArgs.markdownDeprecationMessage": "Yapf support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.yapfArgs.deprecationMessage": "Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.yapfPath.description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", + "python.formatting.yapfPath.markdownDeprecationMessage": "Yapf support will soon be deprecated.
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.yapfPath.deprecationMessage": "Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.", "python.languageServer.description": "Defines type of the language server.", "python.languageServer.defaultDescription": "Automatically select a language server: Pylance if installed and available, otherwise fallback to Jedi.", @@ -57,50 +71,132 @@ "python.languageServer.pylanceDescription": "Use Pylance as a language server.", "python.languageServer.noneDescription": "Disable language server capabilities.", "python.linting.banditArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.banditArgs.markdownDeprecationMessage": "Bandit support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.banditArgs.deprecationMessage": "Bandit support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.banditEnabled.description": "Whether to lint Python files using bandit.", + "python.linting.banditEnabled.markdownDeprecationMessage": "Bandit support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.banditEnabled.deprecationMessage": "Bandit support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.banditPath.description": "Path to bandit, you can use a custom version of bandit by modifying this setting to include the full path.", + "python.linting.banditPath.markdownDeprecationMessage": "Bandit support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.banditPath.deprecationMessage": "Bandit support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.cwd.description": "Optional working directory for linters.", + "python.linting.cwd.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.cwd.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.enabled.description": "Whether to lint Python files.", + "python.linting.enabled.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.enabled.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.flake8Args.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.flake8Args.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.flake8Args.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.flake8CategorySeverity.E.description": "Severity of Flake8 message type 'E'.", + "python.linting.flake8CategorySeverity.E.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.flake8CategorySeverity.E.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.flake8CategorySeverity.F.description": "Severity of Flake8 message type 'F'.", + "python.linting.flake8CategorySeverity.F.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.flake8CategorySeverity.F.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.flake8CategorySeverity.W.description": "Severity of Flake8 message type 'W'.", + "python.linting.flake8CategorySeverity.W.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.flake8CategorySeverity.W.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.flake8Enabled.description": "Whether to lint Python files using flake8.", + "python.linting.flake8Enabled.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.flake8Enabled.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.flake8Path.description": "Path to flake8, you can use a custom version of flake8 by modifying this setting to include the full path.", + "python.linting.flake8Path.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.flake8Path.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.ignorePatterns.description": "Patterns used to exclude files or folders from being linted.", + "python.linting.ignorePatterns.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.ignorePatterns.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.interpreter.infoVisibility.description": "Controls when to display information of selected interpreter in the status bar.", "python.interpreter.infoVisibility.never.description": "Never display information.", "python.interpreter.infoVisibility.onPythonRelated.description": "Only display information if Python-related files are opened.", "python.interpreter.infoVisibility.always.description": "Always display information.", "python.linting.lintOnSave.description": "Whether to lint Python files when saved.", + "python.linting.lintOnSave.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.lintOnSave.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.maxNumberOfProblems.description": "Controls the maximum number of problems produced by the server.", + "python.linting.maxNumberOfProblems.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.maxNumberOfProblems.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.mypyArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.mypyArgs.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.mypyArgs.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.mypyCategorySeverity.error.description": "Severity of Mypy message type 'Error'.", + "python.linting.mypyCategorySeverity.error.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.mypyCategorySeverity.error.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.mypyCategorySeverity.note.description": "Severity of Mypy message type 'Note'.", + "python.linting.mypyCategorySeverity.note.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.mypyCategorySeverity.note.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.mypyEnabled.description": "Whether to lint Python files using mypy.", + "python.linting.mypyEnabled.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.mypyEnabled.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.mypyPath.description": "Path to mypy, you can use a custom version of mypy by modifying this setting to include the full path.", + "python.linting.mypyPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.mypyPath.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.prospectorArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.prospectorArgs.markdownDeprecationMessage": "Prospector support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.prospectorArgs.deprecationMessage": "Prospector support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.prospectorEnabled.description": "Whether to lint Python files using prospector.", + "python.linting.prospectorEnabled.markdownDeprecationMessage": "Prospector support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.prospectorEnabled.deprecationMessage": "Prospector support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.prospectorPath.description": "Path to Prospector, you can use a custom version of prospector by modifying this setting to include the full path.", + "python.linting.prospectorPath.markdownDeprecationMessage": "Prospector support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.prospectorPath.deprecationMessage": "Prospector support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pycodestyleArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.pycodestyleArgs.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pycodestyleArgs.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pycodestyleCategorySeverity.E.description": "Severity of pycodestyle message type 'E'.", + "python.linting.pycodestyleCategorySeverity.E.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pycodestyleCategorySeverity.E.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pycodestyleCategorySeverity.W.description": "Severity of pycodestyle message type 'W'.", + "python.linting.pycodestyleCategorySeverity.W.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pycodestyleCategorySeverity.W.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pycodestyleEnabled.description": "Whether to lint Python files using pycodestyle.", + "python.linting.pycodestyleEnabled.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pycodestyleEnabled.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pycodestylePath.description": "Path to pycodestyle, you can use a custom version of pycodestyle by modifying this setting to include the full path.", + "python.linting.pycodestylePath.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pycodestylePath.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pydocstyleArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.pydocstyleArgs.markdownDeprecationMessage": "Pydocstyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pydocstyleArgs.deprecationMessage": "Pydocstyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pydocstyleEnabled.description": "Whether to lint Python files using pydocstyle.", + "python.linting.pydocstyleEnabled.markdownDeprecationMessage": "Pydocstyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pydocstyleEnabled.deprecationMessage": "Pydocstyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pydocstylePath.description": "Path to pydocstyle, you can use a custom version of pydocstyle by modifying this setting to include the full path.", + "python.linting.pydocstylePath.markdownDeprecationMessage": "Pydocstyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pydocstylePath.deprecationMessage": "Pydocstyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylamaArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.pylamaArgs.markdownDeprecationMessage": "Pylama support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylamaArgs.deprecationMessage": "Pylama support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylamaEnabled.description": "Whether to lint Python files using pylama.", + "python.linting.pylamaEnabled.markdownDeprecationMessage": "Pylama support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylamaEnabled.deprecationMessage": "Pylama support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylamaPath.description": "Path to pylama, you can use a custom version of pylama by modifying this setting to include the full path.", + "python.linting.pylamaPath.markdownDeprecationMessage": "Pylama support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylamaPath.deprecationMessage": "Pylama support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintArgs.description": "Arguments passed in. Each argument is a separate item in the array.", + "python.linting.pylintArgs.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintArgs.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintCategorySeverity.convention.description": "Severity of Pylint message type 'Convention/C'.", + "python.linting.pylintCategorySeverity.convention.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintCategorySeverity.convention.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintCategorySeverity.error.description": "Severity of Pylint message type 'Error/E'.", + "python.linting.pylintCategorySeverity.error.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintCategorySeverity.error.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintCategorySeverity.fatal.description": "Severity of Pylint message type 'Error/F'.", + "python.linting.pylintCategorySeverity.fatal.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintCategorySeverity.fatal.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintCategorySeverity.refactor.description": "Severity of Pylint message type 'Refactor/R'.", + "python.linting.pylintCategorySeverity.refactor.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintCategorySeverity.refactor.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintCategorySeverity.warning.description": "Severity of Pylint message type 'Warning/W'.", + "python.linting.pylintCategorySeverity.warning.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintCategorySeverity.warning.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintEnabled.description": "Whether to lint Python files using pylint.", + "python.linting.pylintEnabled.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintEnabled.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.linting.pylintPath.description": "Path to Pylint, you can use a custom version of pylint by modifying this setting to include the full path.", + "python.linting.pylintPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", + "python.linting.pylintPath.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.logging.level.description": "The logging level the extension logs at, defaults to 'error'", "python.logging.level.deprecation": "This setting is deprecated. Please use command `Developer: Set Log Level...` to set logging level.", "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", From e07aa5ed30e0f54e5b1ee718ee6ef409db4dc946 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 13 Jul 2023 18:20:35 -0700 Subject: [PATCH 0086/1136] Update writing style based on recommendation (#21525) Closes https://github.com/microsoft/vscode-python/issues/21521 Closes https://github.com/microsoft/vscode-python/issues/21519 --------- Co-authored-by: Brett Cannon Co-authored-by: Courtney Webster <60238438+cwebster-99@users.noreply.github.com> Co-authored-by: Luciana Abud <45497113+luabud@users.noreply.github.com> --- src/client/common/utils/localize.ts | 27 +++++++++---------- .../debugger/extension/adapter/factory.ts | 2 +- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index f32c4fec0ac9..b3af0c476957 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -53,7 +53,6 @@ export namespace Common { export const close = l10n.t('Close'); export const bannerLabelYes = l10n.t('Yes'); export const bannerLabelNo = l10n.t('No'); - export const yesPlease = l10n.t('Yes, please'); export const canceled = l10n.t('Canceled'); export const cancel = l10n.t('Cancel'); export const ok = l10n.t('Ok'); @@ -143,7 +142,7 @@ export namespace TensorBoard { export const launchNativeTensorBoardSessionCodeLens = l10n.t('▶ Launch TensorBoard Session'); export const launchNativeTensorBoardSessionCodeAction = l10n.t('Launch TensorBoard session'); export const missingSourceFile = l10n.t( - 'We could not locate the requested source file on disk. Please manually specify the file.', + 'The Python extension could not locate the requested source file on disk. Please manually specify the file.', ); export const selectMissingSourceFile = l10n.t('Choose File'); export const selectMissingSourceFileDescription = l10n.t( @@ -167,7 +166,7 @@ export namespace LanguageService { ); export const reloadAfterLanguageServerChange = l10n.t( - 'Please reload the window switching between language servers.', + 'Reload the window after switching between language servers.', ); export const lsFailedToStart = l10n.t( @@ -184,7 +183,7 @@ export namespace LanguageService { export const extractionCompletedOutputMessage = l10n.t('Language server download complete.'); export const extractionDoneOutputMessage = l10n.t('done.'); export const reloadVSCodeIfSeachPathHasChanged = l10n.t( - 'Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly.', + 'Search paths have changed for this Python interpreter. Reload the extension to ensure that the IntelliSense works correctly.', ); } export namespace Interpreters { @@ -211,11 +210,11 @@ export namespace Interpreters { 'Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar', ); export const installPythonTerminalMessageLinux = l10n.t( - '💡 Please try installing the Python package using your package manager. Alternatively you can also download it from https://www.python.org/downloads', + '💡 Try installing the Python package using your package manager. Alternatively you can also download it from https://www.python.org/downloads', ); export const installPythonTerminalMacMessage = l10n.t( - '💡 Brew does not seem to be available. Please try to download Python from https://www.python.org/downloads. Alternatively, you can install the Python package using some other available package manager.', + '💡 Brew does not seem to be available. You can download Python from https://www.python.org/downloads. Alternatively, you can install the Python package using some other available package manager.', ); export const changePythonInterpreter = l10n.t('Change Python Interpreter'); export const selectedPythonInterpreter = l10n.t('Selected Python Interpreter'); @@ -225,7 +224,7 @@ export namespace InterpreterQuickPickList { export const condaEnvWithoutPythonTooltip = l10n.t( 'Python is not available in this environment, it will automatically be installed upon selecting it', ); - export const noPythonInstalled = l10n.t('Python is not installed, please download and install it'); + export const noPythonInstalled = l10n.t('Python is not installed'); export const clickForInstructions = l10n.t('Click for instructions...'); export const globalGroupName = l10n.t('Global'); export const workspaceGroupName = l10n.t('Workspace'); @@ -266,7 +265,7 @@ export namespace Installer { export namespace ExtensionSurveyBanner { export const bannerMessage = l10n.t( - 'Can you please take 2 minutes to tell us how the Python extension is working for you?', + 'Can you take 2 minutes to tell us how the Python extension is working for you?', ); export const bannerLabelYes = l10n.t('Yes, take survey now'); export const bannerLabelNo = l10n.t('No, thanks'); @@ -418,7 +417,7 @@ export namespace Testing { export namespace OutdatedDebugger { export const outdatedDebuggerMessage = l10n.t( - 'We noticed you are attaching to ptvsd (Python debugger), which was deprecated on May 1st, 2020. Please switch to [debugpy](https://aka.ms/migrateToDebugpy).', + 'We noticed you are attaching to ptvsd (Python debugger), which was deprecated on May 1st, 2020. Use [debugpy](https://aka.ms/migrateToDebugpy) instead.', ); } @@ -435,13 +434,13 @@ export namespace SwitchToDefaultLS { } export namespace CreateEnv { - export const informEnvCreation = l10n.t('We have selected the following environment:'); + export const informEnvCreation = l10n.t('The following environment is selected:'); export const statusTitle = l10n.t('Creating environment'); export const statusStarting = l10n.t('Starting...'); export const hasVirtualEnv = l10n.t('Workspace folder contains a virtual environment'); - export const noWorkspace = l10n.t('Please open a folder when creating an environment using venv.'); + export const noWorkspace = l10n.t('A workspace is required when creating an environment using venv.'); export const pickWorkspacePlaceholder = l10n.t('Select a workspace to create environment'); @@ -465,12 +464,12 @@ export namespace CreateEnv { } export namespace Conda { - export const condaMissing = l10n.t('Please install `conda` to create conda environments.'); + export const condaMissing = l10n.t('Install `conda` to create conda environments.'); export const created = l10n.t('Environment created...'); export const installingPackages = l10n.t('Installing packages...'); export const errorCreatingEnvironment = l10n.t('Error while creating conda environment.'); export const selectPythonQuickPickPlaceholder = l10n.t( - 'Please select the version of Python to install in the environment', + 'Select the version of Python to install in the environment', ); export const creating = l10n.t('Creating conda environment...'); export const providerDescription = l10n.t('Creates a `.conda` Conda environment in the current workspace'); @@ -485,7 +484,7 @@ export namespace ToolsExtensions { 'Use the Pylint extension to enable easier configuration and new features such as quick fixes.', ); export const isortPromptMessage = l10n.t( - 'To use sort imports, please install the isort extension. It provides easier configuration and new features such as code actions.', + 'To use sort imports, install the isort extension. It provides easier configuration and new features such as code actions.', ); export const installPylintExtension = l10n.t('Install Pylint extension'); export const installFlake8Extension = l10n.t('Install Flake8 extension'); diff --git a/src/client/debugger/extension/adapter/factory.ts b/src/client/debugger/extension/adapter/factory.ts index 067c2e405ea0..ecbd8afcc287 100644 --- a/src/client/debugger/extension/adapter/factory.ts +++ b/src/client/debugger/extension/adapter/factory.ts @@ -203,6 +203,6 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac * @memberof DebugAdapterDescriptorFactory */ private async notifySelectInterpreter() { - await showErrorMessage(l10n.t('Please install Python or select a Python Interpreter to use the debugger.')); + await showErrorMessage(l10n.t('Install Python or select a Python Interpreter to use the debugger.')); } } From ea768589cfaa255757dc83eee0ab021a5bef9f78 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 14 Jul 2023 09:39:34 -0700 Subject: [PATCH 0087/1136] Handle error tests as a different test icon (#21630) fixes https://github.com/microsoft/vscode-python/issues/21625 adds a few things: - returns error on any caught errors in pytest that aren't assertion errors - add error node type handling to result resolver - update tests for both files --- .../expected_execution_test_output.py | 2 +- .../tests/pytestadapter/test_execution.py | 5 ++- pythonFiles/vscode_pytest/__init__.py | 6 ++- .../testController/common/resultResolver.ts | 24 +++++++++++- .../resultResolver.unit.test.ts | 39 +++++++++++++++++++ 5 files changed, 71 insertions(+), 5 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index 92b881fdc8b8..fe1d40a55b43 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -157,7 +157,7 @@ error_raised_exception_execution_expected_output = { "error_raise_exception.py::TestSomething::test_a": { "test": "error_raise_exception.py::TestSomething::test_a", - "outcome": "failure", + "outcome": "error", "message": "ERROR MESSAGE", "traceback": "TRACEBACK", "subtest": None, diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index 8f5fa191e38c..ffc84955bf54 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -174,7 +174,10 @@ def test_pytest_execution(test_ids, expected_const): assert a["cwd"] == os.fspath(TEST_DATA_PATH) actual_result_dict.update(a["result"]) for key in actual_result_dict: - if actual_result_dict[key]["outcome"] == "failure": + if ( + actual_result_dict[key]["outcome"] == "failure" + or actual_result_dict[key]["outcome"] == "error" + ): actual_result_dict[key]["message"] = "ERROR MESSAGE" if actual_result_dict[key]["traceback"] != None: actual_result_dict[key]["traceback"] = "TRACEBACK" diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 20b3dba325ef..072c5ef5d3ad 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -82,7 +82,9 @@ def pytest_exception_interact(node, call, report): ) else: # if execution, send this data that the given node failed. - report_value = "failure" + report_value = "error" + if call.excinfo.typename == "AssertionError": + report_value = "failure" node_id = str(node.nodeid) if node_id not in collected_tests_so_far: collected_tests_so_far.append(node_id) @@ -119,7 +121,7 @@ class TestOutcome(Dict): """ test: str - outcome: Literal["success", "failure", "skipped"] + outcome: Literal["success", "failure", "skipped", "error"] message: Union[str, None] traceback: Union[str, None] subtest: Optional[str] diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 320c00bf6ad0..8baf4d0d7ae7 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -102,7 +102,29 @@ export class PythonResultResolver implements ITestResultResolver { testCases.push(...tempArr); }); - if ( + if (rawTestExecData.result[keyTemp].outcome === 'error') { + const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + const text = `${rawTestExecData.result[keyTemp].test} failed with error: ${ + rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome + }\r\n${traceback}\r\n`; + const message = new TestMessage(text); + + const grabVSid = this.runIdToVSid.get(keyTemp); + // search through freshly built array of testItem to find the failed test and update UI. + testCases.forEach((indiItem) => { + if (indiItem.id === grabVSid) { + if (indiItem.uri && indiItem.range) { + message.location = new Location(indiItem.uri, indiItem.range); + runInstance.errored(indiItem, message); + runInstance.appendOutput(fixLogLines(text)); + } + } + }); + } else if ( rawTestExecData.result[keyTemp].outcome === 'failure' || rawTestExecData.result[keyTemp].outcome === 'passed-unexpected' ) { diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts index 30b2ccecf513..09a68128167d 100644 --- a/src/test/testing/testController/resultResolver.unit.test.ts +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -390,6 +390,45 @@ suite('Result Resolver tests', () => { // verify that the passed function was called for the single test item runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.once()); }); + test('resolveExecution handles error correctly as test outcome', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'error', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.errored(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); + }); test('resolveExecution handles success correctly', async () => { // test specific constants used expected values testProvider = 'pytest'; From f38f6e558f3fb3f024b4cfa6b0ebae182be94f94 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 14 Jul 2023 15:25:02 -0700 Subject: [PATCH 0088/1136] Add npm project for Python API (#21631) For https://github.com/microsoft/vscode-python/issues/20949 --- .eslintignore | 2 + .gitignore | 1 + build/azure-pipelines/pipeline.yml | 56 +++ .../azure-pipelines/templates/pack-steps.yml | 14 + .../azure-pipelines/templates/test-steps.yml | 23 ++ build/fail.js | 6 + gulpfile.js | 18 +- package.json | 4 +- pythonExtensionApi/.npmignore | 8 + pythonExtensionApi/LICENSE.md | 21 + pythonExtensionApi/README.md | 31 ++ pythonExtensionApi/SECURITY.md | 41 ++ pythonExtensionApi/example/todo.txt | 0 pythonExtensionApi/package-lock.json | 32 ++ pythonExtensionApi/package.json | 40 ++ .../api => pythonExtensionApi/src}/main.ts | 0 pythonExtensionApi/tsconfig.json | 34 ++ src/client/api.ts | 2 +- src/client/api/package-lock.json | 63 --- src/client/api/package.json | 26 -- src/client/api/types.ts | 389 ++++++++++++++++++ src/client/deprecatedProposedApiTypes.ts | 2 +- src/client/environmentApi.ts | 2 +- src/client/extension.ts | 2 +- .../creation/proposed.createEnvApis.ts | 2 +- src/test/api.test.ts | 2 +- src/test/common.ts | 2 +- src/test/environmentApi.unit.test.ts | 2 +- src/test/initialize.ts | 2 +- tsconfig.json | 3 +- 30 files changed, 728 insertions(+), 102 deletions(-) create mode 100644 build/azure-pipelines/pipeline.yml create mode 100644 build/azure-pipelines/templates/pack-steps.yml create mode 100644 build/azure-pipelines/templates/test-steps.yml create mode 100644 build/fail.js create mode 100644 pythonExtensionApi/.npmignore create mode 100644 pythonExtensionApi/LICENSE.md create mode 100644 pythonExtensionApi/README.md create mode 100644 pythonExtensionApi/SECURITY.md create mode 100644 pythonExtensionApi/example/todo.txt create mode 100644 pythonExtensionApi/package-lock.json create mode 100644 pythonExtensionApi/package.json rename {src/client/api => pythonExtensionApi/src}/main.ts (100%) create mode 100644 pythonExtensionApi/tsconfig.json delete mode 100644 src/client/api/package-lock.json delete mode 100644 src/client/api/package.json create mode 100644 src/client/api/types.ts diff --git a/.eslintignore b/.eslintignore index ad69cab31ea7..7f6bb48d6c8e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,5 @@ +pythonExtensionApi/out/ + # The following files were grandfathered out of eslint. They can be removed as time permits. src/test/analysisEngineTest.ts diff --git a/.gitignore b/.gitignore index 0fc4c34d7127..ec46f481e79b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ cucumber-report.json port.txt precommit.hook pythonFiles/lib/** +pythonFiles/get_pip.py debug_coverage*/** languageServer/** languageServer.*/** diff --git a/build/azure-pipelines/pipeline.yml b/build/azure-pipelines/pipeline.yml new file mode 100644 index 000000000000..9cab7dedf9b1 --- /dev/null +++ b/build/azure-pipelines/pipeline.yml @@ -0,0 +1,56 @@ +############################################################################################### +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +############################################################################################### +name: $(Date:yyyyMMdd)$(Rev:.r) + +pr: none + +resources: + repositories: + - repository: templates + type: github + name: microsoft/vscode-engineering + ref: main + endpoint: Monaco + +parameters: + - name: quality + displayName: Quality + type: string + default: latest + values: + - latest + - next + - name: publishPythonApi + displayName: 🚀 Publish pythonExtensionApi + type: boolean + default: false + +extends: + template: azure-pipelines/npm-package/pipeline.yml@templates + parameters: + npmPackages: + - name: pythonExtensionApi + testPlatforms: + - name: Linux + nodeVersions: + - 16.17.1 + - name: MacOS + nodeVersions: + - 16.17.1 + - name: Windows + nodeVersions: + - 16.17.1 + testSteps: + - template: /build/azure-pipelines/templates/test-steps.yml@self + parameters: + package: pythonExtensionApi + buildSteps: + - template: /build/azure-pipelines/templates/pack-steps.yml@self + parameters: + package: pythonExtensionApi + ghTagPrefix: release/pythonExtensionApi/ + tag: ${{ parameters.quality }} + publishPackage: ${{ parameters.publishPythonApi }} + workingDirectory: $(Build.SourcesDirectory)/pythonExtensionApi diff --git a/build/azure-pipelines/templates/pack-steps.yml b/build/azure-pipelines/templates/pack-steps.yml new file mode 100644 index 000000000000..97037efb59ba --- /dev/null +++ b/build/azure-pipelines/templates/pack-steps.yml @@ -0,0 +1,14 @@ +############################################################################################### +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +############################################################################################### +parameters: +- name: package + +steps: + - script: npm install --root-only + workingDirectory: $(Build.SourcesDirectory) + displayName: Install root dependencies + - script: npm install + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.package }} + displayName: Install package dependencies diff --git a/build/azure-pipelines/templates/test-steps.yml b/build/azure-pipelines/templates/test-steps.yml new file mode 100644 index 000000000000..15eb3db6384d --- /dev/null +++ b/build/azure-pipelines/templates/test-steps.yml @@ -0,0 +1,23 @@ +############################################################################################### +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +############################################################################################### +parameters: +- name: package + type: string +- name: script + type: string + default: 'all:publish' + +steps: + - script: npm install --root-only + workingDirectory: $(Build.SourcesDirectory) + displayName: Install root dependencies + - bash: | + /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + echo ">>> Started xvfb" + displayName: Start xvfb + condition: eq(variables['Agent.OS'], 'Linux') + - script: npm run ${{ parameters.script }} + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.package }} + displayName: Verify package diff --git a/build/fail.js b/build/fail.js new file mode 100644 index 000000000000..2adc808d8da9 --- /dev/null +++ b/build/fail.js @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +process.exitCode = 1; diff --git a/gulpfile.js b/gulpfile.js index 0d47127f187e..66f96bf48ec0 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -27,7 +27,7 @@ const tsProject = ts.createProject('./tsconfig.json', { typescript }); const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; -gulp.task('compile', (done) => { +gulp.task('compileCore', (done) => { let failed = false; tsProject .src() @@ -39,6 +39,22 @@ gulp.task('compile', (done) => { .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); }); +const apiTsProject = ts.createProject('./pythonExtensionApi/tsconfig.json', { typescript }); + +gulp.task('compileApi', (done) => { + let failed = false; + apiTsProject + .src() + .pipe(apiTsProject()) + .on('error', () => { + failed = true; + }) + .js.pipe(gulp.dest('./pythonExtensionApi/out')) + .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); +}); + +gulp.task('compile', gulp.series('compileCore', 'compileApi')); + gulp.task('precommit', (done) => run({ exitOnError: true, mode: 'staged' }, done)); gulp.task('output:clean', () => del(['coverage'])); diff --git a/package.json b/package.json index 0ef0ff4d79b6..e38e6921e50a 100644 --- a/package.json +++ b/package.json @@ -2064,8 +2064,8 @@ "testSmoke": "cross-env INSTALL_JUPYTER_EXTENSION=true \"node ./out/test/smokeTest.js\"", "testInsiders": "cross-env VSC_PYTHON_CI_TEST_VSC_CHANNEL=insiders INSTALL_PYLANCE_EXTENSION=true TEST_FILES_SUFFIX=insiders.test CODE_TESTS_WORKSPACE=src/testMultiRootWkspc/smokeTests \"node ./out/test/standardTest.js\"", "lint-staged": "node gulpfile.js", - "lint": "eslint --ext .ts,.js src build", - "lint-fix": "eslint --fix --ext .ts,.js src build gulpfile.js", + "lint": "eslint --ext .ts,.js src build pythonExtensionApi", + "lint-fix": "eslint --fix --ext .ts,.js src build pythonExtensionApi gulpfile.js", "format-check": "prettier --check 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", "format-fix": "prettier --write 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", "clean": "gulp clean", diff --git a/pythonExtensionApi/.npmignore b/pythonExtensionApi/.npmignore new file mode 100644 index 000000000000..283d589ea5fe --- /dev/null +++ b/pythonExtensionApi/.npmignore @@ -0,0 +1,8 @@ +example/** +dist/ +out/**/*.map +out/**/*.tsbuildInfo +src/ +.eslintrc* +.eslintignore +tsconfig*.json diff --git a/pythonExtensionApi/LICENSE.md b/pythonExtensionApi/LICENSE.md new file mode 100644 index 000000000000..767f4076ba05 --- /dev/null +++ b/pythonExtensionApi/LICENSE.md @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pythonExtensionApi/README.md b/pythonExtensionApi/README.md new file mode 100644 index 000000000000..d7ca4ccfdae4 --- /dev/null +++ b/pythonExtensionApi/README.md @@ -0,0 +1,31 @@ +# Python extension's API + +This npm module implements an API facade for the Python extension in VS Code. + +## Example + +The source code of the example can be found [here](TODO Update example extension link here) + +First we need to define a `package.json` for the extension that wants to use the API: + +```jsonc +{ + "name": "...", + ... + // depend on the Python extension + "extensionDependencies": [ + "ms-python.python" + ], + // Depend on the Python extension facade npm module to get easier API access to the + // core extension. + "dependencies": { + "@vscode/python-extension": "..." + }, +} +``` + +TODO insert example here + +```typescript +TODO +``` diff --git a/pythonExtensionApi/SECURITY.md b/pythonExtensionApi/SECURITY.md new file mode 100644 index 000000000000..a050f362c152 --- /dev/null +++ b/pythonExtensionApi/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + + diff --git a/pythonExtensionApi/example/todo.txt b/pythonExtensionApi/example/todo.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json new file mode 100644 index 000000000000..9c7c4046870e --- /dev/null +++ b/pythonExtensionApi/package-lock.json @@ -0,0 +1,32 @@ +{ + "name": "@vscode/python-extension", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@vscode/python-extension", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@types/vscode": "^1.78.0" + }, + "engines": { + "node": ">=16.17.1", + "vscode": "^1.78.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.80.0.tgz", + "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==" + } + }, + "dependencies": { + "@types/vscode": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.80.0.tgz", + "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==" + } + } +} diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json new file mode 100644 index 000000000000..43b4c19877c3 --- /dev/null +++ b/pythonExtensionApi/package.json @@ -0,0 +1,40 @@ +{ + "name": "@vscode/python-extension", + "description": "An API facade for the Python extension in VS Code", + "version": "1.0.0", + "author": { + "name": "Microsoft Corporation" + }, + "keywords": [ + "Python", + "VSCode", + "API" + ], + "main": "./out/main.js", + "types": "./out/main.d.ts", + "engines": { + "node": ">=16.17.1", + "vscode": "^1.78.0" + }, + "license": "MIT", + "homepage": "https://github.com/Microsoft/vscode-python", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/vscode-python" + }, + "bugs": { + "url": "https://github.com/Microsoft/vscode-python/issues" + }, + "dependencies": { + "@types/vscode": "^1.78.0" + }, + "scripts": { + "prepublishOnly": "echo \"⛔ Can only publish from a secure pipeline ⛔\" && node ../build/fail", + "prepack": "npm run all:publish", + "compile": "node ../node_modules/typescript/lib/tsc.js -b ./tsconfig.json", + "clean": "node ../node_modules/rimraf/bin.js out", + "lint": "node ../node_modules/eslint/bin/eslint.js --ext ts src", + "all": "npm run clean && npm run compile", + "all:publish": "git clean -xfd . && npm install && npm run compile" + } +} diff --git a/src/client/api/main.ts b/pythonExtensionApi/src/main.ts similarity index 100% rename from src/client/api/main.ts rename to pythonExtensionApi/src/main.ts diff --git a/pythonExtensionApi/tsconfig.json b/pythonExtensionApi/tsconfig.json new file mode 100644 index 000000000000..d90209b3a4b6 --- /dev/null +++ b/pythonExtensionApi/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": ["types/*"] + }, + "module": "commonjs", + "target": "es2018", + "outDir": "./out", + "lib": [ + "es6", + "es2018", + "dom", + "ES2019", + "ES2020" + ], + "sourceMap": true, + "rootDir": "src", + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "removeComments": true + }, + "exclude": [ + "node_modules", + "out" + ] +} diff --git a/src/client/api.ts b/src/client/api.ts index 7cf580d9f78f..32ce68f6a28b 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -10,7 +10,7 @@ import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient import { LanguageClient } from 'vscode-languageclient/node'; import { PYLANCE_NAME } from './activation/node/languageClientFactory'; import { ILanguageServerOutputChannel } from './activation/types'; -import { PythonExtension } from './api/main'; +import { PythonExtension } from './api/types'; import { isTestExecution, PYTHON_LANGUAGE } from './common/constants'; import { IConfigurationService, Resource } from './common/types'; import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extension/adapter/remoteLaunchers'; diff --git a/src/client/api/package-lock.json b/src/client/api/package-lock.json deleted file mode 100644 index 137262c6523b..000000000000 --- a/src/client/api/package-lock.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "@vscode/python-extension", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "@vscode/python-extension", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@types/vscode": "^1.78.0" - }, - "devDependencies": { - "@types/node": "^16.11.7", - "typescript": "^4.7.2" - } - }, - "node_modules/@types/node": { - "version": "16.18.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.27.tgz", - "integrity": "sha512-GFfndd/RINWD19W+xNJ9Qh/sOZ5ieTiOSagA86ER/12i/l+MEnQxsbldGRF23azWjRfe7zUlAldyrwN84a1E5w==", - "dev": true - }, - "node_modules/@types/vscode": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.78.0.tgz", - "integrity": "sha512-LJZIJpPvKJ0HVQDqfOy6W4sNKUBBwyDu1Bs8chHBZOe9MNuKTJtidgZ2bqjhmmWpUb0TIIqv47BFUcVmAsgaVA==" - }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - } - }, - "dependencies": { - "@types/node": { - "version": "16.18.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.27.tgz", - "integrity": "sha512-GFfndd/RINWD19W+xNJ9Qh/sOZ5ieTiOSagA86ER/12i/l+MEnQxsbldGRF23azWjRfe7zUlAldyrwN84a1E5w==", - "dev": true - }, - "@types/vscode": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.78.0.tgz", - "integrity": "sha512-LJZIJpPvKJ0HVQDqfOy6W4sNKUBBwyDu1Bs8chHBZOe9MNuKTJtidgZ2bqjhmmWpUb0TIIqv47BFUcVmAsgaVA==" - }, - "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true - } - } -} diff --git a/src/client/api/package.json b/src/client/api/package.json deleted file mode 100644 index 901ab3f13cfd..000000000000 --- a/src/client/api/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@vscode/python-extension", - "description": "VSCode Python extension's public API", - "version": "1.0.0", - "publisher": "ms-python", - "author": { - "name": "Microsoft Corporation" - }, - "types": "./index.d.ts", - "license": "MIT", - "homepage": "https://github.com/Microsoft/vscode-python", - "repository": { - "type": "git", - "url": "https://github.com/Microsoft/vscode-python" - }, - "bugs": { - "url": "https://github.com/Microsoft/vscode-python/issues" - }, - "dependencies": { - "@types/vscode": "^1.78.0" - }, - "devDependencies": { - "@types/node": "^16.11.7", - "typescript": "^4.7.2" - } -} diff --git a/src/client/api/types.ts b/src/client/api/types.ts new file mode 100644 index 000000000000..4e13ec4853ec --- /dev/null +++ b/src/client/api/types.ts @@ -0,0 +1,389 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem } from 'vscode'; + +/* + * Do not introduce any breaking changes to this API. + * This is the public API for other extensions to interact with this extension. + */ +export interface PythonExtension { + /** + * Promise indicating whether all parts of the extension have completed loading or not. + * @type {Promise} + */ + ready: Promise; + jupyter: { + registerHooks(): void; + }; + debug: { + /** + * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. + * Users can append another array of strings of what they want to execute along with relevant arguments to Python. + * E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` + * @param {string} host + * @param {number} port + * @param {boolean} [waitUntilDebuggerAttaches=true] + * @returns {Promise} + */ + getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; + + /** + * Gets the path to the debugger package used by the extension. + * @returns {Promise} + */ + getDebuggerPackagePath(): Promise; + }; + + datascience: { + /** + * Launches Data Viewer component. + * @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data. + * @param {string} title Data Viewer title + */ + showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; + /** + * Registers a remote server provider component that's used to pick remote jupyter server URIs + * @param serverProvider object called back when picking jupyter server URI + */ + registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void; + }; + + /** + * These APIs provide a way for extensions to work with by python environments available in the user's machine + * as found by the Python extension. See + * https://github.com/microsoft/vscode-python/wiki/Python-Environment-APIs for usage examples and more. + */ + readonly environments: { + /** + * Returns the environment configured by user in settings. Note that this can be an invalid environment, use + * {@link resolveEnvironment} to get full details. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getActiveEnvironmentPath(resource?: Resource): EnvironmentPath; + /** + * Sets the active environment path for the python extension for the resource. Configuration target will always + * be the workspace folder. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + * @param resource : [optional] File or workspace to scope to a particular workspace folder. + */ + updateActiveEnvironmentPath( + environment: string | EnvironmentPath | Environment, + resource?: Resource, + ): Promise; + /** + * This event is triggered when the active environment setting changes. + */ + readonly onDidChangeActiveEnvironmentPath: Event; + /** + * Carries environments known to the extension at the time of fetching the property. Note this may not + * contain all environments in the system as a refresh might be going on. + * + * Only reports environments in the current workspace. + */ + readonly known: readonly Environment[]; + /** + * This event is triggered when the known environment list changes, like when a environment + * is found, existing environment is removed, or some details changed on an environment. + */ + readonly onDidChangeEnvironments: Event; + /** + * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. + * Useful for making sure env list is up-to-date when the caller needs it for the first time. + * + * To force trigger a refresh regardless of whether a refresh was already triggered, see option + * {@link RefreshOptions.forceRefresh}. + * + * Note that if there is a refresh already going on then this returns the promise for that refresh. + * @param options Additional options for refresh. + * @param token A cancellation token that indicates a refresh is no longer needed. + */ + refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise; + /** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + */ + resolveEnvironment( + environment: Environment | EnvironmentPath | string, + ): Promise; + /** + * Returns the environment variables used by the extension for a resource, which includes the custom + * variables configured by user in `.env` files. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getEnvironmentVariables(resource?: Resource): EnvironmentVariables; + /** + * This event is fired when the environment variables for a resource change. Note it's currently not + * possible to detect if environment variables in the system change, so this only fires if custom + * environment variables are updated in `.env` files. + */ + readonly onDidEnvironmentVariablesChange: Event; + }; +} + +interface IJupyterServerUri { + baseUrl: string; + token: string; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + authorizationHeader: any; // JSON object for authorization header. + expiration?: Date; // Date/time when header expires and should be refreshed. + displayName: string; +} + +type JupyterServerUriHandle = string; + +export interface IJupyterUriProvider { + readonly id: string; // Should be a unique string (like a guid) + getQuickPickEntryItems(): QuickPickItem[]; + handleQuickPick(item: QuickPickItem, backEnabled: boolean): Promise; + getServerUri(handle: JupyterServerUriHandle): Promise; +} + +interface IDataFrameInfo { + columns?: { key: string; type: ColumnType }[]; + indexColumn?: string; + rowCount?: number; +} + +export interface IDataViewerDataProvider { + dispose(): void; + getDataFrameInfo(): Promise; + getAllRows(): Promise; + getRows(start: number, end: number): Promise; +} + +enum ColumnType { + String = 'string', + Number = 'number', + Bool = 'bool', +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type IRowsResponse = any[]; + +export type RefreshOptions = { + /** + * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so + * it's best to only use it if user manually triggers a refresh. + */ + forceRefresh?: boolean; +}; + +/** + * Details about the environment. Note the environment folder, type and name never changes over time. + */ +export type Environment = EnvironmentPath & { + /** + * Carries details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness if known at this moment. + */ + readonly bitness: Bitness | undefined; + /** + * Value of `sys.prefix` in sys module if known at this moment. + */ + readonly sysPrefix: string | undefined; + }; + /** + * Carries details if it is an environment, otherwise `undefined` in case of global interpreters and others. + */ + readonly environment: + | { + /** + * Type of the environment. + */ + readonly type: EnvironmentType; + /** + * Name to the environment if any. + */ + readonly name: string | undefined; + /** + * Uri of the environment folder. + */ + readonly folderUri: Uri; + /** + * Any specific workspace folder this environment is created for. + */ + readonly workspaceFolder: WorkspaceFolder | undefined; + } + | undefined; + /** + * Carries Python version information known at this moment, carries `undefined` for envs without python. + */ + readonly version: + | (VersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string | undefined; + }) + | undefined; + /** + * Tools/plugins which created the environment or where it came from. First value in array corresponds + * to the primary tool which manages the environment, which never changes over time. + * + * Array is empty if no tool is responsible for creating/managing the environment. Usually the case for + * global interpreters. + */ + readonly tools: readonly EnvironmentTools[]; +}; + +/** + * Derived form of {@link Environment} where certain properties can no longer be `undefined`. Meant to represent an + * {@link Environment} with complete information. + */ +export type ResolvedEnvironment = Environment & { + /** + * Carries complete details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness of the environment. + */ + readonly bitness: Bitness; + /** + * Value of `sys.prefix` in sys module. + */ + readonly sysPrefix: string; + }; + /** + * Carries complete Python version information, carries `undefined` for envs without python. + */ + readonly version: + | (ResolvedVersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string; + }) + | undefined; +}; + +export type EnvironmentsChangeEvent = { + readonly env: Environment; + /** + * * "add": New environment is added. + * * "remove": Existing environment in the list is removed. + * * "update": New information found about existing environment. + */ + readonly type: 'add' | 'remove' | 'update'; +}; + +export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & { + /** + * Workspace folder the environment changed for. + */ + readonly resource: WorkspaceFolder | undefined; +}; + +/** + * Uri of a file inside a workspace or workspace folder itself. + */ +export type Resource = Uri | WorkspaceFolder; + +export type EnvironmentPath = { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; +}; + +/** + * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentTools} or custom string which + * was contributed. + */ +export type EnvironmentTools = KnownEnvironmentTools | string; +/** + * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink + * once tools have their own separate extensions. + */ +export type KnownEnvironmentTools = + | 'Conda' + | 'Pipenv' + | 'Poetry' + | 'VirtualEnv' + | 'Venv' + | 'VirtualEnvWrapper' + | 'Pyenv' + | 'Unknown'; + +/** + * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. + */ +export type EnvironmentType = KnownEnvironmentTypes | string; +/** + * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their + * own separate extensions, in which case they're expected to provide the type themselves. + */ +export type KnownEnvironmentTypes = 'VirtualEnvironment' | 'Conda' | 'Unknown'; + +/** + * Carries bitness for an environment. + */ +export type Bitness = '64-bit' | '32-bit' | 'Unknown'; + +/** + * The possible Python release levels. + */ +export type PythonReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; + +/** + * Release information for a Python version. + */ +export type PythonVersionRelease = { + readonly level: PythonReleaseLevel; + readonly serial: number; +}; + +export type VersionInfo = { + readonly major: number | undefined; + readonly minor: number | undefined; + readonly micro: number | undefined; + readonly release: PythonVersionRelease | undefined; +}; + +export type ResolvedVersionInfo = { + readonly major: number; + readonly minor: number; + readonly micro: number; + readonly release: PythonVersionRelease; +}; + +/** + * A record containing readonly keys. + */ +export type EnvironmentVariables = { readonly [key: string]: string | undefined }; + +export type EnvironmentVariablesChangeEvent = { + /** + * Workspace folder the environment variables changed for. + */ + readonly resource: WorkspaceFolder | undefined; + /** + * Updated value of environment variables. + */ + readonly env: EnvironmentVariables; +}; diff --git a/src/client/deprecatedProposedApiTypes.ts b/src/client/deprecatedProposedApiTypes.ts index 407cb1dab394..79b267d5b873 100644 --- a/src/client/deprecatedProposedApiTypes.ts +++ b/src/client/deprecatedProposedApiTypes.ts @@ -4,7 +4,7 @@ import { Uri, Event } from 'vscode'; import { PythonEnvKind, EnvPathType } from './pythonEnvironments/base/info'; import { ProgressNotificationEvent, GetRefreshEnvironmentsOptions } from './pythonEnvironments/base/locator'; -import { Resource } from './api/main'; +import { Resource } from './api/types'; export interface EnvironmentDetailsOptions { useCache: boolean; diff --git a/src/client/environmentApi.ts b/src/client/environmentApi.ts index ee0b28ae2ff4..da6a132b2b44 100644 --- a/src/client/environmentApi.ts +++ b/src/client/environmentApi.ts @@ -30,7 +30,7 @@ import { RefreshOptions, ResolvedEnvironment, Resource, -} from './api/main'; +} from './api/types'; import { buildEnvironmentCreationApi } from './pythonEnvironments/creation/createEnvApi'; type ActiveEnvironmentChangeEvent = { diff --git a/src/client/extension.ts b/src/client/extension.ts index 89649d377c74..a4f5bd5dbfd0 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -41,7 +41,7 @@ import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry'; import { IStartupDurations } from './types'; import { runAfterActivation } from './common/utils/runAfterActivation'; import { IInterpreterService } from './interpreter/contracts'; -import { PythonExtension } from './api/main'; +import { PythonExtension } from './api/types'; import { WorkspaceService } from './common/application/workspace'; import { disposeAll } from './common/utils/resourceLifecycle'; import { ProposedExtensionAPI } from './proposedApiTypes'; diff --git a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts index e0e79134fc56..0120b2a6e8d7 100644 --- a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts +++ b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License import { Event, Disposable, WorkspaceFolder } from 'vscode'; -import { EnvironmentTools } from '../../api/main'; +import { EnvironmentTools } from '../../api/types'; export type CreateEnvironmentUserActions = 'Back' | 'Cancel'; export type EnvironmentProviderId = string; diff --git a/src/test/api.test.ts b/src/test/api.test.ts index 488ce79073c2..f0813ce16a9b 100644 --- a/src/test/api.test.ts +++ b/src/test/api.test.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { expect } from 'chai'; -import { PythonExtension } from '../client/api/main'; +import { PythonExtension } from '../client/api/types'; import { ProposedExtensionAPI } from '../client/proposedApiTypes'; import { initialize } from './initialize'; diff --git a/src/test/common.ts b/src/test/common.ts index 0168ee47fc37..95345f91e5e0 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -10,7 +10,7 @@ import * as glob from 'glob'; import * as path from 'path'; import { coerce, SemVer } from 'semver'; import { ConfigurationTarget, Event, TextDocument, Uri } from 'vscode'; -import type { PythonExtension } from '../client/api/main'; +import type { PythonExtension } from '../client/api/types'; import { IProcessService } from '../client/common/process/types'; import { IDisposable } from '../client/common/types'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; diff --git a/src/test/environmentApi.unit.test.ts b/src/test/environmentApi.unit.test.ts index 76441247db49..1d8dc3e5c847 100644 --- a/src/test/environmentApi.unit.test.ts +++ b/src/test/environmentApi.unit.test.ts @@ -37,7 +37,7 @@ import { EnvironmentVariablesChangeEvent, EnvironmentsChangeEvent, PythonExtension, -} from '../client/api/main'; +} from '../client/api/types'; suite('Python Environment API', () => { const workspacePath = 'path/to/workspace'; diff --git a/src/test/initialize.ts b/src/test/initialize.ts index 8a7abca0d91c..add1d8624461 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import type { PythonExtension } from '../client/api/main'; +import type { PythonExtension } from '../client/api/types'; import { clearPythonPathInWorkspaceFolder, IExtensionTestApi, diff --git a/tsconfig.json b/tsconfig.json index 89f7a9c808b8..797bf6736f15 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -38,6 +38,7 @@ "src/smoke", "build", "out", - "tmp" + "tmp", + "pythonExtensionApi" ] } From f4d755634937b43c14c7db2d4d99fe17a2377204 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 14 Jul 2023 16:03:00 -0700 Subject: [PATCH 0089/1136] Update README for Python extension API to point to examples (#21638) For https://github.com/microsoft/vscode-python/issues/20949 --- build/azure-pipelines/pipeline.yml | 2 ++ pythonExtensionApi/README.md | 27 +++++++++++++++++++++++---- pythonExtensionApi/example/todo.txt | 0 3 files changed, 25 insertions(+), 4 deletions(-) delete mode 100644 pythonExtensionApi/example/todo.txt diff --git a/build/azure-pipelines/pipeline.yml b/build/azure-pipelines/pipeline.yml index 9cab7dedf9b1..85b41c16efc0 100644 --- a/build/azure-pipelines/pipeline.yml +++ b/build/azure-pipelines/pipeline.yml @@ -4,6 +4,8 @@ ############################################################################################### name: $(Date:yyyyMMdd)$(Rev:.r) +trigger: none + pr: none resources: diff --git a/pythonExtensionApi/README.md b/pythonExtensionApi/README.md index d7ca4ccfdae4..3f313721d17c 100644 --- a/pythonExtensionApi/README.md +++ b/pythonExtensionApi/README.md @@ -4,8 +4,6 @@ This npm module implements an API facade for the Python extension in VS Code. ## Example -The source code of the example can be found [here](TODO Update example extension link here) - First we need to define a `package.json` for the extension that wants to use the API: ```jsonc @@ -24,8 +22,29 @@ First we need to define a `package.json` for the extension that wants to use the } ``` -TODO insert example here +The actual source code to get the active environment to run some script could look like this: ```typescript -TODO +// Import the API +import { PythonExtension } from '@vscode/python-extension'; + +// Load the Python extension API +const pythonApi: PythonExtension = await PythonExtension.api(); + +// This will return something like /usr/bin/python +const environmentPath = pythonApi.environments.getActiveEnvironmentPath(); + +// `environmentPath.path` carries the value of the setting. Note that this path may point to a folder and not the +// python binary. Depends entirely on how the env was created. +// E.g., `conda create -n myenv python` ensures the env has a python binary +// `conda create -n myenv` does not include a python binary. +// Also, the path specified may not be valid, use the following to get complete details for this environment if +// need be. + +const environment = await pythonApi.environments.resolveEnvironment(environmentPath); +if (environment) { + // run your script here. +} ``` + +Check out [the wiki](https://aka.ms/pythonEnvironmentApi) for many more examples and usage. diff --git a/pythonExtensionApi/example/todo.txt b/pythonExtensionApi/example/todo.txt deleted file mode 100644 index e69de29bb2d1..000000000000 From fc1c391c33fad027d1e51ff60cbf648668af4b1c Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Mon, 17 Jul 2023 22:16:13 +0200 Subject: [PATCH 0090/1136] Compare global storage data using only `key` (#21636) Closes https://github.com/microsoft/vscode-python/issues/21635 by applying the same fix as done in https://github.com/microsoft/vscode-python/pull/17627. --- src/client/common/persistentState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts index 48e885a676a2..76f6d2112fe0 100644 --- a/src/client/common/persistentState.ts +++ b/src/client/common/persistentState.ts @@ -173,7 +173,7 @@ export interface IPersistentStorage { */ export function getGlobalStorage(context: IExtensionContext, key: string, defaultValue?: T): IPersistentStorage { const globalKeysStorage = new PersistentState(context.globalState, GLOBAL_PERSISTENT_KEYS, []); - const found = globalKeysStorage.value.find((value) => value.key === key && value.defaultValue === defaultValue); + const found = globalKeysStorage.value.find((value) => value.key === key); if (!found) { const newValue = [{ key, defaultValue }, ...globalKeysStorage.value]; globalKeysStorage.updateValue(newValue).ignoreErrors(); From 2e8dc67455ed779eddab6a609601026f763b6803 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 17 Jul 2023 13:55:15 -0700 Subject: [PATCH 0091/1136] Add extra logging regarding interpreter discovery (#21639) For https://github.com/microsoft/vscode-python/issues/21310 --- src/client/common/process/rawProcessApis.ts | 4 +++- src/client/common/utils/async.ts | 6 ++++++ .../base/locators/lowLevel/activeStateLocator.ts | 1 + .../base/locators/lowLevel/microsoftStoreLocator.ts | 1 + .../base/locators/lowLevel/posixKnownPathsLocator.ts | 1 + .../base/locators/lowLevel/pyenvLocator.ts | 1 + .../base/locators/lowLevel/windowsKnownPathsLocator.ts | 1 + .../base/locators/lowLevel/windowsRegistryLocator.ts | 1 + 8 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/client/common/process/rawProcessApis.ts b/src/client/common/process/rawProcessApis.ts index 025e5b607229..6f3e40d68736 100644 --- a/src/client/common/process/rawProcessApis.ts +++ b/src/client/common/process/rawProcessApis.ts @@ -100,7 +100,7 @@ export function plainExec( const deferred = createDeferred>(); const disposable: IDisposable = { dispose: () => { - if (!proc.killed && !deferred.completed) { + if (!proc.killed) { proc.kill(); } }, @@ -156,10 +156,12 @@ export function plainExec( deferred.resolve({ stdout, stderr }); } internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); }); proc.once('error', (ex) => { deferred.reject(ex); internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); }); return deferred.promise; diff --git a/src/client/common/utils/async.ts b/src/client/common/utils/async.ts index 29bf4a8d6fca..5905399cd4a1 100644 --- a/src/client/common/utils/async.ts +++ b/src/client/common/utils/async.ts @@ -50,11 +50,17 @@ class DeferredImpl implements Deferred { } public resolve(_value: T | PromiseLike) { + if (this.completed) { + return; + } this._resolve.apply(this.scope ? this.scope : this, [_value]); this._resolved = true; } public reject(_reason?: string | Error | Record) { + if (this.completed) { + return; + } this._reject.apply(this.scope ? this.scope : this, [_reason]); this._rejected = true; } diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts index 987abf4f4157..dc507b9c94bd 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts @@ -20,6 +20,7 @@ export class ActiveStateLocator extends LazyResourceBasedLocator { traceVerbose(`Couldn't locate the state binary.`); return; } + traceVerbose(`Searching for active state environments`); const projects = await state.getProjects(); if (projects === undefined) { traceVerbose(`Couldn't fetch State Tool projects.`); diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts index 9b5283f7f967..7adeeae89858 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts @@ -87,6 +87,7 @@ export class MicrosoftStoreLocator extends FSWatchingLocator { protected doIterEnvs(): IPythonEnvsIterator { const iterator = async function* (kind: PythonEnvKind) { + traceVerbose('Searching for windows store envs'); const exes = await getMicrosoftStorePythonExes(); yield* exes.map(async (executablePath: string) => ({ kind, diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts index 97726307c573..2e4e2dc13e61 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts @@ -26,6 +26,7 @@ export class PosixKnownPathsLocator extends Locator { } const iterator = async function* (kind: PythonEnvKind) { + traceVerbose('Searching for interpreters in posix paths locator'); // Filter out pyenv shims. They are not actual python binaries, they are used to launch // the binaries specified in .python-version file in the cwd. We should not be reporting // those binaries as environments. diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts index dc3290c9993c..4fd1891a179c 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts @@ -16,6 +16,7 @@ import { traceError, traceVerbose } from '../../../../logging'; * all the environments (global or virtual) in that directory. */ async function* getPyenvEnvironments(): AsyncIterableIterator { + traceVerbose('Searching for pyenv environments'); const pyenvVersionDir = getPyenvVersionsDir(); const subDirs = getSubDirs(pyenvVersionDir, { resolveSymlinks: true }); diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts index 377b1117b858..337a8fb09a97 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts @@ -93,6 +93,7 @@ function getDirFilesLocator( // rather than in each low-level locator. In the meantime we // take a naive approach. async function* iterEnvs(query: PythonLocatorQuery): IPythonEnvsIterator { + traceVerbose('Searching for windows path interpreters'); yield* await getEnvs(locator.iterEnvs(query)); traceVerbose('Finished searching for windows path interpreters'); } diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts index 954d1bfd2a41..b52e9c35779f 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts @@ -14,6 +14,7 @@ export class WindowsRegistryLocator extends Locator { // eslint-disable-next-line class-methods-use-this public iterEnvs(): IPythonEnvsIterator { const iterator = async function* () { + traceVerbose('Searching for windows registry interpreters'); const interpreters = await getRegistryInterpreters(); for (const interpreter of interpreters) { try { From f7125dadd5422c31b26cb1d50f36fb638a6c34f0 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 17 Jul 2023 16:13:11 -0700 Subject: [PATCH 0092/1136] Use correct `tsconfig.json` when generating npm package (#21651) For #21631 - Unset `removeComment` as that leads to declarations without docstrings - Set to generate declarations - Use updated typescript which results in cleaner declaration files --- pythonExtensionApi/.eslintrc | 8 ++++++++ pythonExtensionApi/README.md | 2 ++ pythonExtensionApi/package-lock.json | 26 ++++++++++++++++++++++++-- pythonExtensionApi/package.json | 10 +++++++--- pythonExtensionApi/src/main.ts | 5 +++++ pythonExtensionApi/tsconfig.json | 2 +- 6 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 pythonExtensionApi/.eslintrc diff --git a/pythonExtensionApi/.eslintrc b/pythonExtensionApi/.eslintrc new file mode 100644 index 000000000000..cbf1a6350029 --- /dev/null +++ b/pythonExtensionApi/.eslintrc @@ -0,0 +1,8 @@ +{ + "rules": { + "padding-line-between-statements": [ + "error", + { "blankLine": "always", "prev": "export", "next": "*" } + ] + } +} diff --git a/pythonExtensionApi/README.md b/pythonExtensionApi/README.md index 3f313721d17c..1587635aae40 100644 --- a/pythonExtensionApi/README.md +++ b/pythonExtensionApi/README.md @@ -28,6 +28,8 @@ The actual source code to get the active environment to run some script could lo // Import the API import { PythonExtension } from '@vscode/python-extension'; +... + // Load the Python extension API const pythonApi: PythonExtension = await PythonExtension.api(); diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index 9c7c4046870e..a8abd6d85bfc 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -1,16 +1,19 @@ { "name": "@vscode/python-extension", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vscode/python-extension", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@types/vscode": "^1.78.0" }, + "devDependencies": { + "typescript": "5.0.4" + }, "engines": { "node": ">=16.17.1", "vscode": "^1.78.0" @@ -20,6 +23,19 @@ "version": "1.80.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.80.0.tgz", "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==" + }, + "node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } } }, "dependencies": { @@ -27,6 +43,12 @@ "version": "1.80.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.80.0.tgz", "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==" + }, + "typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true } } } diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index 43b4c19877c3..aaeaaf54a0c4 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -1,7 +1,7 @@ { "name": "@vscode/python-extension", "description": "An API facade for the Python extension in VS Code", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Microsoft Corporation" }, @@ -28,13 +28,17 @@ "dependencies": { "@types/vscode": "^1.78.0" }, + "devDependencies": { + "typescript": "5.0.4" + }, "scripts": { "prepublishOnly": "echo \"⛔ Can only publish from a secure pipeline ⛔\" && node ../build/fail", "prepack": "npm run all:publish", - "compile": "node ../node_modules/typescript/lib/tsc.js -b ./tsconfig.json", + "compile": "node ./node_modules/typescript/lib/tsc.js -b ./tsconfig.json", "clean": "node ../node_modules/rimraf/bin.js out", "lint": "node ../node_modules/eslint/bin/eslint.js --ext ts src", "all": "npm run clean && npm run compile", - "all:publish": "git clean -xfd . && npm install && npm run compile" + "formatTypings": "node ../node_modules/eslint/bin/eslint.js --fix ./out/main.d.ts", + "all:publish": "git clean -xfd . && npm install && npm run compile && npm run formatTypings" } } diff --git a/pythonExtensionApi/src/main.ts b/pythonExtensionApi/src/main.ts index b9266a732826..b980a06b72f8 100644 --- a/pythonExtensionApi/src/main.ts +++ b/pythonExtensionApi/src/main.ts @@ -316,6 +316,7 @@ export type EnvironmentPath = { * was contributed. */ export type EnvironmentTools = KnownEnvironmentTools | string; + /** * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink * once tools have their own separate extensions. @@ -334,6 +335,7 @@ export type KnownEnvironmentTools = * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. */ export type EnvironmentType = KnownEnvironmentTypes | string; + /** * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their * own separate extensions, in which case they're expected to provide the type themselves. @@ -392,6 +394,9 @@ export const PVSC_EXTENSION_ID = 'ms-python.python'; // eslint-disable-next-line @typescript-eslint/no-namespace export namespace PythonExtension { + /** + * Returns the API exposed by the Python extension in VS Code. + */ export async function api(): Promise { const extension = extensions.getExtension(PVSC_EXTENSION_ID); if (extension === undefined) { diff --git a/pythonExtensionApi/tsconfig.json b/pythonExtensionApi/tsconfig.json index d90209b3a4b6..9ab7617023df 100644 --- a/pythonExtensionApi/tsconfig.json +++ b/pythonExtensionApi/tsconfig.json @@ -25,7 +25,7 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "resolveJsonModule": true, - "removeComments": true + "declaration": true }, "exclude": [ "node_modules", From c1442000d8582e081054e48501b32f4d13c07b01 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 17 Jul 2023 16:53:45 -0700 Subject: [PATCH 0093/1136] Modify .eslintrc to turn off any errors for declaration files (#21652) For #21631 --- pythonExtensionApi/.eslintrc | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pythonExtensionApi/.eslintrc b/pythonExtensionApi/.eslintrc index cbf1a6350029..8828c49002ed 100644 --- a/pythonExtensionApi/.eslintrc +++ b/pythonExtensionApi/.eslintrc @@ -1,8 +1,11 @@ { - "rules": { - "padding-line-between-statements": [ - "error", - { "blankLine": "always", "prev": "export", "next": "*" } - ] - } + "overrides": [ + { + "files": ["**/main.d.ts"], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "padding-line-between-statements": ["error", { "blankLine": "always", "prev": "export", "next": "*" }] + } + } + ] } From 81ae205e871d4326e7b549ee2667844c50e16c34 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 18 Jul 2023 16:34:58 -0700 Subject: [PATCH 0094/1136] Bring back feature to Run Python file in dedicated terminal (#21656) Closes https://github.com/microsoft/vscode-python/issues/21282 Closes https://github.com/microsoft/vscode-python/issues/21420 Closes https://github.com/microsoft/vscode-python/issues/21215 Reverts microsoft/vscode-python#21418 --- package.json | 19 +++++++ package.nls.json | 1 + src/client/common/constants.ts | 1 + src/client/common/terminal/factory.ts | 21 +++++-- src/client/common/terminal/types.ts | 2 +- src/client/telemetry/index.ts | 6 ++ .../codeExecution/codeExecutionManager.ts | 56 ++++++++++++------- .../codeExecution/terminalCodeExecution.ts | 40 +++++++------ src/client/terminals/types.ts | 2 +- .../common/terminals/factory.unit.test.ts | 47 +++++++++++++++- .../codeExecutionManager.unit.test.ts | 25 ++++++--- .../terminalCodeExec.unit.test.ts | 29 +++++++++- 12 files changed, 190 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index e38e6921e50a..abadcdb3bb0b 100644 --- a/package.json +++ b/package.json @@ -392,6 +392,12 @@ "icon": "$(play)", "title": "%python.command.python.execInTerminalIcon.title%" }, + { + "category": "Python", + "command": "python.execInDedicatedTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInDedicatedTerminal.title%" + }, { "category": "Python", "command": "python.debugInTerminal", @@ -1818,6 +1824,13 @@ "title": "%python.command.python.execInTerminalIcon.title%", "when": "false" }, + { + "category": "Python", + "command": "python.execInDedicatedTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInDedicatedTerminal.title%", + "when": "false" + }, { "category": "Python", "command": "python.debugInTerminal", @@ -1976,6 +1989,12 @@ "title": "%python.command.python.execInTerminalIcon.title%", "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" }, + { + "command": "python.execInDedicatedTerminal", + "group": "navigation@0", + "title": "%python.command.python.execInDedicatedTerminal.title%", + "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace && shellExecutionSupported" + }, { "command": "python.debugInTerminal", "group": "navigation@1", diff --git a/package.nls.json b/package.nls.json index 84c920b7c22e..79609f02e83a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -7,6 +7,7 @@ "python.command.python.execInTerminal.title": "Run Python File in Terminal", "python.command.python.debugInTerminal.title": "Debug Python File", "python.command.python.execInTerminalIcon.title": "Run Python File", + "python.command.python.execInDedicatedTerminal.title": "Run Python File in Dedicated Terminal", "python.command.python.setInterpreter.title": "Select Interpreter", "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", "python.command.python.viewOutput.title": "Show Output", diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index b285667aaa6a..bea0ef9e235c 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -44,6 +44,7 @@ export namespace Commands { export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; export const Exec_In_Terminal = 'python.execInTerminal'; export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; + export const Exec_In_Separate_Terminal = 'python.execInDedicatedTerminal'; export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; export const GetSelectedInterpreterPath = 'python.interpreterPath'; diff --git a/src/client/common/terminal/factory.ts b/src/client/common/terminal/factory.ts index 3855cb6cee3c..39cc88c4b024 100644 --- a/src/client/common/terminal/factory.ts +++ b/src/client/common/terminal/factory.ts @@ -3,6 +3,7 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; +import * as path from 'path'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; @@ -23,13 +24,17 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { ) { this.terminalServices = new Map(); } - public getTerminalService(options: TerminalCreationOptions): ITerminalService { + public getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService { const resource = options?.resource; const title = options?.title; - const terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; + let terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; const interpreter = options?.interpreter; - const id = this.getTerminalId(terminalTitle, resource, interpreter); + const id = this.getTerminalId(terminalTitle, resource, interpreter, options.newTerminalPerFile); if (!this.terminalServices.has(id)) { + if (resource && options.newTerminalPerFile) { + terminalTitle = `${terminalTitle}: ${path.basename(resource.fsPath).replace('.py', '')}`; + } + options.title = terminalTitle; const terminalService = new TerminalService(this.serviceContainer, options); this.terminalServices.set(id, terminalService); } @@ -46,13 +51,19 @@ export class TerminalServiceFactory implements ITerminalServiceFactory { title = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; return new TerminalService(this.serviceContainer, { resource, title }); } - private getTerminalId(title: string, resource?: Uri, interpreter?: PythonEnvironment): string { + private getTerminalId( + title: string, + resource?: Uri, + interpreter?: PythonEnvironment, + newTerminalPerFile?: boolean, + ): string { if (!resource && !interpreter) { return title; } const workspaceFolder = this.serviceContainer .get(IWorkspaceService) .getWorkspaceFolder(resource || undefined); - return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}`; + const fileId = resource && newTerminalPerFile ? resource.fsPath : ''; + return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}:${fileId}`; } } diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts index 880bf0dd72fb..303188682378 100644 --- a/src/client/common/terminal/types.ts +++ b/src/client/common/terminal/types.ts @@ -97,7 +97,7 @@ export interface ITerminalServiceFactory { * @returns {ITerminalService} * @memberof ITerminalServiceFactory */ - getTerminalService(options: TerminalCreationOptions): ITerminalService; + getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService; createTerminalService(resource?: Uri, title?: string): ITerminalService; } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 50d7ebb0f9b1..f0b57a4043d9 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -827,6 +827,12 @@ export interface IEventNamePropertyMapping { * @type {('command' | 'icon')} */ trigger?: 'command' | 'icon'; + /** + * Whether user chose to execute this Python file in a separate terminal or not. + * + * @type {boolean} + */ + newTerminalPerFile?: boolean; }; /** * Telemetry Event sent when user executes code against Django Shell. diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index ed671f2846a2..9f1ba6e90d90 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -36,25 +36,31 @@ export class CodeExecutionManager implements ICodeExecutionManager { } public registerCommands() { - [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon].forEach((cmd) => { - this.disposableRegistry.push( - this.commandManager.registerCommand(cmd as any, async (file: Resource) => { - const interpreterService = this.serviceContainer.get(IInterpreterService); - const interpreter = await interpreterService.getActiveInterpreter(file); - if (!interpreter) { - this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); - return; - } - const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; - await this.executeFileInTerminal(file, trigger) - .then(() => { - if (this.shouldTerminalFocusOnStart(file)) - this.commandManager.executeCommand('workbench.action.terminal.focus'); + [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon, Commands.Exec_In_Separate_Terminal].forEach( + (cmd) => { + this.disposableRegistry.push( + this.commandManager.registerCommand(cmd as any, async (file: Resource) => { + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager + .executeCommand(Commands.TriggerEnvironmentSelection, file) + .then(noop, noop); + return; + } + const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; + await this.executeFileInTerminal(file, trigger, { + newTerminalPerFile: cmd === Commands.Exec_In_Separate_Terminal, }) - .catch((ex) => traceError('Failed to execute file in terminal', ex)); - }), - ); - }); + .then(() => { + if (this.shouldTerminalFocusOnStart(file)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); + }) + .catch((ex) => traceError('Failed to execute file in terminal', ex)); + }), + ); + }, + ); this.disposableRegistry.push( this.commandManager.registerCommand(Commands.Exec_Selection_In_Terminal as any, async (file: Resource) => { const interpreterService = this.serviceContainer.get(IInterpreterService); @@ -87,8 +93,16 @@ export class CodeExecutionManager implements ICodeExecutionManager { ), ); } - private async executeFileInTerminal(file: Resource, trigger: 'command' | 'icon') { - sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'file', trigger }); + private async executeFileInTerminal( + file: Resource, + trigger: 'command' | 'icon', + options?: { newTerminalPerFile: boolean }, + ): Promise { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { + scope: 'file', + trigger, + newTerminalPerFile: options?.newTerminalPerFile, + }); const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); file = file instanceof Uri ? file : undefined; let fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); @@ -110,7 +124,7 @@ export class CodeExecutionManager implements ICodeExecutionManager { } const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); - await executionService.executeFile(fileToExecute); + await executionService.executeFile(fileToExecute, options); } @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index 9261483b45e1..4c329e939599 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -10,7 +10,7 @@ import { IWorkspaceService } from '../../common/application/types'; import '../../common/extensions'; import { IPlatformService } from '../../common/platform/types'; import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types'; -import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; import { IInterpreterService } from '../../interpreter/contracts'; import { buildPythonExecInfo, PythonExecInfo } from '../../pythonEnvironments/exec'; import { ICodeExecutionService } from '../../terminals/types'; @@ -19,7 +19,6 @@ import { ICodeExecutionService } from '../../terminals/types'; export class TerminalCodeExecutionProvider implements ICodeExecutionService { private hasRanOutsideCurrentDrive = false; protected terminalTitle!: string; - private _terminalService!: ITerminalService; private replActive?: Promise; constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, @@ -30,13 +29,13 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { @inject(IInterpreterService) protected readonly interpreterService: IInterpreterService, ) {} - public async executeFile(file: Uri) { + public async executeFile(file: Uri, options?: { newTerminalPerFile: boolean }) { await this.setCwdForFileExecution(file); const { command, args } = await this.getExecuteFileArgs(file, [ file.fsPath.fileToCommandArgumentForPythonExt(), ]); - await this.getTerminalService(file).sendCommand(command, args); + await this.getTerminalService(file, options).sendCommand(command, args); } public async execute(code: string, resource?: Uri): Promise { @@ -44,21 +43,27 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { return; } - await this.initializeRepl(); + await this.initializeRepl(resource); await this.getTerminalService(resource).sendText(code); } - public async initializeRepl(resource?: Uri) { + public async initializeRepl(resource: Resource) { + const terminalService = this.getTerminalService(resource); if (this.replActive && (await this.replActive)) { - await this._terminalService.show(); + await terminalService.show(); return; } this.replActive = new Promise(async (resolve) => { const replCommandArgs = await this.getExecutableInfo(resource); - await this.getTerminalService(resource).sendCommand(replCommandArgs.command, replCommandArgs.args); + terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args); // Give python repl time to start before we start sending text. setTimeout(() => resolve(true), 1000); }); + this.disposables.push( + terminalService.onDidCloseTerminal(() => { + this.replActive = undefined; + }), + ); await this.replActive; } @@ -76,19 +81,12 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise { return this.getExecutableInfo(resource, executeArgs); } - private getTerminalService(resource?: Uri): ITerminalService { - if (!this._terminalService) { - this._terminalService = this.terminalServiceFactory.getTerminalService({ - resource, - title: this.terminalTitle, - }); - this.disposables.push( - this._terminalService.onDidCloseTerminal(() => { - this.replActive = undefined; - }), - ); - } - return this._terminalService; + private getTerminalService(resource: Resource, options?: { newTerminalPerFile: boolean }): ITerminalService { + return this.terminalServiceFactory.getTerminalService({ + resource, + title: this.terminalTitle, + newTerminalPerFile: options?.newTerminalPerFile, + }); } private async setCwdForFileExecution(file: Uri) { const pythonSettings = this.configurationService.getSettings(file); diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index cf31f4ef1dd0..47ac16d9e08b 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -8,7 +8,7 @@ export const ICodeExecutionService = Symbol('ICodeExecutionService'); export interface ICodeExecutionService { execute(code: string, resource?: Uri): Promise; - executeFile(file: Uri): Promise; + executeFile(file: Uri, options?: { newTerminalPerFile: boolean }): Promise; initializeRepl(resource?: Uri): Promise; } diff --git a/src/test/common/terminals/factory.unit.test.ts b/src/test/common/terminals/factory.unit.test.ts index ef6b7d8f5b0f..5ad2da8e793a 100644 --- a/src/test/common/terminals/factory.unit.test.ts +++ b/src/test/common/terminals/factory.unit.test.ts @@ -105,7 +105,7 @@ suite('Terminal Service Factory', () => { expect(notSameAsThirdInstance).to.not.equal(true, 'Instances are the same'); }); - test('Ensure same terminal is returned when using resources from the same workspace', () => { + test('Ensure same terminal is returned when using different resources from the same workspace', () => { const file1A = Uri.file('1a'); const file2A = Uri.file('2a'); const fileB = Uri.file('b'); @@ -140,4 +140,49 @@ suite('Terminal Service Factory', () => { 'Instances should be different for different workspaces', ); }); + + test('When `newTerminalPerFile` is true, ensure different terminal is returned when using different resources from the same workspace', () => { + const file1A = Uri.file('1a'); + const file2A = Uri.file('2a'); + const fileB = Uri.file('b'); + const workspaceUriA = Uri.file('A'); + const workspaceUriB = Uri.file('B'); + const workspaceFolderA = TypeMoq.Mock.ofType(); + workspaceFolderA.setup((w) => w.uri).returns(() => workspaceUriA); + const workspaceFolderB = TypeMoq.Mock.ofType(); + workspaceFolderB.setup((w) => w.uri).returns(() => workspaceUriB); + + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file1A))) + .returns(() => workspaceFolderA.object); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file2A))) + .returns(() => workspaceFolderA.object); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(fileB))) + .returns(() => workspaceFolderB.object); + + const terminalForFile1A = factory.getTerminalService({ + resource: file1A, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalForFile2A = factory.getTerminalService({ + resource: file2A, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + const terminalForFileB = factory.getTerminalService({ + resource: fileB, + newTerminalPerFile: true, + }) as SynchronousTerminalService; + + const terminalsAreSameForWorkspaceA = terminalForFile1A.terminalService === terminalForFile2A.terminalService; + expect(terminalsAreSameForWorkspaceA).to.equal(false, 'Instances are the same for Workspace A'); + + const terminalsForWorkspaceABAreDifferent = + terminalForFile1A.terminalService === terminalForFileB.terminalService; + expect(terminalsForWorkspaceABAreDifferent).to.equal( + false, + 'Instances should be different for different workspaces', + ); + }); }); diff --git a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts index 3676834873a0..30f95c94d217 100644 --- a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +++ b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -77,12 +77,15 @@ suite('Terminal - Code Execution Manager', () => { executionManager.registerCommands(); const sorted = registered.sort(); - expect(sorted).to.deep.equal([ - Commands.Exec_In_Terminal, - Commands.Exec_In_Terminal_Icon, - Commands.Exec_Selection_In_Django_Shell, - Commands.Exec_Selection_In_Terminal, - ]); + expect(sorted).to.deep.equal( + [ + Commands.Exec_In_Separate_Terminal, + Commands.Exec_In_Terminal, + Commands.Exec_In_Terminal_Icon, + Commands.Exec_Selection_In_Django_Shell, + Commands.Exec_Selection_In_Terminal, + ].sort(), + ); }); test('Ensure executeFileInterTerminal will do nothing if no file is avialble', async () => { @@ -135,7 +138,10 @@ suite('Terminal - Code Execution Manager', () => { const fileToExecute = Uri.file('x'); await commandHandler!(fileToExecute); helper.verify(async (h) => h.getFileToExecute(), TypeMoq.Times.never()); - executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + executionService.verify( + async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); test('Ensure executeFileInterTerminal will use active file', async () => { @@ -164,7 +170,10 @@ suite('Terminal - Code Execution Manager', () => { .returns(() => executionService.object); await commandHandler!(fileToExecute); - executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + executionService.verify( + async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); async function testExecutionOfSelectionWithoutAnyActiveDocument(commandId: string, executionSericeId: string) { diff --git a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts index 1f33b619fad0..9523f35a1040 100644 --- a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts +++ b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts @@ -25,7 +25,7 @@ import { TerminalCodeExecutionProvider } from '../../../client/terminals/codeExe import { ICodeExecutionService } from '../../../client/terminals/types'; import { PYTHON_PATH } from '../../common'; import * as sinon from 'sinon'; -import assert from 'assert'; +import { assert } from 'chai'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { IInterpreterService } from '../../../client/interpreter/contracts'; @@ -390,6 +390,7 @@ suite('Terminal - Code Execution', () => { const env = await createCondaEnv(condaEnv, procService.object, fileSystem.object); if (!env) { assert(false, 'Should not be undefined for conda version 4.9.0'); + return; } const procs = createPythonProcessService(procService.object, env); const condaExecutionService = { @@ -509,6 +510,7 @@ suite('Terminal - Code Execution', () => { const env = await createCondaEnv(condaEnv, procService.object, fileSystem.object); if (!env) { assert(false, 'Should not be undefined for conda version 4.9.0'); + return; } const procs = createPythonProcessService(procService.object, env); const condaExecutionService = { @@ -650,6 +652,31 @@ suite('Terminal - Code Execution', () => { await executor.execute('cmd2'); terminalService.verify(async (t) => t.sendText('cmd2'), TypeMoq.Times.once()); }); + + test('Ensure code is sent to the same terminal for a particular resource', async () => { + const resource = Uri.file('a'); + terminalFactory.reset(); + terminalFactory + .setup((f) => f.getTerminalService(TypeMoq.It.isAny())) + .callback((options: TerminalCreationOptions) => { + assert.deepEqual(options.resource, resource); + }) + .returns(() => terminalService.object); + + const pythonPath = 'usr/bin/python1234'; + const terminalArgs = ['-a', 'b', 'c']; + platform.setup((p) => p.isWindows).returns(() => false); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); + terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); + + await executor.execute('cmd1', resource); + terminalService.verify(async (t) => t.sendText('cmd1'), TypeMoq.Times.once()); + + await executor.execute('cmd2', resource); + terminalService.verify(async (t) => t.sendText('cmd2'), TypeMoq.Times.once()); + }); }); }); }); From c25667861a963a17b3ba934c995a15944048bfeb Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 18 Jul 2023 17:26:39 -0700 Subject: [PATCH 0095/1136] Prevent posix paths locator from crashing (#21657) For https://github.com/microsoft/vscode-python/issues/21310 --- .../lowLevel/posixKnownPathsLocator.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts index 2e4e2dc13e61..4cacbd53f5aa 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts @@ -27,23 +27,27 @@ export class PosixKnownPathsLocator extends Locator { const iterator = async function* (kind: PythonEnvKind) { traceVerbose('Searching for interpreters in posix paths locator'); - // Filter out pyenv shims. They are not actual python binaries, they are used to launch - // the binaries specified in .python-version file in the cwd. We should not be reporting - // those binaries as environments. - const knownDirs = (await commonPosixBinPaths()).filter((dirname) => !isPyenvShimDir(dirname)); - let pythonBinaries = await getPythonBinFromPosixPaths(knownDirs); + try { + // Filter out pyenv shims. They are not actual python binaries, they are used to launch + // the binaries specified in .python-version file in the cwd. We should not be reporting + // those binaries as environments. + const knownDirs = (await commonPosixBinPaths()).filter((dirname) => !isPyenvShimDir(dirname)); + let pythonBinaries = await getPythonBinFromPosixPaths(knownDirs); - // Filter out MacOS system installs of Python 2 if necessary. - if (isMacPython2Deprecated) { - pythonBinaries = pythonBinaries.filter((binary) => !isMacDefaultPythonPath(binary)); - } + // Filter out MacOS system installs of Python 2 if necessary. + if (isMacPython2Deprecated) { + pythonBinaries = pythonBinaries.filter((binary) => !isMacDefaultPythonPath(binary)); + } - for (const bin of pythonBinaries) { - try { - yield { executablePath: bin, kind, source: [PythonEnvSource.PathEnvVar] }; - } catch (ex) { - traceError(`Failed to process environment: ${bin}`, ex); + for (const bin of pythonBinaries) { + try { + yield { executablePath: bin, kind, source: [PythonEnvSource.PathEnvVar] }; + } catch (ex) { + traceError(`Failed to process environment: ${bin}`, ex); + } } + } catch (ex) { + traceError('Failed to process posix paths', ex); } traceVerbose('Finished searching for interpreters in posix paths locator'); }; From be334bdb07199c614cbcf22d4a6d2fc84d53e3f8 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 19 Jul 2023 14:16:23 -0700 Subject: [PATCH 0096/1136] Do not resolve symbolic links in posix locator if they exceed the count limit (#21658) Closes https://github.com/microsoft/vscode-python/issues/21310 Fixes interpreter discovery running forever for non-Windows OS --- .../base/locators/lowLevel/posixKnownPathsLocator.ts | 1 + .../pythonEnvironments/common/externalDependencies.ts | 11 ++++++++--- src/client/pythonEnvironments/common/posixUtils.ts | 3 ++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts index 4cacbd53f5aa..2d7ebc2af111 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts @@ -33,6 +33,7 @@ export class PosixKnownPathsLocator extends Locator { // those binaries as environments. const knownDirs = (await commonPosixBinPaths()).filter((dirname) => !isPyenvShimDir(dirname)); let pythonBinaries = await getPythonBinFromPosixPaths(knownDirs); + traceVerbose(`Found ${pythonBinaries.length} python binaries in posix paths`); // Filter out MacOS system installs of Python 2 if necessary. if (isMacPython2Deprecated) { diff --git a/src/client/pythonEnvironments/common/externalDependencies.ts b/src/client/pythonEnvironments/common/externalDependencies.ts index 54f614ebdd49..ecb6f2212aba 100644 --- a/src/client/pythonEnvironments/common/externalDependencies.ts +++ b/src/client/pythonEnvironments/common/externalDependencies.ts @@ -10,7 +10,7 @@ import { IDisposable, IConfigurationService } from '../../common/types'; import { chain, iterable } from '../../common/utils/async'; import { getOSType, OSType } from '../../common/utils/platform'; import { IServiceContainer } from '../../ioc/types'; -import { traceVerbose } from '../../logging'; +import { traceError, traceVerbose } from '../../logging'; let internalServiceContainer: IServiceContainer; export function initializeExternalDependencies(serviceContainer: IServiceContainer): void { @@ -93,16 +93,21 @@ export function arePathsSame(path1: string, path2: string): boolean { return normCasePath(path1) === normCasePath(path2); } -export async function resolveSymbolicLink(absPath: string, stats?: fsapi.Stats): Promise { +export async function resolveSymbolicLink(absPath: string, stats?: fsapi.Stats, count?: number): Promise { stats = stats ?? (await fsapi.lstat(absPath)); if (stats.isSymbolicLink()) { + if (count && count > 5) { + traceError(`Detected a potential symbolic link loop at ${absPath}, terminating resolution.`); + return absPath; + } const link = await fsapi.readlink(absPath); // Result from readlink is not guaranteed to be an absolute path. For eg. on Mac it resolves // /usr/local/bin/python3.9 -> ../../../Library/Frameworks/Python.framework/Versions/3.9/bin/python3.9 // // The resultant path is reported relative to the symlink directory we resolve. Convert that to absolute path. const absLinkPath = path.isAbsolute(link) ? link : path.resolve(path.dirname(absPath), link); - return resolveSymbolicLink(absLinkPath); + count = count ? count + 1 : 1; + return resolveSymbolicLink(absLinkPath, undefined, count); } return absPath; } diff --git a/src/client/pythonEnvironments/common/posixUtils.ts b/src/client/pythonEnvironments/common/posixUtils.ts index cd8f62bf9a08..eb60fc029949 100644 --- a/src/client/pythonEnvironments/common/posixUtils.ts +++ b/src/client/pythonEnvironments/common/posixUtils.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import { uniq } from 'lodash'; import { getSearchPathEntries } from '../../common/utils/exec'; import { resolveSymbolicLink } from './externalDependencies'; -import { traceError, traceInfo } from '../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; /** * Determine if the given filename looks like the simplest Python executable. @@ -123,6 +123,7 @@ export async function getPythonBinFromPosixPaths(searchDirs: string[]): Promise< // Ensure that we have a collection of unique global binaries by // resolving all symlinks to the target binaries. try { + traceVerbose(`Attempting to resolve symbolic link: ${filepath}`); const resolvedBin = await resolveSymbolicLink(filepath); if (binToLinkMap.has(resolvedBin)) { binToLinkMap.get(resolvedBin)?.push(filepath); From 713007f035b77d2724f4a471b97f53db1a9a12e3 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 19 Jul 2023 14:45:18 -0700 Subject: [PATCH 0097/1136] correct discovery on unittest skip at file level (#21665) given a file called skip_test_file_node.py that has `raise SkipTest(".....")` this should appear in the sidebar with no children. The bug is that currently it shows a "unittest" node that gives "loader" and other incorrect nodes below it. --- .../.data/unittest_skip/unittest_skip_file.py | 10 +++ .../unittest_skip/unittest_skip_function.py | 18 +++++ .../expected_discovery_test_output.py | 68 +++++++++++++++++++ .../tests/unittestadapter/test_discovery.py | 21 +++++- pythonFiles/unittestadapter/utils.py | 8 +++ 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py create mode 100644 pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py create mode 100644 pythonFiles/tests/unittestadapter/expected_discovery_test_output.py diff --git a/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py b/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py new file mode 100644 index 000000000000..927a56bc920b --- /dev/null +++ b/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from unittest import SkipTest + +raise SkipTest("This is unittest.SkipTest calling") + + +def test_example(): + assert 1 == 1 diff --git a/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py b/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py new file mode 100644 index 000000000000..59e66e9a1d40 --- /dev/null +++ b/pythonFiles/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + + +def add(x, y): + return x + y + + +class SimpleTest(unittest.TestCase): + @unittest.skip("demonstrating skipping") + def testadd1(self): + self.assertEquals(add(4, 5), 9) + + +if __name__ == "__main__": + unittest.main() diff --git a/pythonFiles/tests/unittestadapter/expected_discovery_test_output.py b/pythonFiles/tests/unittestadapter/expected_discovery_test_output.py new file mode 100644 index 000000000000..3043ec158a2e --- /dev/null +++ b/pythonFiles/tests/unittestadapter/expected_discovery_test_output.py @@ -0,0 +1,68 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +from unittestadapter.utils import TestNodeTypeEnum +from .helpers import TEST_DATA_PATH + +skip_unittest_folder_discovery_output = { + "path": os.fspath(TEST_DATA_PATH / "unittest_skip"), + "name": "unittest_skip", + "type_": TestNodeTypeEnum.folder, + "children": [ + { + "path": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_file.py" + ), + "name": "unittest_skip_file.py", + "type_": TestNodeTypeEnum.file, + "children": [], + "id_": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_file.py" + ), + }, + { + "path": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" + ), + "name": "unittest_skip_function.py", + "type_": TestNodeTypeEnum.file, + "children": [ + { + "path": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" + ), + "name": "SimpleTest", + "type_": TestNodeTypeEnum.class_, + "children": [ + { + "name": "testadd1", + "path": os.fspath( + TEST_DATA_PATH + / "unittest_skip" + / "unittest_skip_function.py" + ), + "lineno": "13", + "type_": TestNodeTypeEnum.test, + "id_": os.fspath( + TEST_DATA_PATH + / "unittest_skip" + / "unittest_skip_function.py" + ) + + "\\SimpleTest\\testadd1", + "runID": "unittest_skip_function.SimpleTest.testadd1", + } + ], + "id_": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" + ) + + "\\SimpleTest", + } + ], + "id_": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" + ), + }, + ], + "id_": os.fspath(TEST_DATA_PATH / "unittest_skip"), +} diff --git a/pythonFiles/tests/unittestadapter/test_discovery.py b/pythonFiles/tests/unittestadapter/test_discovery.py index 28dc51f55dcd..c4778aa85852 100644 --- a/pythonFiles/tests/unittestadapter/test_discovery.py +++ b/pythonFiles/tests/unittestadapter/test_discovery.py @@ -12,7 +12,7 @@ parse_discovery_cli_args, ) from unittestadapter.utils import TestNodeTypeEnum, parse_unittest_args - +from . import expected_discovery_test_output from .helpers import TEST_DATA_PATH, is_same_tree @@ -214,3 +214,22 @@ def test_error_discovery() -> None: assert actual["status"] == "error" assert is_same_tree(expected, actual.get("tests")) assert len(actual.get("error", [])) == 1 + + +def test_unit_skip() -> None: + """The discover_tests function should return a dictionary with a "success" status, a uuid, no errors, and test tree. + if unittest discovery was performed and found a test in one file marked as skipped and another file marked as skipped. + """ + start_dir = os.fsdecode(TEST_DATA_PATH / "unittest_skip") + pattern = "unittest_*" + + uuid = "some-uuid" + actual = discover_tests(start_dir, pattern, None, uuid) + + assert actual["status"] == "success" + assert "tests" in actual + assert is_same_tree( + actual.get("tests"), + expected_discovery_test_output.skip_unittest_folder_discovery_output, + ) + assert "error" not in actual diff --git a/pythonFiles/unittestadapter/utils.py b/pythonFiles/unittestadapter/utils.py index a461baf7d870..78000e2a945f 100644 --- a/pythonFiles/unittestadapter/utils.py +++ b/pythonFiles/unittestadapter/utils.py @@ -159,6 +159,14 @@ def build_test_tree( test_id = test_case.id() if test_id.startswith("unittest.loader._FailedTest"): error.append(str(test_case._exception)) # type: ignore + elif test_id.startswith("unittest.loader.ModuleSkipped"): + components = test_id.split(".") + class_name = f"{components[-1]}.py" + # Find/build class node. + file_path = os.fsdecode(os.path.join(directory_path, class_name)) + current_node = get_child_node( + class_name, file_path, TestNodeTypeEnum.file, root + ) else: # Get the static test path components: filename, class name and function name. components = test_id.split(".") From 9bcb82d65d19e51e6d057937a037b23b8feab7f5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 24 Jul 2023 07:54:51 -0700 Subject: [PATCH 0098/1136] Ensure `Run Python in dedicated terminal` uses `python.executeInFirDir` setting (#21681) --- .../base/locators/lowLevel/windowsKnownPathsLocator.ts | 6 ++++-- .../terminals/codeExecution/terminalCodeExecution.ts | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts index 337a8fb09a97..5bfc62d99d48 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts @@ -94,8 +94,10 @@ function getDirFilesLocator( // take a naive approach. async function* iterEnvs(query: PythonLocatorQuery): IPythonEnvsIterator { traceVerbose('Searching for windows path interpreters'); - yield* await getEnvs(locator.iterEnvs(query)); - traceVerbose('Finished searching for windows path interpreters'); + yield* await getEnvs(locator.iterEnvs(query)).then((res) => { + traceVerbose('Finished searching for windows path interpreters'); + return res; + }); } return { providerId: locator.providerId, diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index 4c329e939599..50270c3586c4 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -30,7 +30,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { ) {} public async executeFile(file: Uri, options?: { newTerminalPerFile: boolean }) { - await this.setCwdForFileExecution(file); + await this.setCwdForFileExecution(file, options); const { command, args } = await this.getExecuteFileArgs(file, [ file.fsPath.fileToCommandArgumentForPythonExt(), ]); @@ -88,7 +88,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { newTerminalPerFile: options?.newTerminalPerFile, }); } - private async setCwdForFileExecution(file: Uri) { + private async setCwdForFileExecution(file: Uri, options?: { newTerminalPerFile: boolean }) { const pythonSettings = this.configurationService.getSettings(file); if (!pythonSettings.terminal.executeInFileDir) { return; @@ -106,7 +106,9 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { await this.getTerminalService(file).sendText(`${fileDrive}:`); } } - await this.getTerminalService(file).sendText(`cd ${fileDirPath.fileToCommandArgumentForPythonExt()}`); + await this.getTerminalService(file, options).sendText( + `cd ${fileDirPath.fileToCommandArgumentForPythonExt()}`, + ); } } } From 73a0e9ddd853202614c48ae605b5f8f65e28bc43 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 24 Jul 2023 09:54:01 -0700 Subject: [PATCH 0099/1136] handle skip unittest at file without error (#21678) fixes https://github.com/microsoft/vscode-python/issues/21653 --- .../.data/unittest_skiptest_file_level.py | 13 +++++++++++++ .../pytestadapter/expected_discovery_test_output.py | 10 ++++++++++ pythonFiles/tests/pytestadapter/test_discovery.py | 4 ++++ pythonFiles/vscode_pytest/__init__.py | 2 ++ 4 files changed, 29 insertions(+) create mode 100644 pythonFiles/tests/pytestadapter/.data/unittest_skiptest_file_level.py diff --git a/pythonFiles/tests/pytestadapter/.data/unittest_skiptest_file_level.py b/pythonFiles/tests/pytestadapter/.data/unittest_skiptest_file_level.py new file mode 100644 index 000000000000..362c74cbb76f --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/unittest_skiptest_file_level.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +from unittest import SkipTest + +# Due to the skip at the file level, no tests will be discovered. +raise SkipTest("Skip all tests in this file, they should not be recognized by pytest.") + + +class SimpleTest(unittest.TestCase): + def testadd1(self): + assert True diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index fb8234350fb4..91c1453dfc77 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -100,6 +100,16 @@ "id_": TEST_DATA_PATH_STR, } +# This is the expected output for the unittest_skip_file_level test. +# └── unittest_skiptest_file_level.py +unittest_skip_file_level_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [], + "id_": TEST_DATA_PATH_STR, +} + # This is the expected output for the unittest_folder tests # └── unittest_folder # ├── test_add.py diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 02ea1ddcd871..5288c7ad769e 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -87,6 +87,10 @@ def test_parameterized_error_collect(): @pytest.mark.parametrize( "file, expected_const", [ + ( + "unittest_skiptest_file_level.py", + expected_discovery_test_output.unittest_skip_file_level_expected_output, + ), ( "param_same_name", expected_discovery_test_output.param_same_name_expected_output, diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 072c5ef5d3ad..1ac287a8410a 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -73,6 +73,8 @@ def pytest_exception_interact(node, call, report): # if discovery, then add the error to error logs. if type(report) == pytest.CollectReport: if call.excinfo and call.excinfo.typename != "AssertionError": + if report.outcome == "skipped" and "SkipTest" in str(call): + return ERRORS.append( call.excinfo.exconly() + "\n Check Python Test Logs for more details." ) From 6af959d34870a43e27b2bc3059239206b8851016 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Mon, 24 Jul 2023 15:56:06 -0700 Subject: [PATCH 0100/1136] Dev Container Using MCR (#21675) Dev container rewrite using MCR. Pyenv for installing and managing python versions. Fish also installed as optional (able to view as shell option in codespaces). Also fixing conda error. Takes care of: #21591 rewrite from: #21435 to adhere to company policy. --- .devcontainer/Dockerfile | 32 +++++++---------- .devcontainer/devcontainer.json | 3 +- scripts/onCreateCommand.sh | 36 +++++++++++++++++++ scripts/postCreateCommand.sh | 15 -------- src/client/common/interpreterPathService.ts | 2 +- .../interpreterPathService.unit.test.ts | 9 +++-- .../common/commonUtils.functional.test.ts | 4 --- 7 files changed, 58 insertions(+), 43 deletions(-) create mode 100644 scripts/onCreateCommand.sh delete mode 100644 scripts/postCreateCommand.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f5f49445b399..5fbf068de65f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,26 +1,18 @@ -# This image will serve as a starting point for devcontainer.json. -# Get latest image of Fedora as the base image. -FROM docker.io/library/fedora:latest +FROM mcr.microsoft.com/devcontainers/typescript-node:16-bookworm -# Install supported python versions and nodejs. -RUN dnf -y --nodocs install /usr/bin/{python3.7,python3.8,python3.9,python3.10,python3.11,git,conda,clang} && \ - dnf clean all +RUN apt-get install -y wget bzip2 -ENV NVM_VERSION=0.39.3 -ENV NODE_VERSION=16.17.1 -ENV NPM_VERSION=8.19.3 - -# Installation instructions from https://github.com/nvm-sh/nvm . -RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v$NVM_VERSION/install.sh | bash -RUN export NVM_DIR="$HOME/.nvm" && \ - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ - [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" && \ - nvm install $NODE_VERSION && \ - npm install -g npm@$NPM_VERSION - -# For clean open source builds. -ENV DISABLE_TRANSLATIONS=true +# Run in silent mode and save downloaded script as anaconda.sh. +# Run with /bin/bash and run in silent mode to /opt/conda. +# Also get rid of installation script after finishing. +RUN wget --quiet https://repo.anaconda.com/archive/Anaconda3-2023.07-1-Linux-x86_64.sh -O ~/anaconda.sh && \ + /bin/bash ~/anaconda.sh -b -p /opt/conda && \ + rm ~/anaconda.sh +ENV PATH="/opt/conda/bin:$PATH" +# Sudo apt update needs to run in order for installation of fish to work . +RUN sudo apt update && \ + sudo apt install fish -y diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6435ba5bbda8..fe15f35764e6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,7 +21,8 @@ }, // Commands to execute on container creation,start. "postCreateCommand": "bash scripts/postCreateCommand.sh", - // Environment variable placed inside containerEnv following: https://containers.dev/implementors/json_reference/#general-properties + "onCreateCommand": "bash scripts/onCreateCommand.sh", + "containerEnv": { "CI_PYTHON_PATH": "/workspaces/vscode-python/.venv/bin/python" } diff --git a/scripts/onCreateCommand.sh b/scripts/onCreateCommand.sh new file mode 100644 index 000000000000..e93c74f610b9 --- /dev/null +++ b/scripts/onCreateCommand.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Install pyenv and Python versions here to avoid using shim. +curl https://pyenv.run | bash +echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc +echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc +# echo 'eval "$(pyenv init -)"' >> ~/.bashrc + +export PYENV_ROOT="$HOME/.pyenv" +command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" +# eval "$(pyenv init -)" Comment this out and DO NOT use shim. +source ~/.bashrc + +# Install Python via pyenv . +pyenv install 3.7:latest 3.8:latest 3.9:latest 3.10:latest 3.11:latest + +# Set default Python version to 3.7 . +pyenv global 3.7.17 + +npm ci + +# Create Virutal environment. +pyenv exec python3.7 -m venv .venv + +# Activate Virtual environment. +source /workspaces/vscode-python/.venv/bin/activate + +# Install required Python libraries. +npx gulp installPythonLibs + +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/test-requirements.txt +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/smoke-test-requirements.txt +/workspaces/vscode-python/.venv/bin/python -m pip install -r build/functional-test-requirements.txt + +# Below will crash codespace +# npm run compile diff --git a/scripts/postCreateCommand.sh b/scripts/postCreateCommand.sh deleted file mode 100644 index 85462caf7fad..000000000000 --- a/scripts/postCreateCommand.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -npm ci -# Create Virutal environment. -python3.7 -m venv /workspaces/vscode-python/.venv - -# Activate Virtual environment. -source /workspaces/vscode-python/.venv/bin/activate - -# Install required Python libraries. -npx gulp installPythonLibs - -# Install testing requirement using python in .venv . -/workspaces/vscode-python/.venv/bin/python -m pip install -r build/test-requirements.txt -/workspaces/vscode-python/.venv/bin/python -m pip install -r build/smoke-test-requirements.txt -/workspaces/vscode-python/.venv/bin/python -m pip install -r build/functional-test-requirements.txt diff --git a/src/client/common/interpreterPathService.ts b/src/client/common/interpreterPathService.ts index 9eea1548977c..8af142962565 100644 --- a/src/client/common/interpreterPathService.ts +++ b/src/client/common/interpreterPathService.ts @@ -30,7 +30,7 @@ export const isRemoteGlobalSettingCopiedKey = 'isRemoteGlobalSettingCopiedKey'; export const defaultInterpreterPathSetting: keyof IPythonSettings = 'defaultInterpreterPath'; const CI_PYTHON_PATH = getCIPythonPath(); -function getCIPythonPath(): string { +export function getCIPythonPath(): string { if (process.env.CI_PYTHON_PATH && fs.existsSync(process.env.CI_PYTHON_PATH)) { return process.env.CI_PYTHON_PATH; } diff --git a/src/test/common/interpreterPathService.unit.test.ts b/src/test/common/interpreterPathService.unit.test.ts index 6ba63d9d663d..58a34b3cbcde 100644 --- a/src/test/common/interpreterPathService.unit.test.ts +++ b/src/test/common/interpreterPathService.unit.test.ts @@ -15,7 +15,11 @@ import { WorkspaceConfiguration, } from 'vscode'; import { IApplicationEnvironment, IWorkspaceService } from '../../client/common/application/types'; -import { defaultInterpreterPathSetting, InterpreterPathService } from '../../client/common/interpreterPathService'; +import { + defaultInterpreterPathSetting, + getCIPythonPath, + InterpreterPathService, +} from '../../client/common/interpreterPathService'; import { FileSystemPaths } from '../../client/common/platform/fs-paths'; import { InterpreterConfigurationScope, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; import { createDeferred, sleep } from '../../client/common/utils/async'; @@ -447,7 +451,8 @@ suite('Interpreter Path Service', async () => { workspaceValue: undefined, }); const settingValue = interpreterPathService.get(resource); - expect(settingValue).to.equal('python'); + + expect(settingValue).to.equal(getCIPythonPath()); }); test('If defaultInterpreterPathSetting is changed, an event is fired', async () => { diff --git a/src/test/pythonEnvironments/common/commonUtils.functional.test.ts b/src/test/pythonEnvironments/common/commonUtils.functional.test.ts index e0c1f755e2c8..647a17a40a90 100644 --- a/src/test/pythonEnvironments/common/commonUtils.functional.test.ts +++ b/src/test/pythonEnvironments/common/commonUtils.functional.test.ts @@ -86,7 +86,6 @@ suite('pyenvs common utils - finding Python executables', () => { python3 -> sub2/sub2.2/python3 python3.7 -> sub2/sub2.1/sub2.1.1/python - python2.7 -> does-not-exist `); } }); @@ -106,7 +105,6 @@ suite('pyenvs common utils - finding Python executables', () => { // These will match. 'python', 'python2', - 'python2.7', 'python3', 'python3.7', 'python3.8', @@ -137,7 +135,6 @@ suite('pyenvs common utils - finding Python executables', () => { // These will match. 'python', 'python2', - 'python2.7', 'python3', 'python3.7', 'python3.8', @@ -167,7 +164,6 @@ suite('pyenvs common utils - finding Python executables', () => { // These will match. 'python', 'python2', - 'python2.7', 'python3', 'python3.7', 'python3.8', From a42cb33cea73a6e276f2bc2ad635f62fc2c1e6a2 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 24 Jul 2023 15:59:12 -0700 Subject: [PATCH 0101/1136] Add new telemetry property to GPDR (#21683) This property was added for tracking diagnostics we emit in Pylance. --- src/client/telemetry/pylance.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index 905cacc5fbf2..c59d279b98c2 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -50,7 +50,8 @@ "numfilesinprogram" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "peakrssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "resolverid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "rssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + "rssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "diagnosticsseen" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ /* __GDPR__ From f536b744a757bd8fa8f0668c58b7906b518d98b4 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Mon, 24 Jul 2023 18:05:39 -0700 Subject: [PATCH 0102/1136] Edit issue-labels.yml, triage-info-needed.yml (#21685) Add Anthony to issue-labels.yml and triage-info-needed.yml --- .github/workflows/issue-labels.yml | 2 +- .github/workflows/triage-info-needed.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index ec2c5eb002fd..e942a4c965ec 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -7,7 +7,7 @@ on: env: # To update the list of labels, see `getLabels.js`. REPO_LABELS: '["area-api","area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' - TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd"]' + TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd","anthonykim1"]' permissions: issues: write diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml index 1c384d824da5..c717d7ec94b3 100644 --- a/.github/workflows/triage-info-needed.yml +++ b/.github/workflows/triage-info-needed.yml @@ -5,7 +5,7 @@ on: types: [created] env: - TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd", "brettcannon"]' + TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd", "brettcannon","anthonykim1"]' jobs: add_label: From 8b9bca1fd5a684d454e0d97583642cb46553241e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 25 Jul 2023 12:21:08 -0700 Subject: [PATCH 0103/1136] Do not show "Select at workspace level" option if only one workspace folder is opened (#21689) Closes https://github.com/microsoft/vscode-python/issues/21220 --- .../configuration/interpreterSelector/commands/base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/base.ts b/src/client/interpreter/configuration/interpreterSelector/commands/base.ts index 6ed2dee36c89..6307e286dbfe 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/base.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/base.ts @@ -53,7 +53,7 @@ export abstract class BaseInterpreterSelectorCommand implements IExtensionSingle }, ]; } - if (!this.workspaceService.workspaceFile && workspaceFolders.length === 1) { + if (workspaceFolders.length === 1) { return [ { folderUri: workspaceFolders[0].uri, From d6730049ca5bf08aa9183266cdbbb4ce0d638ac2 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 25 Jul 2023 13:57:06 -0700 Subject: [PATCH 0104/1136] Convert JS-style typings to native TS in `@vscode/python-extension` (#21692) Closes https://github.com/microsoft/vscode-python/issues/21690 --- pythonExtensionApi/src/main.ts | 14 +++++--------- src/client/api/types.ts | 34 ++++++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/pythonExtensionApi/src/main.ts b/pythonExtensionApi/src/main.ts index b980a06b72f8..cf1461f04d81 100644 --- a/pythonExtensionApi/src/main.ts +++ b/pythonExtensionApi/src/main.ts @@ -10,7 +10,6 @@ import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem, extensio export interface PythonExtension { /** * Promise indicating whether all parts of the extension have completed loading or not. - * @type {Promise} */ ready: Promise; jupyter: { @@ -21,10 +20,9 @@ export interface PythonExtension { * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. * Users can append another array of strings of what they want to execute along with relevant arguments to Python. * E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` - * @param {string} host - * @param {number} port - * @param {boolean} [waitUntilDebuggerAttaches=true] - * @returns {Promise} + * @param host + * @param port + * @param waitUntilDebuggerAttaches Defaults to `true`. */ getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; @@ -38,8 +36,8 @@ export interface PythonExtension { datascience: { /** * Launches Data Viewer component. - * @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data. - * @param {string} title Data Viewer title + * @param dataProvider Instance that will be used by the Data Viewer component to fetch data. + * @param title Data Viewer title */ showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; /** @@ -316,7 +314,6 @@ export type EnvironmentPath = { * was contributed. */ export type EnvironmentTools = KnownEnvironmentTools | string; - /** * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink * once tools have their own separate extensions. @@ -335,7 +332,6 @@ export type KnownEnvironmentTools = * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. */ export type EnvironmentType = KnownEnvironmentTypes | string; - /** * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their * own separate extensions, in which case they're expected to provide the type themselves. diff --git a/src/client/api/types.ts b/src/client/api/types.ts index 4e13ec4853ec..cf1461f04d81 100644 --- a/src/client/api/types.ts +++ b/src/client/api/types.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem } from 'vscode'; +import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem, extensions } from 'vscode'; /* * Do not introduce any breaking changes to this API. @@ -10,7 +10,6 @@ import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem } from 'v export interface PythonExtension { /** * Promise indicating whether all parts of the extension have completed loading or not. - * @type {Promise} */ ready: Promise; jupyter: { @@ -21,10 +20,9 @@ export interface PythonExtension { * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. * Users can append another array of strings of what they want to execute along with relevant arguments to Python. * E.g `['/Users/..../pythonVSCode/pythonFiles/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` - * @param {string} host - * @param {number} port - * @param {boolean} [waitUntilDebuggerAttaches=true] - * @returns {Promise} + * @param host + * @param port + * @param waitUntilDebuggerAttaches Defaults to `true`. */ getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; @@ -38,8 +36,8 @@ export interface PythonExtension { datascience: { /** * Launches Data Viewer component. - * @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data. - * @param {string} title Data Viewer title + * @param dataProvider Instance that will be used by the Data Viewer component to fetch data. + * @param title Data Viewer title */ showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; /** @@ -387,3 +385,23 @@ export type EnvironmentVariablesChangeEvent = { */ readonly env: EnvironmentVariables; }; + +export const PVSC_EXTENSION_ID = 'ms-python.python'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PythonExtension { + /** + * Returns the API exposed by the Python extension in VS Code. + */ + export async function api(): Promise { + const extension = extensions.getExtension(PVSC_EXTENSION_ID); + if (extension === undefined) { + throw new Error(`Python extension is not installed or is disabled`); + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonApi: PythonExtension = extension.exports; + return pythonApi; + } +} From 83107cc253508a329de2bc2f084bb6f4294a4289 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 27 Jul 2023 13:48:23 -0700 Subject: [PATCH 0105/1136] Move "vscode" out of required dependencies for npm package (#21701) Closes https://github.com/microsoft/vscode-python/issues/21684 --- pythonExtensionApi/package-lock.json | 17 ++++++++++------- pythonExtensionApi/package.json | 7 ++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index a8abd6d85bfc..a03c33ab5c0d 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -1,28 +1,30 @@ { "name": "@vscode/python-extension", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vscode/python-extension", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", - "dependencies": { - "@types/vscode": "^1.78.0" - }, "devDependencies": { + "@types/vscode": "^1.78.0", "typescript": "5.0.4" }, "engines": { "node": ">=16.17.1", "vscode": "^1.78.0" + }, + "peerDependencies": { + "@types/vscode": "^1.78.0" } }, "node_modules/@types/vscode": { "version": "1.80.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.80.0.tgz", - "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==" + "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==", + "dev": true }, "node_modules/typescript": { "version": "5.0.4", @@ -42,7 +44,8 @@ "@types/vscode": { "version": "1.80.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.80.0.tgz", - "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==" + "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==", + "dev": true }, "typescript": { "version": "5.0.4", diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index aaeaaf54a0c4..b19a64dd7d66 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -1,7 +1,7 @@ { "name": "@vscode/python-extension", "description": "An API facade for the Python extension in VS Code", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Microsoft Corporation" }, @@ -25,11 +25,12 @@ "bugs": { "url": "https://github.com/Microsoft/vscode-python/issues" }, - "dependencies": { + "peerDependencies": { "@types/vscode": "^1.78.0" }, "devDependencies": { - "typescript": "5.0.4" + "typescript": "5.0.4", + "@types/vscode": "^1.78.0" }, "scripts": { "prepublishOnly": "echo \"⛔ Can only publish from a secure pipeline ⛔\" && node ../build/fail", From 06d62aa04557d51576633014bcca6b5d7cae50ca Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 27 Jul 2023 21:11:28 -0700 Subject: [PATCH 0106/1136] Update homepage for Python API package (#21703) For #21631 --- pythonExtensionApi/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index b19a64dd7d66..dd0e921f8abe 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -17,7 +17,7 @@ "vscode": "^1.78.0" }, "license": "MIT", - "homepage": "https://github.com/Microsoft/vscode-python", + "homepage": "https://github.com/microsoft/vscode-python/tree/main/pythonExtensionApi", "repository": { "type": "git", "url": "https://github.com/Microsoft/vscode-python" From efcc3d7382c4a7f6ce930372dcd05a7c5358dc83 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 28 Jul 2023 09:52:29 -0700 Subject: [PATCH 0107/1136] Make test_ids relative to workspace path not root dir (#21682) makes sure all testIds that are returned to the extension are relative to the workspace (which will be the invocation directory) instead of to the root. This will stop testIds for not being recognized when using a config file or another parameter that changes the root directory during pytest. fixes https://github.com/microsoft/vscode-python/issues/21640 and https://github.com/microsoft/vscode-python/issues/21637 --- .../pytestadapter/.data/root/tests/pytest.ini | 0 .../pytestadapter/.data/root/tests/test_a.py | 6 + .../pytestadapter/.data/root/tests/test_b.py | 6 + .../expected_discovery_test_output.py | 516 +++++++++++++----- .../expected_execution_test_output.py | 362 +++++++++--- pythonFiles/tests/pytestadapter/helpers.py | 16 +- .../tests/pytestadapter/test_discovery.py | 52 +- .../tests/pytestadapter/test_execution.py | 46 +- pythonFiles/vscode_pytest/__init__.py | 58 +- 9 files changed, 812 insertions(+), 250 deletions(-) create mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini create mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py create mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini b/pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py b/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py new file mode 100644 index 000000000000..3ec3dd9626cb --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def test_a_function(): # test_marker--test_a_function + assert True diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py b/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py new file mode 100644 index 000000000000..0d3148641f85 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def test_b_function(): # test_marker--test_b_function + assert True diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 91c1453dfc77..2b2c07ab8ea7 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -1,6 +1,7 @@ import os -from .helpers import TEST_DATA_PATH, find_test_line_number + +from .helpers import TEST_DATA_PATH, find_test_line_number, get_absolute_test_id # This file contains the expected output dictionaries for tests discovery and is used in test_discovery.py. @@ -18,7 +19,7 @@ # This is the expected output for the simple_pytest.py file. # └── simple_pytest.py # └── test_function -simple_test_file_path = os.fspath(TEST_DATA_PATH / "simple_pytest.py") +simple_test_file_path = TEST_DATA_PATH / "simple_pytest.py" simple_discovery_pytest_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -26,20 +27,24 @@ "children": [ { "name": "simple_pytest.py", - "path": simple_test_file_path, + "path": os.fspath(simple_test_file_path), "type_": "file", - "id_": simple_test_file_path, + "id_": os.fspath(simple_test_file_path), "children": [ { "name": "test_function", - "path": simple_test_file_path, + "path": os.fspath(simple_test_file_path), "lineno": find_test_line_number( "test_function", simple_test_file_path, ), "type_": "test", - "id_": "simple_pytest.py::test_function", - "runID": "simple_pytest.py::test_function", + "id_": get_absolute_test_id( + "simple_pytest.py::test_function", simple_test_file_path + ), + "runID": get_absolute_test_id( + "simple_pytest.py::test_function", simple_test_file_path + ), } ], } @@ -52,7 +57,7 @@ # ├── TestExample # │ └── test_true_unittest # └── test_true_pytest -unit_pytest_same_file_path = os.fspath(TEST_DATA_PATH / "unittest_pytest_same_file.py") +unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" unit_pytest_same_file_discovery_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -60,39 +65,51 @@ "children": [ { "name": "unittest_pytest_same_file.py", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "type_": "file", - "id_": unit_pytest_same_file_path, + "id_": os.fspath(unit_pytest_same_file_path), "children": [ { "name": "TestExample", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "type_": "class", "children": [ { "name": "test_true_unittest", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "lineno": find_test_line_number( "test_true_unittest", - unit_pytest_same_file_path, + os.fspath(unit_pytest_same_file_path), ), "type_": "test", - "id_": "unittest_pytest_same_file.py::TestExample::test_true_unittest", - "runID": "unittest_pytest_same_file.py::TestExample::test_true_unittest", + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), + "runID": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), } ], "id_": "unittest_pytest_same_file.py::TestExample", }, { "name": "test_true_pytest", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "lineno": find_test_line_number( "test_true_pytest", unit_pytest_same_file_path, ), "type_": "test", - "id_": "unittest_pytest_same_file.py::test_true_pytest", - "runID": "unittest_pytest_same_file.py::test_true_pytest", + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), + "runID": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), }, ], } @@ -124,9 +141,9 @@ # └── test_subtract_positive_numbers # │ └── TestDuplicateFunction # │ └── test_dup_s -unittest_folder_path = os.fspath(TEST_DATA_PATH / "unittest_folder") -test_add_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_add.py") -test_subtract_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_subtract.py") +unittest_folder_path = TEST_DATA_PATH / "unittest_folder" +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" unittest_folder_discovery_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -134,61 +151,79 @@ "children": [ { "name": "unittest_folder", - "path": unittest_folder_path, + "path": os.fspath(unittest_folder_path), "type_": "folder", - "id_": unittest_folder_path, + "id_": os.fspath(unittest_folder_path), "children": [ { "name": "test_add.py", - "path": test_add_path, + "path": os.fspath(test_add_path), "type_": "file", - "id_": test_add_path, + "id_": os.fspath(test_add_path), "children": [ { "name": "TestAddFunction", - "path": test_add_path, + "path": os.fspath(test_add_path), "type_": "class", "children": [ { "name": "test_add_negative_numbers", - "path": test_add_path, + "path": os.fspath(test_add_path), "lineno": find_test_line_number( "test_add_negative_numbers", - test_add_path, + os.fspath(test_add_path), ), "type_": "test", - "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", - "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), }, { "name": "test_add_positive_numbers", - "path": test_add_path, + "path": os.fspath(test_add_path), "lineno": find_test_line_number( "test_add_positive_numbers", - test_add_path, + os.fspath(test_add_path), ), "type_": "test", - "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", - "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), }, ], "id_": "unittest_folder/test_add.py::TestAddFunction", }, { "name": "TestDuplicateFunction", - "path": test_add_path, + "path": os.fspath(test_add_path), "type_": "class", "children": [ { "name": "test_dup_a", - "path": test_add_path, + "path": os.fspath(test_add_path), "lineno": find_test_line_number( "test_dup_a", - test_add_path, + os.fspath(test_add_path), ), "type_": "test", - "id_": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", - "runID": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), }, ], "id_": "unittest_folder/test_add.py::TestDuplicateFunction", @@ -197,55 +232,73 @@ }, { "name": "test_subtract.py", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "type_": "file", - "id_": test_subtract_path, + "id_": os.fspath(test_subtract_path), "children": [ { "name": "TestSubtractFunction", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "type_": "class", "children": [ { "name": "test_subtract_negative_numbers", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "lineno": find_test_line_number( "test_subtract_negative_numbers", - test_subtract_path, + os.fspath(test_subtract_path), ), "type_": "test", - "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", - "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), }, { "name": "test_subtract_positive_numbers", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "lineno": find_test_line_number( "test_subtract_positive_numbers", - test_subtract_path, + os.fspath(test_subtract_path), ), "type_": "test", - "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", - "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), }, ], "id_": "unittest_folder/test_subtract.py::TestSubtractFunction", }, { "name": "TestDuplicateFunction", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "type_": "class", "children": [ { "name": "test_dup_s", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "lineno": find_test_line_number( "test_dup_s", - test_subtract_path, + os.fspath(test_subtract_path), ), "type_": "test", - "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", - "runID": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), }, ], "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction", @@ -268,20 +321,23 @@ # └── test_bottom_folder.py # └── test_bottom_function_t # └── test_bottom_function_f -dual_level_nested_folder_path = os.fspath(TEST_DATA_PATH / "dual_level_nested_folder") -test_top_folder_path = os.fspath( +dual_level_nested_folder_path = TEST_DATA_PATH / "dual_level_nested_folder" +test_top_folder_path = ( TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" ) -test_nested_folder_one_path = os.fspath( + +test_nested_folder_one_path = ( TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" ) -test_bottom_folder_path = os.fspath( + +test_bottom_folder_path = ( TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" ) + dual_level_nested_folder_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -289,73 +345,97 @@ "children": [ { "name": "dual_level_nested_folder", - "path": dual_level_nested_folder_path, + "path": os.fspath(dual_level_nested_folder_path), "type_": "folder", - "id_": dual_level_nested_folder_path, + "id_": os.fspath(dual_level_nested_folder_path), "children": [ { "name": "test_top_folder.py", - "path": test_top_folder_path, + "path": os.fspath(test_top_folder_path), "type_": "file", - "id_": test_top_folder_path, + "id_": os.fspath(test_top_folder_path), "children": [ { "name": "test_top_function_t", - "path": test_top_folder_path, + "path": os.fspath(test_top_folder_path), "lineno": find_test_line_number( "test_top_function_t", test_top_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", - "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "id_": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + test_top_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + test_top_folder_path, + ), }, { "name": "test_top_function_f", - "path": test_top_folder_path, + "path": os.fspath(test_top_folder_path), "lineno": find_test_line_number( "test_top_function_f", test_top_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", - "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "id_": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + test_top_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + test_top_folder_path, + ), }, ], }, { "name": "nested_folder_one", - "path": test_nested_folder_one_path, + "path": os.fspath(test_nested_folder_one_path), "type_": "folder", - "id_": test_nested_folder_one_path, + "id_": os.fspath(test_nested_folder_one_path), "children": [ { "name": "test_bottom_folder.py", - "path": test_bottom_folder_path, + "path": os.fspath(test_bottom_folder_path), "type_": "file", - "id_": test_bottom_folder_path, + "id_": os.fspath(test_bottom_folder_path), "children": [ { "name": "test_bottom_function_t", - "path": test_bottom_folder_path, + "path": os.fspath(test_bottom_folder_path), "lineno": find_test_line_number( "test_bottom_function_t", test_bottom_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "id_": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + test_bottom_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + test_bottom_folder_path, + ), }, { "name": "test_bottom_function_f", - "path": test_bottom_folder_path, + "path": os.fspath(test_bottom_folder_path), "lineno": find_test_line_number( "test_bottom_function_f", test_bottom_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "id_": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + test_bottom_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + test_bottom_folder_path, + ), }, ], } @@ -374,12 +454,10 @@ # └── test_nest.py # └── test_function -folder_a_path = os.fspath(TEST_DATA_PATH / "folder_a") -folder_b_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b") -folder_a_nested_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a") -test_nest_path = os.fspath( - TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" -) +folder_a_path = TEST_DATA_PATH / "folder_a" +folder_b_path = TEST_DATA_PATH / "folder_a" / "folder_b" +folder_a_nested_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" +test_nest_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" double_nested_folder_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -387,38 +465,44 @@ "children": [ { "name": "folder_a", - "path": folder_a_path, + "path": os.fspath(folder_a_path), "type_": "folder", - "id_": folder_a_path, + "id_": os.fspath(folder_a_path), "children": [ { "name": "folder_b", - "path": folder_b_path, + "path": os.fspath(folder_b_path), "type_": "folder", - "id_": folder_b_path, + "id_": os.fspath(folder_b_path), "children": [ { "name": "folder_a", - "path": folder_a_nested_path, + "path": os.fspath(folder_a_nested_path), "type_": "folder", - "id_": folder_a_nested_path, + "id_": os.fspath(folder_a_nested_path), "children": [ { "name": "test_nest.py", - "path": test_nest_path, + "path": os.fspath(test_nest_path), "type_": "file", - "id_": test_nest_path, + "id_": os.fspath(test_nest_path), "children": [ { "name": "test_function", - "path": test_nest_path, + "path": os.fspath(test_nest_path), "lineno": find_test_line_number( "test_function", test_nest_path, ), "type_": "test", - "id_": "folder_a/folder_b/folder_a/test_nest.py::test_function", - "runID": "folder_a/folder_b/folder_a/test_nest.py::test_function", + "id_": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", + test_nest_path, + ), + "runID": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", + test_nest_path, + ), } ], } @@ -438,7 +522,7 @@ # └── [3+5-8] # └── [2+4-6] # └── [6+9-16] -parameterize_tests_path = os.fspath(TEST_DATA_PATH / "parametrize_tests.py") +parameterize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" parametrize_tests_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -446,77 +530,107 @@ "children": [ { "name": "parametrize_tests.py", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "type_": "file", - "id_": parameterize_tests_path, + "id_": os.fspath(parameterize_tests_path), "children": [ { "name": "test_adding", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "type_": "function", "id_": "parametrize_tests.py::test_adding", "children": [ { "name": "[3+5-8]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_adding[3+5-8]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_adding[3+5-8]", - "runID": "parametrize_tests.py::test_adding[3+5-8]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", + parameterize_tests_path, + ), }, { "name": "[2+4-6]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_adding[2+4-6]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_adding[2+4-6]", - "runID": "parametrize_tests.py::test_adding[2+4-6]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", + parameterize_tests_path, + ), }, { "name": "[6+9-16]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_adding[6+9-16]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_adding[6+9-16]", - "runID": "parametrize_tests.py::test_adding[6+9-16]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", + parameterize_tests_path, + ), }, ], }, { "name": "test_under_ten", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "type_": "function", "children": [ { "name": "[1]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_under_ten[1]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_under_ten[1]", - "runID": "parametrize_tests.py::test_under_ten[1]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[1]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[1]", + parameterize_tests_path, + ), }, { "name": "[2]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_under_ten[2]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_under_ten[2]", - "runID": "parametrize_tests.py::test_under_ten[2]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[2]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[2]", + parameterize_tests_path, + ), }, ], "id_": "parametrize_tests.py::test_under_ten", @@ -529,7 +643,7 @@ # This is the expected output for the text_docstring.txt tests. # └── text_docstring.txt -text_docstring_path = os.fspath(TEST_DATA_PATH / "text_docstring.txt") +text_docstring_path = TEST_DATA_PATH / "text_docstring.txt" doctest_pytest_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -537,20 +651,24 @@ "children": [ { "name": "text_docstring.txt", - "path": text_docstring_path, + "path": os.fspath(text_docstring_path), "type_": "file", - "id_": text_docstring_path, + "id_": os.fspath(text_docstring_path), "children": [ { "name": "text_docstring.txt", - "path": text_docstring_path, + "path": os.fspath(text_docstring_path), "lineno": find_test_line_number( "text_docstring.txt", - text_docstring_path, + os.fspath(text_docstring_path), ), "type_": "test", - "id_": "text_docstring.txt::text_docstring.txt", - "runID": "text_docstring.txt::text_docstring.txt", + "id_": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", text_docstring_path + ), + "runID": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", text_docstring_path + ), } ], } @@ -570,8 +688,8 @@ # └── [1] # └── [2] # └── [3] -param1_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param1.py") -param2_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param2.py") +param1_path = TEST_DATA_PATH / "param_same_name" / "test_param1.py" +param2_path = TEST_DATA_PATH / "param_same_name" / "test_param2.py" param_same_name_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -585,38 +703,56 @@ "children": [ { "name": "test_param1.py", - "path": param1_path, + "path": os.fspath(param1_path), "type_": "file", - "id_": param1_path, + "id_": os.fspath(param1_path), "children": [ { "name": "test_odd_even", - "path": param1_path, + "path": os.fspath(param1_path), "type_": "function", "children": [ { "name": "[a]", - "path": param1_path, + "path": os.fspath(param1_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param1.py::test_odd_even[a]", - "runID": "param_same_name/test_param1.py::test_odd_even[a]", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[a]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[a]", + param1_path, + ), }, { "name": "[b]", - "path": param1_path, + "path": os.fspath(param1_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param1.py::test_odd_even[b]", - "runID": "param_same_name/test_param1.py::test_odd_even[b]", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[b]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[b]", + param1_path, + ), }, { "name": "[c]", - "path": param1_path, + "path": os.fspath(param1_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param1.py::test_odd_even[c]", - "runID": "param_same_name/test_param1.py::test_odd_even[c]", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[c]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[c]", + param1_path, + ), }, ], "id_": "param_same_name/test_param1.py::test_odd_even", @@ -625,38 +761,56 @@ }, { "name": "test_param2.py", - "path": param2_path, + "path": os.fspath(param2_path), "type_": "file", - "id_": param2_path, + "id_": os.fspath(param2_path), "children": [ { "name": "test_odd_even", - "path": param2_path, + "path": os.fspath(param2_path), "type_": "function", "children": [ { "name": "[1]", - "path": param2_path, + "path": os.fspath(param2_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param2.py::test_odd_even[1]", - "runID": "param_same_name/test_param2.py::test_odd_even[1]", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[1]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[1]", + param2_path, + ), }, { "name": "[2]", - "path": param2_path, + "path": os.fspath(param2_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param2.py::test_odd_even[2]", - "runID": "param_same_name/test_param2.py::test_odd_even[2]", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[2]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[2]", + param2_path, + ), }, { "name": "[3]", - "path": param2_path, + "path": os.fspath(param2_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param2.py::test_odd_even[3]", - "runID": "param_same_name/test_param2.py::test_odd_even[3]", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[3]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[3]", + param2_path, + ), }, ], "id_": "param_same_name/test_param2.py::test_odd_even", @@ -668,3 +822,67 @@ ], "id_": TEST_DATA_PATH_STR, } + +tests_path = TEST_DATA_PATH / "root" / "tests" +tests_a_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" +tests_b_path = TEST_DATA_PATH / "root" / "tests" / "test_b.py" +# This is the expected output for the root folder tests. +# └── tests +# └── test_a.py +# └── test_a_function +# └── test_b.py +# └── test_b_function +root_with_config_expected_output = { + "name": "tests", + "path": os.fspath(tests_path), + "type_": "folder", + "children": [ + { + "name": "test_a.py", + "path": os.fspath(tests_a_path), + "type_": "file", + "id_": os.fspath(tests_a_path), + "children": [ + { + "name": "test_a_function", + "path": os.fspath(os.path.join(tests_path, "test_a.py")), + "lineno": find_test_line_number( + "test_a_function", + os.path.join(tests_path, "test_a.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "tests/test_a.py::test_a_function", tests_a_path + ), + "runID": get_absolute_test_id( + "tests/test_a.py::test_a_function", tests_a_path + ), + } + ], + }, + { + "name": "test_b.py", + "path": os.fspath(tests_b_path), + "type_": "file", + "id_": os.fspath(tests_b_path), + "children": [ + { + "name": "test_b_function", + "path": os.fspath(os.path.join(tests_path, "test_b.py")), + "lineno": find_test_line_number( + "test_b_function", + os.path.join(tests_path, "test_b.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "tests/test_b.py::test_b_function", tests_b_path + ), + "runID": get_absolute_test_id( + "tests/test_b.py::test_b_function", tests_b_path + ), + } + ], + }, + ], + "id_": os.fspath(tests_path), +} diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index fe1d40a55b43..0a7e737dfc0e 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -6,6 +6,7 @@ SUCCESS = "success" FAILURE = "failure" TEST_SUBTRACT_FUNCTION_NEGATIVE_NUMBERS_ERROR = "self = \n\n def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers\n self,\n ):\n result = subtract(-2, -3)\n> self.assertEqual(result, 100000)\nE AssertionError: 1 != 100000\n\nunittest_folder/test_subtract.py:25: AssertionError" +from .helpers import TEST_DATA_PATH, get_absolute_test_id # This is the expected output for the unittest_folder execute tests # └── unittest_folder @@ -17,30 +18,52 @@ # └── TestSubtractFunction # ├── test_subtract_negative_numbers: failure # └── test_subtract_positive_numbers: success +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" uf_execution_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers": { - "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + test_subtract_path, + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + test_subtract_path, + ), "outcome": FAILURE, "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers": { - "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ), "outcome": SUCCESS, "message": None, "traceback": None, @@ -55,16 +78,26 @@ # │ └── TestAddFunction # │ ├── test_add_negative_numbers: success # │ └── test_add_positive_numbers: success +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" + uf_single_file_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, @@ -72,19 +105,24 @@ }, } + # This is the expected output for the unittest_folder execute only signle method # └── unittest_folder # ├── test_add.py # │ └── TestAddFunction # │ └── test_add_positive_numbers: success uf_single_method_execution_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, - } + }, } # This is the expected output for the unittest_folder tests run where two tests @@ -96,18 +134,28 @@ # └── test_subtract.py # └── TestSubtractFunction # └── test_subtract_positive_numbers: success +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" + uf_non_adjacent_tests_execution_expected_output = { - TEST_SUBTRACT_FUNCTION - + "test_subtract_positive_numbers": { - "test": TEST_SUBTRACT_FUNCTION + "test_subtract_positive_numbers", + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", test_subtract_path + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - TEST_ADD_FUNCTION - + "test_add_positive_numbers": { - "test": TEST_ADD_FUNCTION + "test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, @@ -115,12 +163,15 @@ }, } + # This is the expected output for the simple_pytest.py file. # └── simple_pytest.py # └── test_function: success +simple_pytest_path = TEST_DATA_PATH / "unittest_folder" / "simple_pytest.py" + simple_execution_pytest_expected_output = { - "simple_pytest.py::test_function": { - "test": "simple_pytest.py::test_function", + get_absolute_test_id("test_function", simple_pytest_path): { + "test": get_absolute_test_id("test_function", simple_pytest_path), "outcome": "success", "message": None, "traceback": None, @@ -128,21 +179,34 @@ } } + # This is the expected output for the unittest_pytest_same_file.py file. # ├── unittest_pytest_same_file.py # ├── TestExample # │ └── test_true_unittest: success # └── test_true_pytest: success +unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" unit_pytest_same_file_execution_expected_output = { - "unittest_pytest_same_file.py::TestExample::test_true_unittest": { - "test": "unittest_pytest_same_file.py::TestExample::test_true_unittest", + get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ): { + "test": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "unittest_pytest_same_file.py::test_true_pytest": { - "test": "unittest_pytest_same_file.py::test_true_pytest", + get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", unit_pytest_same_file_path + ): { + "test": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), "outcome": "success", "message": None, "traceback": None, @@ -154,9 +218,15 @@ # └── error_raise_exception.py # ├── TestSomething # │ └── test_a: failure +error_raised_exception_path = TEST_DATA_PATH / "error_raise_exception.py" error_raised_exception_execution_expected_output = { - "error_raise_exception.py::TestSomething::test_a": { - "test": "error_raise_exception.py::TestSomething::test_a", + get_absolute_test_id( + "error_raise_exception.py::TestSomething::test_a", error_raised_exception_path + ): { + "test": get_absolute_test_id( + "error_raise_exception.py::TestSomething::test_a", + error_raised_exception_path, + ), "outcome": "error", "message": "ERROR MESSAGE", "traceback": "TRACEBACK", @@ -172,44 +242,60 @@ # ├── TestClass # │ └── test_class_function_a: skipped # │ └── test_class_function_b: skipped + +skip_tests_path = TEST_DATA_PATH / "skip_tests.py" skip_tests_execution_expected_output = { - "skip_tests.py::test_something": { - "test": "skip_tests.py::test_something", + get_absolute_test_id("skip_tests.py::test_something", skip_tests_path): { + "test": get_absolute_test_id("skip_tests.py::test_something", skip_tests_path), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::test_another_thing": { - "test": "skip_tests.py::test_another_thing", + get_absolute_test_id("skip_tests.py::test_another_thing", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::test_another_thing", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::test_decorator_thing": { - "test": "skip_tests.py::test_decorator_thing", + get_absolute_test_id("skip_tests.py::test_decorator_thing", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::test_decorator_thing", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::test_decorator_thing_2": { - "test": "skip_tests.py::test_decorator_thing_2", + get_absolute_test_id("skip_tests.py::test_decorator_thing_2", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::test_decorator_thing_2", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::TestClass::test_class_function_a": { - "test": "skip_tests.py::TestClass::test_class_function_a", + get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_a", skip_tests_path + ): { + "test": get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_a", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::TestClass::test_class_function_b": { - "test": "skip_tests.py::TestClass::test_class_function_b", + get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_b", skip_tests_path + ): { + "test": get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_b", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, @@ -227,30 +313,59 @@ # └── test_bottom_folder.py # └── test_bottom_function_t: success # └── test_bottom_function_f: failure +dual_level_nested_folder_top_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" +) +dual_level_nested_folder_bottom_path = ( + TEST_DATA_PATH + / "dual_level_nested_folder" + / "nested_folder_one" + / "test_bottom_folder.py" +) dual_level_nested_folder_execution_expected_output = { - "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + get_absolute_test_id( + "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path + ): { + "test": get_absolute_test_id( + "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + get_absolute_test_id( + "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path + ): { + "test": get_absolute_test_id( + "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + dual_level_nested_folder_bottom_path, + ): { + "test": get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + dual_level_nested_folder_bottom_path, + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + dual_level_nested_folder_bottom_path, + ): { + "test": get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + dual_level_nested_folder_bottom_path, + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, @@ -264,38 +379,59 @@ # └── folder_a # └── test_nest.py # └── test_function: success + +nested_folder_path = ( + TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +) double_nested_folder_expected_execution_output = { - "folder_a/folder_b/folder_a/test_nest.py::test_function": { - "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", + get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path + ): { + "test": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, } } - # This is the expected output for the nested_folder tests. # └── parametrize_tests.py # └── test_adding[3+5-8]: success # └── test_adding[2+4-6]: success # └── test_adding[6+9-16]: failure +parametrize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" + parametrize_tests_expected_execution_output = { - "parametrize_tests.py::test_adding[3+5-8]": { - "test": "parametrize_tests.py::test_adding[3+5-8]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "parametrize_tests.py::test_adding[2+4-6]": { - "test": "parametrize_tests.py::test_adding[2+4-6]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "parametrize_tests.py::test_adding[6+9-16]": { - "test": "parametrize_tests.py::test_adding[6+9-16]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, @@ -307,8 +443,12 @@ # └── parametrize_tests.py # └── test_adding[3+5-8]: success single_parametrize_tests_expected_execution_output = { - "parametrize_tests.py::test_adding[3+5-8]": { - "test": "parametrize_tests.py::test_adding[3+5-8]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ), "outcome": "success", "message": None, "traceback": None, @@ -319,9 +459,12 @@ # This is the expected output for the single parameterized tests. # └── text_docstring.txt # └── text_docstring: success +doc_test_path = TEST_DATA_PATH / "text_docstring.txt" doctest_pytest_expected_execution_output = { - "text_docstring.txt::text_docstring.txt": { - "test": "text_docstring.txt::text_docstring.txt", + get_absolute_test_id("text_docstring.txt::text_docstring.txt", doc_test_path): { + "test": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", doc_test_path + ), "outcome": "success", "message": None, "traceback": None, @@ -330,68 +473,127 @@ } # Will run all tests in the cwd that fit the test file naming pattern. +folder_a_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +dual_level_nested_folder_top_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" +) +dual_level_nested_folder_bottom_path = ( + TEST_DATA_PATH + / "dual_level_nested_folder" + / "nested_folder_one" + / "test_bottom_folder.py" +) +unittest_folder_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +unittest_folder_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" + no_test_ids_pytest_execution_expected_output = { - "folder_a/folder_b/folder_a/test_nest.py::test_function": { - "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", + get_absolute_test_id("test_function", folder_a_path): { + "test": get_absolute_test_id("test_function", folder_a_path), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + get_absolute_test_id("test_top_function_t", dual_level_nested_folder_top_path): { + "test": get_absolute_test_id( + "test_top_function_t", dual_level_nested_folder_top_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + get_absolute_test_id("test_top_function_f", dual_level_nested_folder_top_path): { + "test": get_absolute_test_id( + "test_top_function_f", dual_level_nested_folder_top_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + get_absolute_test_id( + "test_bottom_function_t", dual_level_nested_folder_bottom_path + ): { + "test": get_absolute_test_id( + "test_bottom_function_t", dual_level_nested_folder_bottom_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + get_absolute_test_id( + "test_bottom_function_f", dual_level_nested_folder_bottom_path + ): { + "test": get_absolute_test_id( + "test_bottom_function_f", dual_level_nested_folder_bottom_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers": { - "test": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + get_absolute_test_id( + "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path + ): { + "test": get_absolute_test_id( + "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers": { - "test": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + get_absolute_test_id( + "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path + ): { + "test": get_absolute_test_id( + "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers": { - "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + get_absolute_test_id( + "TestSubtractFunction::test_subtract_negative_numbers", + unittest_folder_subtract_path, + ): { + "test": get_absolute_test_id( + "TestSubtractFunction::test_subtract_negative_numbers", + unittest_folder_subtract_path, + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers": { - "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + get_absolute_test_id( + "TestSubtractFunction::test_subtract_positive_numbers", + unittest_folder_subtract_path, + ): { + "test": get_absolute_test_id( + "TestSubtractFunction::test_subtract_positive_numbers", + unittest_folder_subtract_path, + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, } + +# This is the expected output for the root folder with the config file referenced. +# └── test_a.py +# └── test_a_function: success +test_add_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" +config_file_pytest_expected_execution_output = { + get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path): { + "test": get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index c3e01d52170a..28feb6282b92 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -16,6 +16,13 @@ from typing_extensions import TypedDict +def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: + split_id = test_id.split("::")[1:] + absolute_test_id = "::".join([str(testPath), *split_id]) + print("absolute path", absolute_test_id) + return absolute_test_id + + def create_server( host: str = "127.0.0.1", port: int = 0, @@ -104,6 +111,13 @@ def process_rpc_json(data: str) -> List[Dict[str, Any]]: def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: + """Run the pytest discovery and return the JSON data from the server.""" + return runner_with_cwd(args, TEST_DATA_PATH) + + +def runner_with_cwd( + args: List[str], path: pathlib.Path +) -> Optional[List[Dict[str, Any]]]: """Run the pytest discovery and return the JSON data from the server.""" process_args: List[str] = [ sys.executable, @@ -134,7 +148,7 @@ def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: t2 = threading.Thread( target=_run_test_code, - args=(process_args, env, TEST_DATA_PATH, completed), + args=(process_args, env, path, completed), ) t2.start() diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 5288c7ad769e..8d785be27c8b 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -7,7 +7,7 @@ import pytest from . import expected_discovery_test_output -from .helpers import TEST_DATA_PATH, runner +from .helpers import TEST_DATA_PATH, runner, runner_with_cwd def test_import_error(tmp_path): @@ -153,3 +153,53 @@ def test_pytest_collect(file, expected_const): assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) assert actual["tests"] == expected_const + + +def test_pytest_root_dir(): + """ + Test to test pytest discovery with the command line arg --rootdir specified to be a subfolder + of the workspace root. Discovery should succeed and testids should be relative to workspace root. + """ + rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" + actual = runner_with_cwd( + [ + "--collect-only", + rd, + ], + TEST_DATA_PATH / "root", + ) + if actual: + actual = actual[0] + assert actual + assert all(item in actual for item in ("status", "cwd", "tests")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert ( + actual["tests"] + == expected_discovery_test_output.root_with_config_expected_output + ) + + +def test_pytest_config_file(): + """ + Test to test pytest discovery with the command line arg -c with a specified config file which + changes the workspace root. Discovery should succeed and testids should be relative to workspace root. + """ + actual = runner_with_cwd( + [ + "--collect-only", + "-c", + "tests/pytest.ini", + ], + TEST_DATA_PATH / "root", + ) + if actual: + actual = actual[0] + assert actual + assert all(item in actual for item in ("status", "cwd", "tests")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert ( + actual["tests"] + == expected_discovery_test_output.root_with_config_expected_output + ) diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index ffc84955bf54..2be4886c24c1 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -1,12 +1,56 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json import os import shutil import pytest from tests.pytestadapter import expected_execution_test_output -from .helpers import TEST_DATA_PATH, runner +from .helpers import TEST_DATA_PATH, runner, runner_with_cwd + + +def test_config_file(): + """Test pytest execution when a config file is specified.""" + args = [ + "-c", + "tests/pytest.ini", + str(TEST_DATA_PATH / "root" / "tests" / "test_a.py::test_a_function"), + ] + new_cwd = TEST_DATA_PATH / "root" + actual = runner_with_cwd(args, new_cwd) + expected_const = ( + expected_execution_test_output.config_file_pytest_expected_execution_output + ) + assert actual + assert len(actual) == len(expected_const) + actual_result_dict = dict() + for a in actual: + assert all(item in a for item in ("status", "cwd", "result")) + assert a["status"] == "success" + assert a["cwd"] == os.fspath(new_cwd) + actual_result_dict.update(a["result"]) + assert actual_result_dict == expected_const + + +def test_rootdir_specified(): + """Test pytest execution when a --rootdir is specified.""" + rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" + args = [rd, "tests/test_a.py::test_a_function"] + new_cwd = TEST_DATA_PATH / "root" + actual = runner_with_cwd(args, new_cwd) + expected_const = ( + expected_execution_test_output.config_file_pytest_expected_execution_output + ) + assert actual + assert len(actual) == len(expected_const) + actual_result_dict = dict() + for a in actual: + assert all(item in a for item in ("status", "cwd", "result")) + assert a["status"] == "success" + assert a["cwd"] == os.fspath(new_cwd) + actual_result_dict.update(a["result"]) + assert actual_result_dict == expected_const def test_syntax_error_execution(tmp_path): diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 1ac287a8410a..49d429662e3a 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -69,8 +69,7 @@ def pytest_exception_interact(node, call, report): """ # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. # call.excinfo.exconly() returns the exception as a string. - # See if it is during discovery or execution. - # if discovery, then add the error to error logs. + # If it is during discovery, then add the error to error logs. if type(report) == pytest.CollectReport: if call.excinfo and call.excinfo.typename != "AssertionError": if report.outcome == "skipped" and "SkipTest" in str(call): @@ -83,11 +82,11 @@ def pytest_exception_interact(node, call, report): report.longreprtext + "\n Check Python Test Logs for more details." ) else: - # if execution, send this data that the given node failed. + # If during execution, send this data that the given node failed. report_value = "error" if call.excinfo.typename == "AssertionError": report_value = "failure" - node_id = str(node.nodeid) + node_id = get_absolute_test_id(node.nodeid, get_node_path(node)) if node_id not in collected_tests_so_far: collected_tests_so_far.append(node_id) item_result = create_test_outcome( @@ -106,6 +105,22 @@ def pytest_exception_interact(node, call, report): ) +def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: + """A function that returns the absolute test id. This is necessary because testIds are relative to the rootdir. + This does not work for our case since testIds when referenced during run time are relative to the instantiation + location. Absolute paths for testIds are necessary for the test tree ensures configurations that change the rootdir + of pytest are handled correctly. + + Keyword arguments: + test_id -- the pytest id of the test which is relative to the rootdir. + testPath -- the path to the file the test is located in, as a pathlib.Path object. + """ + split_id = test_id.split("::")[1:] + absolute_test_id = "::".join([str(testPath), *split_id]) + print("absolute path", absolute_test_id) + return absolute_test_id + + def pytest_keyboard_interrupt(excinfo): """A pytest hook that is called when a keyboard interrupt is raised. @@ -130,7 +145,7 @@ class TestOutcome(Dict): def create_test_outcome( - test: str, + testid: str, outcome: str, message: Union[str, None], traceback: Union[str, None], @@ -138,7 +153,7 @@ def create_test_outcome( ) -> TestOutcome: """A function that creates a TestOutcome object.""" return TestOutcome( - test=test, + test=testid, outcome=outcome, message=message, traceback=traceback, # TODO: traceback @@ -154,6 +169,7 @@ class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): IS_DISCOVERY = False +map_id_to_path = dict() def pytest_load_initial_conftests(early_config, parser, args): @@ -184,17 +200,21 @@ def pytest_report_teststatus(report, config): elif report.failed: report_value = "failure" message = report.longreprtext - node_id = str(report.nodeid) - if node_id not in collected_tests_so_far: - collected_tests_so_far.append(node_id) + node_path = map_id_to_path[report.nodeid] + if not node_path: + node_path = cwd + # Calculate the absolute test id and use this as the ID moving forward. + absolute_node_id = get_absolute_test_id(report.nodeid, node_path) + if absolute_node_id not in collected_tests_so_far: + collected_tests_so_far.append(absolute_node_id) item_result = create_test_outcome( - node_id, + absolute_node_id, report_value, message, traceback, ) collected_test = testRunResultDict() - collected_test[node_id] = item_result + collected_test[absolute_node_id] = item_result execution_post( os.fsdecode(cwd), "success", @@ -211,21 +231,22 @@ def pytest_report_teststatus(report, config): def pytest_runtest_protocol(item, nextitem): + map_id_to_path[item.nodeid] = get_node_path(item) skipped = check_skipped_wrapper(item) if skipped: - node_id = str(item.nodeid) + absolute_node_id = get_absolute_test_id(item.nodeid, get_node_path(item)) report_value = "skipped" cwd = pathlib.Path.cwd() - if node_id not in collected_tests_so_far: - collected_tests_so_far.append(node_id) + if absolute_node_id not in collected_tests_so_far: + collected_tests_so_far.append(absolute_node_id) item_result = create_test_outcome( - node_id, + absolute_node_id, report_value, None, None, ) collected_test = testRunResultDict() - collected_test[node_id] = item_result + collected_test[absolute_node_id] = item_result execution_post( os.fsdecode(cwd), "success", @@ -471,13 +492,14 @@ def create_test_node( test_case_loc: str = ( str(test_case.location[1] + 1) if (test_case.location[1] is not None) else "" ) + absolute_test_id = get_absolute_test_id(test_case.nodeid, get_node_path(test_case)) return { "name": test_case.name, "path": get_node_path(test_case), "lineno": test_case_loc, "type_": "test", - "id_": test_case.nodeid, - "runID": test_case.nodeid, + "id_": absolute_test_id, + "runID": absolute_test_id, } From 11a9f1d2a9a81097d16ce5f71385faa730bb7236 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 31 Jul 2023 23:34:41 +1000 Subject: [PATCH 0108/1136] Remove unwanted Jupyter API (#21702) Fixes https://github.com/microsoft/vscode-jupyter/issues/13986 --- pythonExtensionApi/src/main.ts | 60 +----------------------- src/client/api.ts | 12 ----- src/client/api/types.ts | 14 ------ src/client/jupyter/jupyterIntegration.ts | 30 ------------ 4 files changed, 1 insertion(+), 115 deletions(-) diff --git a/pythonExtensionApi/src/main.ts b/pythonExtensionApi/src/main.ts index cf1461f04d81..4de554bf5a24 100644 --- a/pythonExtensionApi/src/main.ts +++ b/pythonExtensionApi/src/main.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem, extensions } from 'vscode'; +import { CancellationToken, Event, Uri, WorkspaceFolder, extensions } from 'vscode'; /* * Do not introduce any breaking changes to this API. @@ -12,9 +12,6 @@ export interface PythonExtension { * Promise indicating whether all parts of the extension have completed loading or not. */ ready: Promise; - jupyter: { - registerHooks(): void; - }; debug: { /** * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. @@ -33,20 +30,6 @@ export interface PythonExtension { getDebuggerPackagePath(): Promise; }; - datascience: { - /** - * Launches Data Viewer component. - * @param dataProvider Instance that will be used by the Data Viewer component to fetch data. - * @param title Data Viewer title - */ - showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; - /** - * Registers a remote server provider component that's used to pick remote jupyter server URIs - * @param serverProvider object called back when picking jupyter server URI - */ - registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void; - }; - /** * These APIs provide a way for extensions to work with by python environments available in the user's machine * as found by the Python extension. See @@ -123,47 +106,6 @@ export interface PythonExtension { }; } -interface IJupyterServerUri { - baseUrl: string; - token: string; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorizationHeader: any; // JSON object for authorization header. - expiration?: Date; // Date/time when header expires and should be refreshed. - displayName: string; -} - -type JupyterServerUriHandle = string; - -export interface IJupyterUriProvider { - readonly id: string; // Should be a unique string (like a guid) - getQuickPickEntryItems(): QuickPickItem[]; - handleQuickPick(item: QuickPickItem, backEnabled: boolean): Promise; - getServerUri(handle: JupyterServerUriHandle): Promise; -} - -interface IDataFrameInfo { - columns?: { key: string; type: ColumnType }[]; - indexColumn?: string; - rowCount?: number; -} - -export interface IDataViewerDataProvider { - dispose(): void; - getDataFrameInfo(): Promise; - getAllRows(): Promise; - getRows(start: number, end: number): Promise; -} - -enum ColumnType { - String = 'string', - Number = 'number', - Bool = 'bool', -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type IRowsResponse = any[]; - export type RefreshOptions = { /** * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so diff --git a/src/client/api.ts b/src/client/api.ts index 32ce68f6a28b..2371a4d88de1 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -4,7 +4,6 @@ 'use strict'; -import { noop } from 'lodash'; import { Uri, Event } from 'vscode'; import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient'; import { LanguageClient } from 'vscode-languageclient/node'; @@ -17,7 +16,6 @@ import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extens import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer, IServiceManager } from './ioc/types'; import { JupyterExtensionIntegration } from './jupyter/jupyterIntegration'; -import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types'; import { traceError } from './logging'; import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { buildEnvironmentApi } from './environmentApi'; @@ -111,16 +109,6 @@ export function buildApi( return { execCommand: pythonPath === '' ? undefined : [pythonPath] }; }, }, - // These are for backwards compatibility. Other extensions are using these APIs and we don't want - // to force them to move to the jupyter extension ... yet. - datascience: { - registerRemoteServerProvider: jupyterIntegration - ? jupyterIntegration.registerRemoteServerProvider.bind(jupyterIntegration) - : ((noop as unknown) as (serverProvider: IJupyterUriProvider) => void), - showDataViewer: jupyterIntegration - ? jupyterIntegration.showDataViewer.bind(jupyterIntegration) - : ((noop as unknown) as (dataProvider: IDataViewerDataProvider, title: string) => Promise), - }, pylance: { createClient: (...args: any[]): BaseLanguageClient => { // Make sure we share output channel so that we can share one with diff --git a/src/client/api/types.ts b/src/client/api/types.ts index cf1461f04d81..63954a16d868 100644 --- a/src/client/api/types.ts +++ b/src/client/api/types.ts @@ -33,20 +33,6 @@ export interface PythonExtension { getDebuggerPackagePath(): Promise; }; - datascience: { - /** - * Launches Data Viewer component. - * @param dataProvider Instance that will be used by the Data Viewer component to fetch data. - * @param title Data Viewer title - */ - showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; - /** - * Registers a remote server provider component that's used to pick remote jupyter server URIs - * @param serverProvider object called back when picking jupyter server URI - */ - registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void; - }; - /** * These APIs provide a way for extensions to work with by python environments available in the user's machine * as found by the Python extension. See diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts index a0fa0fedb63f..7f660d26e5e4 100644 --- a/src/client/jupyter/jupyterIntegration.ts +++ b/src/client/jupyter/jupyterIntegration.ts @@ -33,7 +33,6 @@ import { PythonEnvironmentsChangedEvent, } from '../interpreter/contracts'; import { PythonEnvironment } from '../pythonEnvironments/info'; -import { IDataViewerDataProvider, IJupyterUriProvider } from './types'; import { PylanceApi } from '../activation/node/pylanceApi'; import { ExtensionContextKey } from '../common/application/contextKeys'; /** @@ -168,17 +167,6 @@ type JupyterExtensionApi = { * @param interpreterService */ registerPythonApi(interpreterService: PythonApiForJupyterExtension): void; - /** - * Launches Data Viewer component. - * @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data. - * @param {string} title Data Viewer title - */ - showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise; - /** - * Registers a remote server provider component that's used to pick remote jupyter server URIs - * @param serverProvider object called back when picking jupyter server URI - */ - registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void; }; @injectable() @@ -286,24 +274,6 @@ export class JupyterExtensionIntegration { } } - public registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void { - this.getExtensionApi() - .then((e) => { - if (e) { - e.registerRemoteServerProvider(serverProvider); - } - }) - .ignoreErrors(); - } - - public async showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise { - const api = await this.getExtensionApi(); - if (api) { - return api.showDataViewer(dataProvider, title); - } - return undefined; - } - private async getExtensionApi(): Promise { if (!this.pylanceExtension) { const pylanceExtension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); From ceecdb0c81b46e2e53e2b2dfe1002c7c3ad5343f Mon Sep 17 00:00:00 2001 From: Courtney Webster <60238438+cwebster-99@users.noreply.github.com> Date: Mon, 31 Jul 2023 12:36:31 -0500 Subject: [PATCH 0109/1136] Removing Jupyter Notebooks mentions from package.json (#21708) --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index abadcdb3bb0b..ad196c164a5f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "python", "displayName": "Python", - "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), Jupyter Notebooks, code formatting, refactoring, unit tests, and more.", + "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", "version": "2023.13.0-dev", "featureFlags": { "usingNewInterpreterStorage": true @@ -62,8 +62,7 @@ "Formatters", "Other", "Data Science", - "Machine Learning", - "Notebooks" + "Machine Learning" ], "activationEvents": [ "onDebugInitialConfigurations", From d9e368f04733802d766280b90e67aa2076768c12 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Mon, 31 Jul 2023 11:51:06 -0700 Subject: [PATCH 0110/1136] add area-repl to issue label (#21718) added area-repl as one of the issue label. --- .github/workflows/issue-labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index e942a4c965ec..d54015d94e46 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -6,7 +6,7 @@ on: env: # To update the list of labels, see `getLabels.js`. - REPO_LABELS: '["area-api","area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' + REPO_LABELS: '["area-api","area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-repl","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd","anthonykim1"]' permissions: From 237f82b696725011beaa6a46bc75b6307efefc6f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 31 Jul 2023 15:29:08 -0700 Subject: [PATCH 0111/1136] Fix UUID and disposing to resolve race condition (#21667) fixes https://github.com/microsoft/vscode-python/issues/21599 and https://github.com/microsoft/vscode-python/issues/21507 --- .../testing/testController/common/server.ts | 27 +- .../testing/testController/common/types.ts | 7 +- .../pytest/pytestDiscoveryAdapter.ts | 27 +- .../pytest/pytestExecutionAdapter.ts | 45 ++-- .../unittest/testDiscoveryAdapter.ts | 11 +- .../unittest/testExecutionAdapter.ts | 21 +- src/test/mocks/mockChildProcess.ts | 238 +++++++++++++++++ .../pytestDiscoveryAdapter.unit.test.ts | 76 ++++-- .../pytestExecutionAdapter.unit.test.ts | 118 +++++++-- .../testController/server.unit.test.ts | 248 +++++++++++++----- .../workspaceTestAdapter.unit.test.ts | 3 +- 11 files changed, 664 insertions(+), 157 deletions(-) create mode 100644 src/test/mocks/mockChildProcess.ts diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index f854371ffc35..564bd82f2ef6 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -3,10 +3,11 @@ import * as net from 'net'; import * as crypto from 'crypto'; -import { Disposable, Event, EventEmitter } from 'vscode'; +import { Disposable, Event, EventEmitter, TestRun } from 'vscode'; import * as path from 'path'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -15,6 +16,7 @@ import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER } from './utils'; +import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { private _onDataReceived: EventEmitter = new EventEmitter(); @@ -140,7 +142,12 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDataReceived.event; } - async sendCommand(options: TestCommandOptions, runTestIdPort?: string, callback?: () => void): Promise { + async sendCommand( + options: TestCommandOptions, + runTestIdPort?: string, + runInstance?: TestRun, + callback?: () => void, + ): Promise { const { uuid } = options; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; @@ -154,7 +161,7 @@ export class PythonTestServer implements ITestServer, Disposable { }; if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; - const isRun = !options.testIds; + const isRun = runTestIdPort !== undefined; // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, @@ -195,7 +202,19 @@ export class PythonTestServer implements ITestServer, Disposable { // This means it is running discovery traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); } - await execService.exec(args, spawnOptions); + const deferred = createDeferred>(); + + const result = execService.execObservable(args, spawnOptions); + + runInstance?.token.onCancellationRequested(() => { + result?.proc?.kill(); + }); + result?.proc?.on('close', () => { + traceLog('Exec server closed.', uuid); + deferred.resolve({ stdout: '', stderr: '' }); + callback?.(); + }); + await deferred.promise; } } catch (ex) { this.uuids = this.uuids.filter((u) => u !== uuid); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index d4e54951bfd7..16c0bd0e3cee 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -174,7 +174,12 @@ export interface ITestServer { readonly onDataReceived: Event; readonly onRunDataReceived: Event; readonly onDiscoveryDataReceived: Event; - sendCommand(options: TestCommandOptions, runTestIdsPort?: string, callback?: () => void): Promise; + sendCommand( + options: TestCommandOptions, + runTestIdsPort?: string, + runInstance?: TestRun, + callback?: () => void, + ): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index b83224d4161b..810fae0fa11c 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -4,13 +4,14 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceError, traceVerbose } from '../../../logging'; +import { traceVerbose } from '../../../logging'; import { DataReceivedEvent, DiscoveredTestPayload, @@ -48,7 +49,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - async runPytestDiscovery(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { + async runPytestDiscovery(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); @@ -78,17 +79,15 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); // delete UUID following entire discovery finishing. - execService - ?.exec(['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), spawnOptions) - .then(() => { - this.testServer.deleteUUID(uuid); - return deferred.resolve(); - }) - .catch((err) => { - traceError(`Error while trying to run pytest discovery, \n${err}\r\n\r\n`); - this.testServer.deleteUUID(uuid); - return deferred.reject(err); - }); - return deferred.promise; + const deferredExec = createDeferred>(); + const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + const result = execService?.execObservable(execArgs, spawnOptions); + + result?.proc?.on('close', () => { + deferredExec.resolve({ stdout: '', stderr: '' }); + this.testServer.deleteUUID(uuid); + deferred.resolve(); + }); + await deferredExec.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index a75a6089627c..b05fa21fc046 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -15,6 +15,7 @@ import { } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -22,13 +23,7 @@ import { removePositionalFoldersAndFiles } from './arguments'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { PYTEST_PROVIDER } from '../../common/constants'; import { EXTENSION_ROOT_DIR } from '../../../common/constants'; -import { startTestIdServer } from '../common/utils'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -// (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; -/** - * Wrapper Class for pytest test execution.. - */ +import * as utils from '../common/utils'; export class PytestTestExecutionAdapter implements ITestExecutionAdapter { constructor( @@ -48,18 +43,20 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); traceVerbose(uri, testIds, debugBool); - const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); - try { - await this.runTestsNew(uri, testIds, uuid, debugBool, executionFactory, debugLauncher); - } finally { - this.testServer.deleteUUID(uuid); - disposable.dispose(); - // confirm with testing that this gets called (it must clean this up) - } + const dispose = function (testServer: ITestServer) { + testServer.deleteUUID(uuid); + disposedDataReceived.dispose(); + }; + runInstance?.token.onCancellationRequested(() => { + dispose(this.testServer); + }); + await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, executionFactory, debugLauncher); + // placeholder until after the rewrite is adopted // TODO: remove after adoption. const executionPayload: ExecutionTestPayload = { @@ -74,6 +71,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], uuid: string, + runInstance?: TestRun, debugBool?: boolean, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, @@ -124,7 +122,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } traceLog(`Running PYTEST execution for the following test ids: ${testIds}`); - const pytestRunTestIdsPort = await startTestIdServer(testIds); + const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); @@ -143,6 +141,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions, () => { deferred.resolve(); + this.testServer.deleteUUID(uuid); }); } else { // combine path to run script with run args @@ -150,7 +149,19 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const runArgs = [scriptPath, ...testArgs]; traceInfo(`Running pytests with arguments: ${runArgs.join(' ')}\r\n`); - await execService?.exec(runArgs, spawnOptions); + const deferredExec = createDeferred>(); + const result = execService?.execObservable(runArgs, spawnOptions); + + runInstance?.token.onCancellationRequested(() => { + result?.proc?.kill(); + }); + + result?.proc?.on('close', () => { + deferredExec.resolve({ stdout: '', stderr: '' }); + this.testServer.deleteUUID(uuid); + deferred.resolve(); + }); + await deferredExec.promise; } } catch (ex) { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 6deca55117ea..b49ac3dabd0e 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -46,12 +46,11 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); }); - try { - await this.callSendCommand(options); - } finally { + + await this.callSendCommand(options, () => { this.testServer.deleteUUID(uuid); disposable.dispose(); - } + }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. const discoveryPayload: DiscoveredTestPayload = { @@ -61,8 +60,8 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - private async callSendCommand(options: TestCommandOptions): Promise { - await this.testServer.sendCommand(options); + private async callSendCommand(options: TestCommandOptions, callback: () => void): Promise { + await this.testServer.sendCommand(options, undefined, undefined, callback); const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; return discoveryPayload; } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 4cab941c2608..4cd392f93a43 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -37,18 +37,19 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance?: TestRun, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); - const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); - try { - await this.runTestsNew(uri, testIds, uuid, debugBool); - } finally { + const dispose = function () { + disposedDataReceived.dispose(); + }; + runInstance?.token.onCancellationRequested(() => { this.testServer.deleteUUID(uuid); - disposable.dispose(); - // confirm with testing that this gets called (it must clean this up) - } + dispose(); + }); + await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, dispose); const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; return executionPayload; } @@ -57,7 +58,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], uuid: string, + runInstance?: TestRun, debugBool?: boolean, + dispose?: () => void, ): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -80,8 +83,10 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const runTestIdsPort = await startTestIdServer(testIds); - await this.testServer.sendCommand(options, runTestIdsPort.toString(), () => { + await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, () => { + this.testServer.deleteUUID(uuid); deferred.resolve(); + dispose?.(); }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. diff --git a/src/test/mocks/mockChildProcess.ts b/src/test/mocks/mockChildProcess.ts new file mode 100644 index 000000000000..c038c0f845ab --- /dev/null +++ b/src/test/mocks/mockChildProcess.ts @@ -0,0 +1,238 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Serializable, SendHandle, MessageOptions } from 'child_process'; +import { Writable, Readable, Pipe } from 'stream'; +import { EventEmitter } from 'node:events'; + +export class MockChildProcess extends EventEmitter { + constructor(spawnfile: string, spawnargs: string[]) { + super(); + this.spawnfile = spawnfile; + this.spawnargs = spawnargs; + this.stdin = new Writable(); + this.stdout = new Readable(); + this.stderr = null; + this.channel = null; + this.stdio = [this.stdin, this.stdout, this.stdout, this.stderr, null]; + this.killed = false; + this.connected = false; + this.exitCode = null; + this.signalCode = null; + this.eventMap = new Map(); + } + + stdin: Writable | null; + + stdout: Readable | null; + + stderr: Readable | null; + + eventMap: Map; + + readonly channel?: Pipe | null | undefined; + + readonly stdio: [ + Writable | null, + // stdin + Readable | null, + // stdout + Readable | null, + // stderr + Readable | Writable | null | undefined, + // extra + Readable | Writable | null | undefined, // extra + ]; + + readonly killed: boolean; + + readonly pid?: number | undefined; + + readonly connected: boolean; + + readonly exitCode: number | null; + + readonly signalCode: NodeJS.Signals | null; + + readonly spawnargs: string[]; + + readonly spawnfile: string; + + signal?: NodeJS.Signals | number; + + send(message: Serializable, callback?: (error: Error | null) => void): boolean; + + send(message: Serializable, sendHandle?: SendHandle, callback?: (error: Error | null) => void): boolean; + + send( + message: Serializable, + sendHandle?: SendHandle, + options?: MessageOptions, + callback?: (error: Error | null) => void, + ): boolean; + + send( + message: Serializable, + _sendHandleOrCallback?: SendHandle | ((error: Error | null) => void), + _optionsOrCallback?: MessageOptions | ((error: Error | null) => void), + _callback?: (error: Error | null) => void, + ): boolean { + // Implementation of the send method + // For example, you might want to emit a 'message' event + this.stdout?.push(message.toString()); + return true; + } + + // eslint-disable-next-line class-methods-use-this + disconnect(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + unref(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + ref(): void { + /* noop */ + } + + addListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'disconnect', listener: () => void): this; + + addListener(event: 'error', listener: (err: Error) => void): this; + + addListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + addListener(event: 'spawn', listener: () => void): this; + + addListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + emit(event: 'close', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'disconnect'): boolean; + + emit(event: 'error', err: Error): boolean; + + emit(event: 'exit', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'message', message: Serializable, sendHandle: SendHandle): boolean; + + emit(event: 'spawn', listener: () => void): boolean; + + emit(event: string | symbol, ...args: unknown[]): boolean { + if (this.eventMap.has(event.toString())) { + this.eventMap.get(event.toString()).forEach((listener: (arg0: unknown) => void) => { + const argsArray = Array.isArray(args) ? args : [args]; + listener(argsArray); + }); + } + return true; + } + + on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'disconnect', listener: () => void): this; + + on(event: 'error', listener: (err: Error) => void): this; + + on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + on(event: 'spawn', listener: () => void): this; + + on(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'disconnect', listener: () => void): this; + + once(event: 'error', listener: (err: Error) => void): this; + + once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + once(event: 'spawn', listener: () => void): this; + + once(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'disconnect', listener: () => void): this; + + prependListener(event: 'error', listener: (err: Error) => void): this; + + prependListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependListener(event: 'spawn', listener: () => void): this; + + prependListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependOnceListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'disconnect', listener: () => void): this; + + prependOnceListener(event: 'error', listener: (err: Error) => void): this; + + prependOnceListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependOnceListener(event: 'spawn', listener: () => void): this; + + prependOnceListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + trigger(event: string): Array { + if (this.eventMap.has(event)) { + return this.eventMap.get(event); + } + return []; + } + + kill(_signal?: NodeJS.Signals | number): boolean { + this.stdout?.destroy(); + return true; + } +} diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 18212b2d1032..8ba7dd9a6f00 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; import { ITestServer } from '../../../../client/testing/testController/common/types'; @@ -12,9 +13,11 @@ import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions, + Output, } from '../../../../client/common/process/types'; -import { createDeferred, Deferred } from '../../../../client/common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { Deferred, createDeferred } from '../../../../client/common/utils/async'; suite('pytest test discovery adapter', () => { let testServer: typeMoq.IMock; @@ -29,6 +32,7 @@ suite('pytest test discovery adapter', () => { let expectedPath: string; let uri: Uri; let expectedExtraVariables: Record; + let mockProc: MockChildProcess; setup(() => { const mockExtensionRootDir = typeMoq.Mock.ofType(); @@ -66,32 +70,46 @@ suite('pytest test discovery adapter', () => { }), } as unknown) as IConfigurationService; - // set up exec factory - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => Promise.resolve(execService.object)); - - // set up exec service + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); execService = typeMoq.Mock.ofType(); - deferred = createDeferred(); + const output = new Observable>(() => { + /* no op */ + }); execService - .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => { - deferred.resolve(); - return Promise.resolve({ stdout: '{}' }); - }); + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); outputChannel = typeMoq.Mock.ofType(); }); test('Discovery should call exec with correct basic args', async () => { + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); - await adapter.discoverTests(uri, execFactory.object); - const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); + // verification + const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); @@ -108,16 +126,34 @@ suite('pytest test discovery adapter', () => { const expectedPathNew = path.join('other', 'path'); const configServiceNew: IConfigurationService = ({ getSettings: () => ({ - testing: { pytestArgs: ['.', 'abc', 'xyz'], cwd: expectedPathNew }, + testing: { + pytestArgs: ['.', 'abc', 'xyz'], + cwd: expectedPathNew, + }, }), } as unknown) as IConfigurationService; + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + adapter = new PytestTestDiscoveryAdapter(testServer.object, configServiceNew, outputChannel.object); - await adapter.discoverTests(uri, execFactory.object); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); + + // verification const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.', 'abc', 'xyz']; execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 44116fd753b0..43b763f56e6c 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -6,11 +6,13 @@ import { TestRun, Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as sinon from 'sinon'; import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { ITestServer } from '../../../../client/testing/testController/common/types'; import { IPythonExecutionFactory, IPythonExecutionService, + Output, SpawnOptions, } from '../../../../client/common/process/types'; import { createDeferred, Deferred } from '../../../../client/common/utils/async'; @@ -18,6 +20,7 @@ import { PytestTestExecutionAdapter } from '../../../../client/testing/testContr import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; import * as util from '../../../../client/testing/testController/common/utils'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; suite('pytest test execution adapter', () => { let testServer: typeMoq.IMock; @@ -29,8 +32,8 @@ suite('pytest test execution adapter', () => { let debugLauncher: typeMoq.IMock; (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; let myTestPath: string; - let startTestIdServerStub: sinon.SinonStub; - + let mockProc: MockChildProcess; + let utilsStub: sinon.SinonStub; setup(() => { testServer = typeMoq.Mock.ofType(); testServer.setup((t) => t.getPort()).returns(() => 12345); @@ -47,8 +50,24 @@ suite('pytest test execution adapter', () => { }), isTestExecution: () => false, } as unknown) as IConfigurationService; - execFactory = typeMoq.Mock.ofType(); + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); execService = typeMoq.Mock.ofType(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); + execFactory = typeMoq.Mock.ofType(); + utilsStub = sinon.stub(util, 'startTestIdServer'); debugLauncher = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) @@ -66,7 +85,6 @@ suite('pytest test execution adapter', () => { deferred.resolve(); return Promise.resolve(); }); - startTestIdServerStub = sinon.stub(util, 'startTestIdServer').returns(Promise.resolve(54321)); execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); @@ -77,10 +95,25 @@ suite('pytest test execution adapter', () => { sinon.restore(); }); test('startTestIdServer called with correct testIds', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -88,19 +121,38 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - const testIds = ['test1id', 'test2id']; - await adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); + adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); - sinon.assert.calledWithExactly(startTestIdServerStub, testIds); + // add in await and trigger + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); + + // assert + sinon.assert.calledWithExactly(utilsStub, testIds); }); test('pytest execution called with correct args', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -108,9 +160,12 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - await adapter.runTests(uri, [], false, testRun.object, execFactory.object); + adapter.runTests(uri, [], false, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); @@ -123,7 +178,7 @@ suite('pytest test execution adapter', () => { // execService.verify((x) => x.exec(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); @@ -139,6 +194,21 @@ suite('pytest test execution adapter', () => { ); }); test('pytest execution respects settings.testing.cwd when present', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const newCwd = path.join('new', 'path'); configService = ({ getSettings: () => ({ @@ -149,7 +219,7 @@ suite('pytest test execution adapter', () => { const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -157,9 +227,12 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - await adapter.runTests(uri, [], false, testRun.object, execFactory.object); + adapter.runTests(uri, [], false, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); @@ -172,7 +245,7 @@ suite('pytest test execution adapter', () => { execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); @@ -188,10 +261,17 @@ suite('pytest test execution adapter', () => { ); }); test('Debug launched correctly for pytest', async () => { + const deferred3 = createDeferred(); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -199,9 +279,9 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); await adapter.runTests(uri, [], true, testRun.object, execFactory.object, debugLauncher.object); + await deferred3.promise; debugLauncher.verify( (x) => x.launchDebugger( diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 1131c26c6444..53c2b72e40f7 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -1,15 +1,24 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ import * as assert from 'assert'; import * as net from 'net'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; import { OutputChannel, Uri } from 'vscode'; -import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions } from '../../../client/common/process/types'; +import { Observable } from 'rxjs'; +import * as typeMoq from 'typemoq'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + Output, + SpawnOptions, +} from '../../../client/common/process/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; -import { createDeferred } from '../../../client/common/utils/async'; +import { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; suite('Python Test Server', () => { const fakeUuid = 'fake-uuid'; @@ -18,10 +27,12 @@ suite('Python Test Server', () => { let stubExecutionService: IPythonExecutionService; let server: PythonTestServer; let sandbox: sinon.SinonSandbox; - let execArgs: string[]; - let spawnOptions: SpawnOptions; let v4Stub: sinon.SinonStub; let debugLauncher: ITestDebugLauncher; + let mockProc: MockChildProcess; + let execService: typeMoq.IMock; + let deferred: Deferred; + let execFactory = typeMoq.Mock.ofType(); setup(() => { sandbox = sinon.createSandbox(); @@ -29,27 +40,42 @@ suite('Python Test Server', () => { v4Stub.returns(fakeUuid); stubExecutionService = ({ - exec: (args: string[], spawnOptionsProvided: SpawnOptions) => { - execArgs = args; - spawnOptions = spawnOptionsProvided; - return Promise.resolve({ stdout: '', stderr: '' }); - }, + execObservable: () => Promise.resolve({ stdout: '', stderr: '' }), } as unknown) as IPythonExecutionService; stubExecutionFactory = ({ createActivatedEnvironment: () => Promise.resolve(stubExecutionService), } as unknown) as IPythonExecutionFactory; + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + execService = typeMoq.Mock.ofType(); + const output = new Observable>(() => { + /* no op */ + }); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); }); teardown(() => { sandbox.restore(); - execArgs = []; server.dispose(); }); test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, + command: { + script: 'myscript', + args: ['-foo', 'foo'], + }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, @@ -59,17 +85,31 @@ suite('Python Test Server', () => { outputChannel: undefined, token: undefined, throwOnStdErr: true, - extraVariables: { PYTHONPATH: '/foo/bar', RUN_TEST_IDS_PORT: '56789' }, + extraVariables: { + PYTHONPATH: '/foo/bar', + RUN_TEST_IDS_PORT: '56789', + }, } as SpawnOptions; + const deferred2 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(execFactory.object, debugLauncher); await server.serverReady(); - await server.sendCommand(options, '56789'); - const port = server.getPort(); + server.sendCommand(options, '56789'); + // add in await and trigger + await deferred2.promise; + mockProc.trigger('close'); - assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); - assert.deepStrictEqual(spawnOptions, expectedSpawnOptions); + const port = server.getPort(); + const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']; + execService.verify((x) => x.execObservable(expectedArgs, expectedSpawnOptions), typeMoq.Times.once()); }); test('sendCommand should write to an output channel if it is provided as an option', async () => { @@ -80,17 +120,31 @@ suite('Python Test Server', () => { }, } as OutputChannel; const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, + command: { + script: 'myscript', + args: ['-foo', 'foo'], + }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, outChannel, }; + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(execFactory.object, debugLauncher); await server.serverReady(); - await server.sendCommand(options); + server.sendCommand(options); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); const port = server.getPort(); const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); @@ -99,13 +153,12 @@ suite('Python Test Server', () => { }); test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { - let eventData: { status: string; errors: string[] }; + let eventData: { status: string; errors: string[] } | undefined; stubExecutionService = ({ - exec: () => { + execObservable: () => { throw new Error('Failed to execute'); }, } as unknown) as IPythonExecutionService; - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -122,30 +175,43 @@ suite('Python Test Server', () => { await server.sendCommand(options); - assert.deepStrictEqual(eventData!.status, 'error'); - assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); + assert.notEqual(eventData, undefined); + assert.deepStrictEqual(eventData?.status, 'error'); + assert.deepStrictEqual(eventData?.errors, ['Failed to execute']); }); test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + + deferred = createDeferred(); + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -161,16 +227,17 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); + // add in await and trigger await deferred.promise; + mockProc.trigger('close'); + assert.deepStrictEqual(eventData, ''); }); test('If the server doesnt recognize the UUID it should ignore it', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -178,14 +245,28 @@ suite('Python Test Server', () => { uuid: fakeUuid, }; - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -201,7 +282,7 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, ''); }); @@ -212,23 +293,34 @@ suite('Python Test Server', () => { test('Error if payload does not have a content length header', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -244,7 +336,7 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, ''); }); @@ -267,7 +359,6 @@ Request-uuid: UUID_HERE // Your test logic here let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, @@ -275,15 +366,28 @@ Request-uuid: UUID_HERE cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); const uuid = server.createUUID(); payload = payload.replace('UUID_HERE', uuid); @@ -301,7 +405,7 @@ Request-uuid: UUID_HERE console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, expectedResult); }); @@ -310,8 +414,29 @@ Request-uuid: UUID_HERE test('Calls run resolver if the result header is in the payload', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { + client.connect(server.getPort()); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }, + } as unknown) as IPythonExecutionService; + + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -319,14 +444,6 @@ Request-uuid: UUID_HERE uuid: fakeUuid, }; - stubExecutionService = ({ - exec: async () => { - client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); - }, - } as unknown) as IPythonExecutionService; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); const uuid = server.createUUID(); server.onRunDataReceived(({ data }) => { @@ -349,9 +466,8 @@ Request-uuid: ${uuid} console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; - console.log('event data', eventData); const expectedResult = '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; assert.deepStrictEqual(eventData, expectedResult); diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts index 5a2e48130746..41cd1bbd7ef2 100644 --- a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -164,8 +164,7 @@ suite('Workspace test adapter', () => { const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); const testProvider = 'unittest'; - const abc = await workspaceTestAdapter.discoverTests(testController); - console.log(abc); + await workspaceTestAdapter.discoverTests(testController); sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider); From 3e7118fe205d3489fd7a396c3d1897416c8380d5 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 31 Jul 2023 18:56:11 -0700 Subject: [PATCH 0112/1136] Update packages for Jedi and core python (#21710) --- .../jedilsp_requirements/requirements.txt | 134 +++++++++--------- requirements.txt | 10 +- 2 files changed, 75 insertions(+), 69 deletions(-) diff --git a/pythonFiles/jedilsp_requirements/requirements.txt b/pythonFiles/jedilsp_requirements/requirements.txt index 062b037e0783..90d10b467640 100644 --- a/pythonFiles/jedilsp_requirements/requirements.txt +++ b/pythonFiles/jedilsp_requirements/requirements.txt @@ -4,28 +4,31 @@ # # pip-compile --generate-hashes 'pythonFiles\jedilsp_requirements\requirements.in' # -attrs==22.2.0 \ - --hash=sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836 \ - --hash=sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99 +attrs==23.1.0 \ + --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \ + --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015 # via # cattrs # lsprotocol -cattrs==22.2.0 \ - --hash=sha256:bc12b1f0d000b9f9bee83335887d532a1d3e99a833d1bf0882151c97d3e68c21 \ - --hash=sha256:f0eed5642399423cf656e7b66ce92cdc5b963ecafd041d1b24d136fdde7acf6d +cattrs==23.1.2 \ + --hash=sha256:b2bb14311ac17bed0d58785e5a60f022e5431aca3932e3fc5cc8ed8639de50a4 \ + --hash=sha256:db1c821b8c537382b2c7c66678c3790091ca0275ac486c76f3c8f3920e83c657 # via lsprotocol -docstring-to-markdown==0.11 \ - --hash=sha256:01900aee1bc7fde5aacaf319e517a5e1d4f0bf04e401373c08d28fcf79bfb73b \ - --hash=sha256:5b1da2c89d9d0d09b955dec0ee111284ceadd302a938a03ed93f66e09134f9b5 +docstring-to-markdown==0.12 \ + --hash=sha256:40004224b412bd6f64c0f3b85bb357a41341afd66c4b4896709efa56827fb2bb \ + --hash=sha256:7df6311a887dccf9e770f51242ec002b19f0591994c4783be49d24cdc1df3737 # via jedi-language-server -exceptiongroup==1.1.0 \ - --hash=sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e \ - --hash=sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23 +exceptiongroup==1.1.2 \ + --hash=sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5 \ + --hash=sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f # via cattrs importlib-metadata==3.10.1 \ --hash=sha256:2ec0faae539743ae6aaa84b49a169670a465f7f5d64e6add98388cc29fd1f2f6 \ --hash=sha256:c9356b657de65c53744046fa8f7358afe0714a1af7d570c00c3835c2d724a7c1 - # via jedi-language-server + # via + # attrs + # jedi-language-server + # typeguard jedi==0.18.2 \ --hash=sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e \ --hash=sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612 @@ -34,9 +37,9 @@ jedi-language-server==0.40.0 \ --hash=sha256:53e590400b5cd2f6e363e77a4d824b1883798994b731cb0b4370d103748d30e2 \ --hash=sha256:bacbae2930b6a8a0f1f284c211672fceec94b4808b0415d1c3352fa4b1ac5ad6 # via -r pythonFiles\jedilsp_requirements\requirements.in -lsprotocol==2022.0.0a10 \ - --hash=sha256:2cd78770b7a4ec979f3ee3761265effd50ea0f5e858ce21bf2fba972e1783c50 \ - --hash=sha256:ef516aec43c2b3c8debc06e84558ea9a64c36d635422d1614fd7fd2a45b1d291 +lsprotocol==2023.0.0a2 \ + --hash=sha256:80aae7e39171b49025876a524937c10be2eb986f4be700ca22ee7d186b8488aa \ + --hash=sha256:c4f2f77712b50d065b17f9b50d2b88c480dc2ce4bbaa56eea8269dbf54bc9701 # via # jedi-language-server # pygls @@ -44,62 +47,63 @@ parso==0.8.3 \ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 # via jedi -pydantic==1.10.4 \ - --hash=sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72 \ - --hash=sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423 \ - --hash=sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f \ - --hash=sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c \ - --hash=sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06 \ - --hash=sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53 \ - --hash=sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774 \ - --hash=sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6 \ - --hash=sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c \ - --hash=sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f \ - --hash=sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6 \ - --hash=sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3 \ - --hash=sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817 \ - --hash=sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903 \ - --hash=sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a \ - --hash=sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e \ - --hash=sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d \ - --hash=sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85 \ - --hash=sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00 \ - --hash=sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28 \ - --hash=sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3 \ - --hash=sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024 \ - --hash=sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4 \ - --hash=sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e \ - --hash=sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d \ - --hash=sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa \ - --hash=sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854 \ - --hash=sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15 \ - --hash=sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648 \ - --hash=sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8 \ - --hash=sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c \ - --hash=sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857 \ - --hash=sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f \ - --hash=sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416 \ - --hash=sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978 \ - --hash=sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d +pydantic==1.10.12 \ + --hash=sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303 \ + --hash=sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe \ + --hash=sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47 \ + --hash=sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494 \ + --hash=sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33 \ + --hash=sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86 \ + --hash=sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d \ + --hash=sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c \ + --hash=sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a \ + --hash=sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565 \ + --hash=sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb \ + --hash=sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62 \ + --hash=sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62 \ + --hash=sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0 \ + --hash=sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523 \ + --hash=sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d \ + --hash=sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405 \ + --hash=sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f \ + --hash=sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b \ + --hash=sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718 \ + --hash=sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed \ + --hash=sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb \ + --hash=sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5 \ + --hash=sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc \ + --hash=sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942 \ + --hash=sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe \ + --hash=sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246 \ + --hash=sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350 \ + --hash=sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303 \ + --hash=sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09 \ + --hash=sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33 \ + --hash=sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8 \ + --hash=sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a \ + --hash=sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1 \ + --hash=sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6 \ + --hash=sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d # via jedi-language-server -pygls==1.0.0 \ - --hash=sha256:3414594ac29ff3ab990f004c675d1077e4e2659eae5cc3ae67cc6fa4d861e342 \ - --hash=sha256:c2a1c22e30028f7ca9d3f0a04da8eef29f0f1701bdbd97d8614d8e1e6711f336 +pygls==1.0.2 \ + --hash=sha256:6d278d29fa6559b0f7a448263c85cb64ec6e9369548b02f1a7944060848b21f9 \ + --hash=sha256:888ed63d1f650b4fc64d603d73d37545386ec533c0caac921aed80f80ea946a4 # via # -r pythonFiles\jedilsp_requirements\requirements.in # jedi-language-server -typeguard==2.13.3 \ - --hash=sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4 \ - --hash=sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1 +typeguard==3.0.2 \ + --hash=sha256:bbe993854385284ab42fd5bd3bee6f6556577ce8b50696d6cb956d704f286c8e \ + --hash=sha256:fee5297fdb28f8e9efcb8142b5ee219e02375509cd77ea9d270b5af826358d5a # via pygls -typing-extensions==4.4.0 \ - --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ - --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e +typing-extensions==4.7.1 \ + --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ + --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 # via # cattrs # importlib-metadata # pydantic -zipp==3.12.0 \ - --hash=sha256:73efd63936398aac78fd92b6f4865190119d6c91b531532e798977ea8dd402eb \ - --hash=sha256:9eb0a4c5feab9b08871db0d672745b53450d7f26992fd1e4653aa43345e97b86 + # typeguard +zipp==3.15.0 \ + --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ + --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 # via importlib-metadata diff --git a/requirements.txt b/requirements.txt index 2747490fdba8..f2af0ca4204b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +4,9 @@ # # pip-compile --generate-hashes requirements.in # -importlib-metadata==6.6.0 \ - --hash=sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed \ - --hash=sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705 +importlib-metadata==6.7.0 \ + --hash=sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4 \ + --hash=sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5 # via -r requirements.in microvenv==2023.2.0 \ --hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \ @@ -23,7 +23,9 @@ tomli==2.0.1 \ typing-extensions==4.7.1 \ --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 - # via -r requirements.in + # via + # -r requirements.in + # importlib-metadata zipp==3.15.0 \ --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 From 4ab510d1ae6e079b5d0a2509d7cd21822d93e17e Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 1 Aug 2023 13:59:38 -0700 Subject: [PATCH 0113/1136] Update version for release candidate (#21727) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3678852d1f65..ae13db90c8df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.13.0-dev", + "version": "2023.14.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.13.0-dev", + "version": "2023.14.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index ad196c164a5f..96e252302abb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", - "version": "2023.13.0-dev", + "version": "2023.14.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From a6a8cb183913e20249a2bb8165c9a5b0d91de40e Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 1 Aug 2023 14:23:43 -0700 Subject: [PATCH 0114/1136] Update main to next pre-release (#21728) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae13db90c8df..01f03cb31661 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.14.0-rc", + "version": "2023.15.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.14.0-rc", + "version": "2023.15.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 96e252302abb..b6a4b65b42a9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", - "version": "2023.14.0-rc", + "version": "2023.15.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 8f3d60bf378a2c50609d5512c930e4174bf61728 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 1 Aug 2023 14:32:44 -0700 Subject: [PATCH 0115/1136] unittest discovery errors not displaying in test explorer (#21726) saw an issue where if discovery failed there was no notice in the test explorer for unittest. It was due to a different value for the new blank value for the payload tests. fixes https://github.com/microsoft/vscode-python/issues/21725 and https://github.com/microsoft/vscode-python/issues/21688 --- src/client/testing/testController/common/resultResolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 8baf4d0d7ae7..6e875473c836 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -68,7 +68,7 @@ export class PythonResultResolver implements ITestResultResolver { // remove error node only if no errors exist. this.testController.items.delete(`DiscoveryError:${workspacePath}`); } - if (rawTestData.tests) { + if (rawTestData.tests || rawTestData.tests === null) { // if any tests exist, they should be populated in the test tree, regardless of whether there were errors or not. // parse and insert test data. From 358635da33750e05af3edb1497f601fd9a71a27b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 2 Aug 2023 20:27:00 +1000 Subject: [PATCH 0116/1136] Remove old and unused API for Jupyter Ext (#21731) We have not used any of this API for a while now, hence its safe to remove these. Will be removing more soon. --- src/client/common/installer/condaInstaller.ts | 13 +- .../common/installer/moduleInstaller.ts | 12 -- src/client/common/installer/productNames.ts | 6 - src/client/common/installer/productService.ts | 6 - src/client/common/types.ts | 6 - src/client/jupyter/jupyterIntegration.ts | 144 +---------------- ...eractiveWindowMiddlewareAddon.unit.test.ts | 12 +- .../installer/productInstaller.unit.test.ts | 149 ------------------ 8 files changed, 6 insertions(+), 342 deletions(-) diff --git a/src/client/common/installer/condaInstaller.ts b/src/client/common/installer/condaInstaller.ts index a20b35e0f110..fbb3dcf183ef 100644 --- a/src/client/common/installer/condaInstaller.ts +++ b/src/client/common/installer/condaInstaller.ts @@ -88,18 +88,7 @@ export class CondaInstaller extends ModuleInstaller { // Found that using conda-forge is best at packages like tensorboard & ipykernel which seem to get updated first on conda-forge // https://github.com/microsoft/vscode-jupyter/issues/7787 & https://github.com/microsoft/vscode-python/issues/17628 // Do this just for the datascience packages. - if ( - [ - Product.tensorboard, - Product.ipykernel, - Product.pandas, - Product.nbconvert, - Product.jupyter, - Product.notebook, - ] - .map(translateProductToModule) - .includes(moduleName) - ) { + if ([Product.tensorboard].map(translateProductToModule).includes(moduleName)) { args.push('-c', 'conda-forge'); } if (info && info.name) { diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index 62160b7e25c9..f70dd937aba9 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -264,18 +264,6 @@ export function translateProductToModule(product: Product): string { return 'unittest'; case Product.bandit: return 'bandit'; - case Product.jupyter: - return 'jupyter'; - case Product.notebook: - return 'notebook'; - case Product.pandas: - return 'pandas'; - case Product.ipykernel: - return 'ipykernel'; - case Product.nbconvert: - return 'nbconvert'; - case Product.kernelspec: - return 'kernelspec'; case Product.tensorboard: return 'tensorboard'; case Product.torchProfilerInstallName: diff --git a/src/client/common/installer/productNames.ts b/src/client/common/installer/productNames.ts index 6474e8a2a514..378fd5a38dba 100644 --- a/src/client/common/installer/productNames.ts +++ b/src/client/common/installer/productNames.ts @@ -19,11 +19,5 @@ ProductNames.set(Product.yapf, 'yapf'); ProductNames.set(Product.tensorboard, 'tensorboard'); ProductNames.set(Product.torchProfilerInstallName, 'torch-tb-profiler'); ProductNames.set(Product.torchProfilerImportName, 'torch_tb_profiler'); -ProductNames.set(Product.jupyter, 'jupyter'); -ProductNames.set(Product.notebook, 'notebook'); -ProductNames.set(Product.ipykernel, 'ipykernel'); -ProductNames.set(Product.nbconvert, 'nbconvert'); -ProductNames.set(Product.kernelspec, 'kernelspec'); -ProductNames.set(Product.pandas, 'pandas'); ProductNames.set(Product.pip, 'pip'); ProductNames.set(Product.ensurepip, 'ensurepip'); diff --git a/src/client/common/installer/productService.ts b/src/client/common/installer/productService.ts index 5de130e84d06..26a01e37c3ba 100644 --- a/src/client/common/installer/productService.ts +++ b/src/client/common/installer/productService.ts @@ -25,12 +25,6 @@ export class ProductService implements IProductService { this.ProductTypes.set(Product.autopep8, ProductType.Formatter); this.ProductTypes.set(Product.black, ProductType.Formatter); this.ProductTypes.set(Product.yapf, ProductType.Formatter); - this.ProductTypes.set(Product.jupyter, ProductType.DataScience); - this.ProductTypes.set(Product.notebook, ProductType.DataScience); - this.ProductTypes.set(Product.ipykernel, ProductType.DataScience); - this.ProductTypes.set(Product.nbconvert, ProductType.DataScience); - this.ProductTypes.set(Product.kernelspec, ProductType.DataScience); - this.ProductTypes.set(Product.pandas, ProductType.DataScience); this.ProductTypes.set(Product.tensorboard, ProductType.DataScience); this.ProductTypes.set(Product.torchProfilerInstallName, ProductType.DataScience); this.ProductTypes.set(Product.torchProfilerImportName, ProductType.DataScience); diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 3359854f89b7..b48a2daadaa6 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -109,12 +109,6 @@ export enum Product { isort = 15, black = 16, bandit = 17, - jupyter = 18, - ipykernel = 19, - notebook = 20, - kernelspec = 21, - nbconvert = 22, - pandas = 23, tensorboard = 24, torchProfilerInstallName = 25, torchProfilerImportName = 26, diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts index 7f660d26e5e4..dbfd1bdf5681 100644 --- a/src/client/jupyter/jupyterIntegration.ts +++ b/src/client/jupyter/jupyterIntegration.ts @@ -6,90 +6,20 @@ import { inject, injectable, named } from 'inversify'; import { dirname } from 'path'; -import { CancellationToken, Event, Extension, Memento, Uri } from 'vscode'; +import { Extension, Memento, Uri } from 'vscode'; import type { SemVer } from 'semver'; import { IContextKeyManager, IWorkspaceService } from '../common/application/types'; import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants'; -import { InterpreterUri, ModuleInstallFlags } from '../common/installer/types'; -import { - GLOBAL_MEMENTO, - IExtensions, - IInstaller, - IMemento, - InstallerResponse, - Product, - ProductInstallStatus, - Resource, -} from '../common/types'; +import { GLOBAL_MEMENTO, IExtensions, IMemento, Resource } from '../common/types'; import { getDebugpyPackagePath } from '../debugger/extension/adapter/remoteLaunchers'; import { IEnvironmentActivationService } from '../interpreter/activation/types'; import { IInterpreterQuickPickItem, IInterpreterSelector } from '../interpreter/configuration/types'; -import { - IComponentAdapter, - ICondaService, - IInterpreterDisplay, - IInterpreterService, - IInterpreterStatusbarVisibilityFilter, - PythonEnvironmentsChangedEvent, -} from '../interpreter/contracts'; +import { ICondaService, IInterpreterDisplay, IInterpreterStatusbarVisibilityFilter } from '../interpreter/contracts'; import { PythonEnvironment } from '../pythonEnvironments/info'; import { PylanceApi } from '../activation/node/pylanceApi'; import { ExtensionContextKey } from '../common/application/contextKeys'; -/** - * This allows Python extension to update Product enum without breaking Jupyter. - * I.e. we have a strict contract, else using numbers (in enums) is bound to break across products. - */ -enum JupyterProductToInstall { - jupyter = 'jupyter', - ipykernel = 'ipykernel', - notebook = 'notebook', - kernelspec = 'kernelspec', - nbconvert = 'nbconvert', - pandas = 'pandas', - pip = 'pip', -} - -const ProductMapping: { [key in JupyterProductToInstall]: Product } = { - [JupyterProductToInstall.ipykernel]: Product.ipykernel, - [JupyterProductToInstall.jupyter]: Product.jupyter, - [JupyterProductToInstall.kernelspec]: Product.kernelspec, - [JupyterProductToInstall.nbconvert]: Product.nbconvert, - [JupyterProductToInstall.notebook]: Product.notebook, - [JupyterProductToInstall.pandas]: Product.pandas, - [JupyterProductToInstall.pip]: Product.pip, -}; type PythonApiForJupyterExtension = { - /** - * IInterpreterService - */ - onDidChangeInterpreter: Event; - /** - * IInterpreterService - */ - readonly refreshPromise: Promise | undefined; - /** - * IInterpreterService - */ - readonly onDidChangeInterpreters: Event; - /** - * Equivalent to getInterpreters() in IInterpreterService - */ - getKnownInterpreters(resource?: Uri): PythonEnvironment[]; - /** - * @deprecated Use `getKnownInterpreters`, `onDidChangeInterpreters`, and `refreshPromise` instead. - * Equivalent to getAllInterpreters() in IInterpreterService - */ - getInterpreters(resource?: Uri): Promise; - /** - * IInterpreterService - */ - getActiveInterpreter(resource?: Uri): Promise; - /** - * IInterpreterService - */ - getInterpreterDetails(pythonPath: string, resource?: Uri): Promise; - /** * IEnvironmentActivationService */ @@ -98,31 +28,11 @@ type PythonApiForJupyterExtension = { interpreter?: PythonEnvironment, allowExceptions?: boolean, ): Promise; - isMicrosoftStoreInterpreter(pythonPath: string): Promise; - suggestionToQuickPickItem(suggestion: PythonEnvironment, workspaceUri?: Uri | undefined): IInterpreterQuickPickItem; getKnownSuggestions(resource: Resource): IInterpreterQuickPickItem[]; /** * @deprecated Use `getKnownSuggestions` and `suggestionToQuickPickItem` instead. */ getSuggestions(resource: Resource): Promise; - /** - * IInstaller - */ - install( - product: JupyterProductToInstall, - resource?: InterpreterUri, - cancel?: CancellationToken, - reInstallAndUpdate?: boolean, - installPipIfRequired?: boolean, - ): Promise; - /** - * IInstaller - */ - isProductVersionCompatible( - product: Product, - semVerRequirement: string, - resource?: InterpreterUri, - ): Promise; /** * Returns path to where `debugpy` is. In python extension this is `/pythonFiles/lib/python`. */ @@ -140,10 +50,6 @@ type PythonApiForJupyterExtension = { * Returns the conda executable. */ getCondaFile(): Promise; - getEnvironmentActivationShellCommands( - resource: Resource, - interpreter?: PythonEnvironment, - ): Promise; /** * Call to provide a function that the Python extension can call to request the Python @@ -181,13 +87,10 @@ export class JupyterExtensionIntegration { constructor( @inject(IExtensions) private readonly extensions: IExtensions, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IInterpreterSelector) private readonly interpreterSelector: IInterpreterSelector, - @inject(IInstaller) private readonly installer: IInstaller, @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, @inject(IInterpreterDisplay) private interpreterDisplay: IInterpreterDisplay, - @inject(IComponentAdapter) private pyenvs: IComponentAdapter, @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(ICondaService) private readonly condaService: ICondaService, @inject(IContextKeyManager) private readonly contextManager: IContextKeyManager, @@ -201,54 +104,15 @@ export class JupyterExtensionIntegration { } // Forward python parts jupyterExtensionApi.registerPythonApi({ - onDidChangeInterpreter: this.interpreterService.onDidChangeInterpreter, - getActiveInterpreter: async (resource?: Uri) => this.interpreterService.getActiveInterpreter(resource), - getInterpreterDetails: async (pythonPath: string) => - this.interpreterService.getInterpreterDetails(pythonPath), - refreshPromise: this.interpreterService.refreshPromise, - onDidChangeInterpreters: this.interpreterService.onDidChangeInterpreters, - getKnownInterpreters: (resource: Uri | undefined) => this.pyenvs.getInterpreters(resource), - getInterpreters: async (resource: Uri | undefined) => this.interpreterService.getAllInterpreters(resource), getActivatedEnvironmentVariables: async ( resource: Resource, interpreter?: PythonEnvironment, allowExceptions?: boolean, ) => this.envActivation.getActivatedEnvironmentVariables(resource, interpreter, allowExceptions), - isMicrosoftStoreInterpreter: async (pythonPath: string): Promise => - this.pyenvs.isMicrosoftStoreInterpreter(pythonPath), getSuggestions: async (resource: Resource): Promise => this.interpreterSelector.getAllSuggestions(resource), getKnownSuggestions: (resource: Resource): IInterpreterQuickPickItem[] => this.interpreterSelector.getSuggestions(resource), - suggestionToQuickPickItem: ( - suggestion: PythonEnvironment, - workspaceUri?: Uri | undefined, - ): IInterpreterQuickPickItem => - this.interpreterSelector.suggestionToQuickPickItem(suggestion, workspaceUri), - install: async ( - product: JupyterProductToInstall, - resource?: InterpreterUri, - cancel?: CancellationToken, - reInstallAndUpdate?: boolean, - installPipIfRequired?: boolean, - ): Promise => { - let flags = - reInstallAndUpdate === true - ? ModuleInstallFlags.updateDependencies | ModuleInstallFlags.reInstall - : undefined; - if (installPipIfRequired === true) { - flags = flags - ? flags | ModuleInstallFlags.installPipIfRequired - : ModuleInstallFlags.installPipIfRequired; - } - return this.installer.install(ProductMapping[product], resource, cancel, flags); - }, - isProductVersionCompatible: async ( - product: Product, - semVerRequirement: string, - resource?: InterpreterUri, - ): Promise => - this.installer.isProductVersionCompatible(product, semVerRequirement, resource), getDebuggerPath: async () => dirname(getDebugpyPackagePath()), getInterpreterPathSelectedForJupyterServer: () => this.globalState.get('INTERPRETER_PATH_SELECTED_FOR_JUPYTER_SERVER'), @@ -257,8 +121,6 @@ export class JupyterExtensionIntegration { ), getCondaFile: () => this.condaService.getCondaFile(), getCondaVersion: () => this.condaService.getCondaVersion(), - getEnvironmentActivationShellCommands: (resource: Resource, interpreter?: PythonEnvironment) => - this.envActivation.getEnvironmentActivationShellCommands(resource, interpreter), registerJupyterPythonPathFunction: (func: (uri: Uri) => Promise) => this.registerJupyterPythonPathFunction(func), registerGetNotebookUriForTextDocumentUriFunction: (func: (textDocumentUri: Uri) => Uri | undefined) => diff --git a/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts b/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts index 256e57a5d724..32d9198ef7ba 100644 --- a/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts +++ b/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts @@ -10,13 +10,8 @@ import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; import { LanguageClient } from 'vscode-languageclient/node'; import { LspInteractiveWindowMiddlewareAddon } from '../../../client/activation/node/lspInteractiveWindowMiddlewareAddon'; import { JupyterExtensionIntegration } from '../../../client/jupyter/jupyterIntegration'; -import { IExtensions, IInstaller } from '../../../client/common/types'; -import { - IComponentAdapter, - ICondaService, - IInterpreterDisplay, - IInterpreterService, -} from '../../../client/interpreter/contracts'; +import { IExtensions } from '../../../client/common/types'; +import { ICondaService, IInterpreterDisplay } from '../../../client/interpreter/contracts'; import { IInterpreterSelector } from '../../../client/interpreter/configuration/types'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IContextKeyManager, IWorkspaceService } from '../../../client/common/application/types'; @@ -32,13 +27,10 @@ suite('Pylance Language Server - Interactive Window LSP Notebooks', () => { languageClient = instance(languageClientMock); jupyterApi = new JupyterExtensionIntegration( mock(), - mock(), mock(), - mock(), mock(), new MockMemento(), mock(), - mock(), mock(), mock(), mock(), diff --git a/src/test/common/installer/productInstaller.unit.test.ts b/src/test/common/installer/productInstaller.unit.test.ts index ed1be158c0aa..66e0cc005870 100644 --- a/src/test/common/installer/productInstaller.unit.test.ts +++ b/src/test/common/installer/productInstaller.unit.test.ts @@ -57,155 +57,6 @@ suite('DataScienceInstaller install', async () => { // noop }); - test('Requires interpreter Uri', async () => { - let threwUp = false; - try { - await dataScienceInstaller.install(Product.ipykernel); - } catch (ex) { - threwUp = true; - } - expect(threwUp).to.equal(true, 'Should raise exception'); - }); - - test('Will ignore with no installer modules', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.VirtualEnv, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([])); - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Ignore, 'Should be InstallerResponse.Ignore'); - }); - - test('Will invoke conda for conda environments', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.Conda, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - const testInstaller = TypeMoq.Mock.ofType(); - testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Conda); - testInstaller - .setup((c) => - c.installModule( - TypeMoq.It.isValue(Product.ipykernel), - TypeMoq.It.isValue(testEnvironment), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve()); - - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([testInstaller.object])); - - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); - }); - - test('Will invoke pip by default', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.VirtualEnv, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - const testInstaller = TypeMoq.Mock.ofType(); - - testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Pip); - testInstaller - .setup((c) => - c.installModule( - TypeMoq.It.isValue(Product.ipykernel), - TypeMoq.It.isValue(testEnvironment), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve()); - - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([testInstaller.object])); - - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); - }); - - test('Will invoke poetry', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.Poetry, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - const testInstaller = TypeMoq.Mock.ofType(); - - testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Poetry); - testInstaller - .setup((c) => - c.installModule( - TypeMoq.It.isValue(Product.ipykernel), - TypeMoq.It.isValue(testEnvironment), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve()); - - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([testInstaller.object])); - - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); - }); - - test('Will invoke pipenv', async () => { - const testEnvironment: PythonEnvironment = { - envType: EnvironmentType.Pipenv, - envName: 'test', - envPath: interpreterPath, - path: interpreterPath, - architecture: Architecture.x64, - sysPrefix: '', - }; - const testInstaller = TypeMoq.Mock.ofType(); - - testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Pipenv); - testInstaller - .setup((c) => - c.installModule( - TypeMoq.It.isValue(Product.ipykernel), - TypeMoq.It.isValue(testEnvironment), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve()); - - installationChannelManager - .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) - .returns(() => Promise.resolve([testInstaller.object])); - - const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); - expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); - }); - test('Will invoke pip for pytorch with conda environment', async () => { // See https://github.com/microsoft/vscode-jupyter/issues/5034 const testEnvironment: PythonEnvironment = { From ef16727d0b26238b8606bab3e821720f159bcdda Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:36:26 -0700 Subject: [PATCH 0117/1136] Clean up smoke test requirement (#21729) Cleaning up smoke test dependency: See if all Github action test pass with removing the smoke test requirement file content. Checked one by one, and came to see removing all doesn't seem to have impact on the outcome of running smoke test.(Seems to have no difference in smoke test result outcome when ran with "Run and Debug" in VS Code with smoke-test option selected). Also got rid of below, after checking smoke test correctly passing after removal of smoke-test-requirement.txt content: ![Screenshot 2023-08-01 at 2 57 45 PM](https://github.com/microsoft/vscode-python/assets/62267334/45d404de-74dd-45a5-885b-71a25ef16ad7) Resolve: #21496 --- .github/actions/smoke-tests/action.yml | 6 ------ build/smoke-test-requirements.txt | 6 ------ scripts/onCreateCommand.sh | 1 - 3 files changed, 13 deletions(-) delete mode 100644 build/smoke-test-requirements.txt diff --git a/.github/actions/smoke-tests/action.yml b/.github/actions/smoke-tests/action.yml index 9ad6e87cdd26..b2d002050433 100644 --- a/.github/actions/smoke-tests/action.yml +++ b/.github/actions/smoke-tests/action.yml @@ -26,7 +26,6 @@ runs: cache-dependency-path: | build/test-requirements.txt requirements.txt - build/smoke-test-requirements.txt - name: Install dependencies (npm ci) run: npm ci --prefer-offline @@ -43,11 +42,6 @@ runs: python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --implementation py --no-deps --upgrade --pre debugpy shell: bash - - name: pip install smoke test requirements - run: | - python -m pip install --upgrade -r build/smoke-test-requirements.txt - shell: bash - # Bits from the VSIX are reused by smokeTest.ts to speed things up. - name: Download VSIX uses: actions/download-artifact@v2 diff --git a/build/smoke-test-requirements.txt b/build/smoke-test-requirements.txt deleted file mode 100644 index 7d5ac3da00d9..000000000000 --- a/build/smoke-test-requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -# List of requirements for smoke tests (they will attempt to run a kernel) -jupyter -numpy -matplotlib -pandas -livelossplot \ No newline at end of file diff --git a/scripts/onCreateCommand.sh b/scripts/onCreateCommand.sh index e93c74f610b9..a90a5366417d 100644 --- a/scripts/onCreateCommand.sh +++ b/scripts/onCreateCommand.sh @@ -29,7 +29,6 @@ source /workspaces/vscode-python/.venv/bin/activate npx gulp installPythonLibs /workspaces/vscode-python/.venv/bin/python -m pip install -r build/test-requirements.txt -/workspaces/vscode-python/.venv/bin/python -m pip install -r build/smoke-test-requirements.txt /workspaces/vscode-python/.venv/bin/python -m pip install -r build/functional-test-requirements.txt # Below will crash codespace From 84bbff9c7b6f0a2835829bba414ca263bd66de20 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 2 Aug 2023 15:08:15 -0700 Subject: [PATCH 0118/1136] add cwd for debugging (#21668) fixes https://github.com/microsoft/vscode-python/issues/21648#issuecomment-1638551934 --- src/client/testing/common/debugLauncher.ts | 16 ++++++- src/client/testing/common/helpers.ts | 37 ++++++++++++++ .../testing/common/debugLauncher.unit.test.ts | 8 ++++ src/test/testing/common/helpers.unit.test.ts | 48 +++++++++++++++++++ 4 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 src/client/testing/common/helpers.ts create mode 100644 src/test/testing/common/helpers.unit.test.ts diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index dcf23c0478db..63e2a4543beb 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -17,6 +17,7 @@ import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis import { showErrorMessage } from '../../common/vscodeApis/windowApis'; import { createDeferred } from '../../common/utils/async'; import { pythonTestAdapterRewriteEnabled } from '../testController/common/utils'; +import { addPathToPythonpath } from './helpers'; @injectable() export class DebugLauncher implements ITestDebugLauncher { @@ -223,8 +224,19 @@ export class DebugLauncher implements ITestDebugLauncher { `Missing value for debug setup, both port and uuid need to be defined. port: "${options.pytestPort}" uuid: "${options.pytestUUID}"`, ); } - const pluginPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); - launchArgs.env.PYTHONPATH = pluginPath; + } + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); + // check if PYTHONPATH is already set in the environment variables + if (launchArgs.env) { + const additionalPythonPath = [pluginPath]; + if (launchArgs.cwd) { + additionalPythonPath.push(launchArgs.cwd); + } else if (options.cwd) { + additionalPythonPath.push(options.cwd); + } + // add the plugin path or cwd to PYTHONPATH if it is not already there using the following function + // this function will handle if PYTHONPATH is undefined + addPathToPythonpath(additionalPythonPath, launchArgs.env.PYTHONPATH); } // Clear out purpose so we can detect if the configuration was used to diff --git a/src/client/testing/common/helpers.ts b/src/client/testing/common/helpers.ts new file mode 100644 index 000000000000..021849277b33 --- /dev/null +++ b/src/client/testing/common/helpers.ts @@ -0,0 +1,37 @@ +import * as path from 'path'; + +/** + * This function normalizes the provided paths and the existing paths in PYTHONPATH, + * adds the provided paths to PYTHONPATH if they're not already present, + * and then returns the updated PYTHONPATH. + * + * @param newPaths - An array of paths to be added to PYTHONPATH + * @param launchPythonPath - The initial PYTHONPATH + * @returns The updated PYTHONPATH + */ +export function addPathToPythonpath(newPaths: string[], launchPythonPath: string | undefined): string { + // Split PYTHONPATH into array of paths if it exists + let paths: string[]; + if (!launchPythonPath) { + paths = []; + } else { + paths = launchPythonPath.split(path.delimiter); + } + + // Normalize each path in the existing PYTHONPATH + paths = paths.map((p) => path.normalize(p)); + + // Normalize each new path and add it to PYTHONPATH if it's not already present + newPaths.forEach((newPath) => { + const normalizedNewPath: string = path.normalize(newPath); + + if (!paths.includes(normalizedNewPath)) { + paths.push(normalizedNewPath); + } + }); + + // Join the paths with ':' to create the updated PYTHONPATH + const updatedPythonPath: string = paths.join(path.delimiter); + + return updatedPythonPath; +} diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index 4712c9b6136a..bbb65f0b2e2a 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -215,6 +215,9 @@ suite('Unit Tests - Debug Launcher', () => { if (!expected.cwd) { expected.cwd = workspaceFolders[0].uri.fsPath; } + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); + const pythonPath = `${pluginPath}${path.delimiter}${expected.cwd}`; + expected.env.PYTHONPATH = pythonPath; // added by LaunchConfigurationResolver: if (!expected.python) { @@ -342,6 +345,10 @@ suite('Unit Tests - Debug Launcher', () => { }; const expected = getDefaultDebugConfig(); expected.cwd = 'path/to/settings/cwd'; + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); + const pythonPath = `${pluginPath}${path.delimiter}${expected.cwd}`; + expected.env.PYTHONPATH = pythonPath; + setupSuccess(options, 'unittest', expected); await debugLauncher.launchDebugger(options); @@ -366,6 +373,7 @@ suite('Unit Tests - Debug Launcher', () => { console: 'integratedTerminal', cwd: 'some/dir', env: { + PYTHONPATH: 'one/two/three', SPAM: 'EGGS', }, envFile: 'some/dir/.env', diff --git a/src/test/testing/common/helpers.unit.test.ts b/src/test/testing/common/helpers.unit.test.ts new file mode 100644 index 000000000000..441b257d4d0e --- /dev/null +++ b/src/test/testing/common/helpers.unit.test.ts @@ -0,0 +1,48 @@ +import * as path from 'path'; +import * as assert from 'assert'; +import { addPathToPythonpath } from '../../../client/testing/common/helpers'; + +suite('Unit Tests - Test Helpers', () => { + const newPaths = [path.join('path', 'to', 'new')]; + test('addPathToPythonpath handles undefined path', async () => { + const launchPythonPath = undefined; + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + assert.equal(actualPath, path.join('path', 'to', 'new')); + }); + test('addPathToPythonpath adds path if it does not exist in the python path', async () => { + const launchPythonPath = path.join('random', 'existing', 'pythonpath'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = + path.join('random', 'existing', 'pythonpath') + path.delimiter + path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath does not add to python path if the given python path already contains the path', async () => { + const launchPythonPath = path.join('path', 'to', 'new'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath correctly normalizes both existing and new paths', async () => { + const newerPaths = [path.join('path', 'to', '/', 'new')]; + const launchPythonPath = path.join('path', 'to', '..', 'old'); + const actualPath = addPathToPythonpath(newerPaths, launchPythonPath); + const expectedPath = path.join('path', 'old') + path.delimiter + path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath splits pythonpath then rejoins it', async () => { + const launchPythonPath = + path.join('path', 'to', 'new') + + path.delimiter + + path.join('path', 'to', 'old') + + path.delimiter + + path.join('path', 'to', 'random'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = + path.join('path', 'to', 'new') + + path.delimiter + + path.join('path', 'to', 'old') + + path.delimiter + + path.join('path', 'to', 'random'); + assert.equal(actualPath, expectedPath); + }); +}); From ca4dfd4254ea161f3f409bd95c2af79703e6da36 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 2 Aug 2023 15:14:47 -0700 Subject: [PATCH 0119/1136] update tests only on save with more files excluded (#21741) fixes https://github.com/microsoft/vscode-python/issues/21014 and https://github.com/microsoft/vscode-python/issues/21061 --- .../testing/testController/controller.ts | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index eff333a4cdd9..1550323ff8f8 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -15,6 +15,7 @@ import { CancellationTokenSource, Uri, EventEmitter, + TextDocument, } from 'vscode'; import { IExtensionSingleActivationService } from '../../activation/types'; import { ICommandManager, IWorkspaceService } from '../../common/application/types'; @@ -48,6 +49,7 @@ import { WorkspaceTestAdapter } from './workspaceTestAdapter'; import { ITestDebugLauncher } from '../common/types'; import { IServiceContainer } from '../../ioc/types'; import { PythonResultResolver } from './common/resultResolver'; +import { onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis'; // Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER]; @@ -209,7 +211,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc if (settings.testing.autoTestDiscoverOnSaveEnabled) { traceVerbose(`Testing: Setting up watcher for ${workspace.uri.fsPath}`); this.watchForSettingsChanges(workspace); - this.watchForTestContentChanges(workspace); + this.watchForTestContentChangeOnSave(); } }); } @@ -493,12 +495,23 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.disposables.push(watcher); this.disposables.push( - watcher.onDidChange((uri) => { - traceVerbose(`Testing: Trigger refresh after change in ${uri.fsPath}`); - this.sendTriggerTelemetry('watching'); - this.refreshData.trigger(uri, false); + onDidSaveTextDocument(async (doc: TextDocument) => { + const file = doc.fileName; + // refresh on any settings file save + if ( + file.includes('settings.json') || + file.includes('pytest.ini') || + file.includes('setup.cfg') || + file.includes('pyproject.toml') + ) { + traceVerbose(`Testing: Trigger refresh after saving ${doc.uri.fsPath}`); + this.sendTriggerTelemetry('watching'); + this.refreshData.trigger(doc.uri, false); + } }), ); + /* Keep both watchers for create and delete since config files can change test behavior without content + due to their impact on pythonPath. */ this.disposables.push( watcher.onDidCreate((uri) => { traceVerbose(`Testing: Trigger refresh after creating ${uri.fsPath}`); @@ -515,31 +528,14 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); } - private watchForTestContentChanges(workspace: WorkspaceFolder): void { - const pattern = new RelativePattern(workspace, '**/*.py'); - const watcher = this.workspaceService.createFileSystemWatcher(pattern); - this.disposables.push(watcher); - - this.disposables.push( - watcher.onDidChange((uri) => { - traceVerbose(`Testing: Trigger refresh after change in ${uri.fsPath}`); - this.sendTriggerTelemetry('watching'); - // We want to invalidate tests for code change - this.refreshData.trigger(uri, true); - }), - ); - this.disposables.push( - watcher.onDidCreate((uri) => { - traceVerbose(`Testing: Trigger refresh after creating ${uri.fsPath}`); - this.sendTriggerTelemetry('watching'); - this.refreshData.trigger(uri, false); - }), - ); + private watchForTestContentChangeOnSave(): void { this.disposables.push( - watcher.onDidDelete((uri) => { - traceVerbose(`Testing: Trigger refresh after deleting in ${uri.fsPath}`); - this.sendTriggerTelemetry('watching'); - this.refreshData.trigger(uri, false); + onDidSaveTextDocument(async (doc: TextDocument) => { + if (doc.fileName.endsWith('.py')) { + traceVerbose(`Testing: Trigger refresh after saving ${doc.uri.fsPath}`); + this.sendTriggerTelemetry('watching'); + this.refreshData.trigger(doc.uri, false); + } }), ); } From dff25d4362389014a560f3dd6aaf61a3d814d2a1 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 2 Aug 2023 17:04:10 -0700 Subject: [PATCH 0120/1136] revert absolute test-ids (#21742) seeing a substantial error where test discovery is broken. Reverting this commit seems to be the temporary fix until I can diagnose the real problem. commit it is reverting: https://github.com/microsoft/vscode-python/pull/21682 --- .../pytestadapter/.data/root/tests/pytest.ini | 0 .../pytestadapter/.data/root/tests/test_a.py | 6 - .../pytestadapter/.data/root/tests/test_b.py | 6 - .../expected_discovery_test_output.py | 516 +++++------------- .../expected_execution_test_output.py | 362 +++--------- pythonFiles/tests/pytestadapter/helpers.py | 16 +- .../tests/pytestadapter/test_discovery.py | 52 +- .../tests/pytestadapter/test_execution.py | 46 +- pythonFiles/vscode_pytest/__init__.py | 58 +- 9 files changed, 250 insertions(+), 812 deletions(-) delete mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini delete mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py delete mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini b/pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py b/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py deleted file mode 100644 index 3ec3dd9626cb..000000000000 --- a/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def test_a_function(): # test_marker--test_a_function - assert True diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py b/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py deleted file mode 100644 index 0d3148641f85..000000000000 --- a/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -def test_b_function(): # test_marker--test_b_function - assert True diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 2b2c07ab8ea7..91c1453dfc77 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -1,7 +1,6 @@ import os - -from .helpers import TEST_DATA_PATH, find_test_line_number, get_absolute_test_id +from .helpers import TEST_DATA_PATH, find_test_line_number # This file contains the expected output dictionaries for tests discovery and is used in test_discovery.py. @@ -19,7 +18,7 @@ # This is the expected output for the simple_pytest.py file. # └── simple_pytest.py # └── test_function -simple_test_file_path = TEST_DATA_PATH / "simple_pytest.py" +simple_test_file_path = os.fspath(TEST_DATA_PATH / "simple_pytest.py") simple_discovery_pytest_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -27,24 +26,20 @@ "children": [ { "name": "simple_pytest.py", - "path": os.fspath(simple_test_file_path), + "path": simple_test_file_path, "type_": "file", - "id_": os.fspath(simple_test_file_path), + "id_": simple_test_file_path, "children": [ { "name": "test_function", - "path": os.fspath(simple_test_file_path), + "path": simple_test_file_path, "lineno": find_test_line_number( "test_function", simple_test_file_path, ), "type_": "test", - "id_": get_absolute_test_id( - "simple_pytest.py::test_function", simple_test_file_path - ), - "runID": get_absolute_test_id( - "simple_pytest.py::test_function", simple_test_file_path - ), + "id_": "simple_pytest.py::test_function", + "runID": "simple_pytest.py::test_function", } ], } @@ -57,7 +52,7 @@ # ├── TestExample # │ └── test_true_unittest # └── test_true_pytest -unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" +unit_pytest_same_file_path = os.fspath(TEST_DATA_PATH / "unittest_pytest_same_file.py") unit_pytest_same_file_discovery_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -65,51 +60,39 @@ "children": [ { "name": "unittest_pytest_same_file.py", - "path": os.fspath(unit_pytest_same_file_path), + "path": unit_pytest_same_file_path, "type_": "file", - "id_": os.fspath(unit_pytest_same_file_path), + "id_": unit_pytest_same_file_path, "children": [ { "name": "TestExample", - "path": os.fspath(unit_pytest_same_file_path), + "path": unit_pytest_same_file_path, "type_": "class", "children": [ { "name": "test_true_unittest", - "path": os.fspath(unit_pytest_same_file_path), + "path": unit_pytest_same_file_path, "lineno": find_test_line_number( "test_true_unittest", - os.fspath(unit_pytest_same_file_path), - ), - "type_": "test", - "id_": get_absolute_test_id( - "unittest_pytest_same_file.py::TestExample::test_true_unittest", - unit_pytest_same_file_path, - ), - "runID": get_absolute_test_id( - "unittest_pytest_same_file.py::TestExample::test_true_unittest", unit_pytest_same_file_path, ), + "type_": "test", + "id_": "unittest_pytest_same_file.py::TestExample::test_true_unittest", + "runID": "unittest_pytest_same_file.py::TestExample::test_true_unittest", } ], "id_": "unittest_pytest_same_file.py::TestExample", }, { "name": "test_true_pytest", - "path": os.fspath(unit_pytest_same_file_path), + "path": unit_pytest_same_file_path, "lineno": find_test_line_number( "test_true_pytest", unit_pytest_same_file_path, ), "type_": "test", - "id_": get_absolute_test_id( - "unittest_pytest_same_file.py::test_true_pytest", - unit_pytest_same_file_path, - ), - "runID": get_absolute_test_id( - "unittest_pytest_same_file.py::test_true_pytest", - unit_pytest_same_file_path, - ), + "id_": "unittest_pytest_same_file.py::test_true_pytest", + "runID": "unittest_pytest_same_file.py::test_true_pytest", }, ], } @@ -141,9 +124,9 @@ # └── test_subtract_positive_numbers # │ └── TestDuplicateFunction # │ └── test_dup_s -unittest_folder_path = TEST_DATA_PATH / "unittest_folder" -test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" -test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" +unittest_folder_path = os.fspath(TEST_DATA_PATH / "unittest_folder") +test_add_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_add.py") +test_subtract_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_subtract.py") unittest_folder_discovery_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -151,79 +134,61 @@ "children": [ { "name": "unittest_folder", - "path": os.fspath(unittest_folder_path), + "path": unittest_folder_path, "type_": "folder", - "id_": os.fspath(unittest_folder_path), + "id_": unittest_folder_path, "children": [ { "name": "test_add.py", - "path": os.fspath(test_add_path), + "path": test_add_path, "type_": "file", - "id_": os.fspath(test_add_path), + "id_": test_add_path, "children": [ { "name": "TestAddFunction", - "path": os.fspath(test_add_path), + "path": test_add_path, "type_": "class", "children": [ { "name": "test_add_negative_numbers", - "path": os.fspath(test_add_path), + "path": test_add_path, "lineno": find_test_line_number( "test_add_negative_numbers", - os.fspath(test_add_path), - ), - "type_": "test", - "id_": get_absolute_test_id( - "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", - test_add_path, - ), - "runID": get_absolute_test_id( - "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", test_add_path, ), + "type_": "test", + "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", }, { "name": "test_add_positive_numbers", - "path": os.fspath(test_add_path), + "path": test_add_path, "lineno": find_test_line_number( "test_add_positive_numbers", - os.fspath(test_add_path), - ), - "type_": "test", - "id_": get_absolute_test_id( - "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", - test_add_path, - ), - "runID": get_absolute_test_id( - "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", test_add_path, ), + "type_": "test", + "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", }, ], "id_": "unittest_folder/test_add.py::TestAddFunction", }, { "name": "TestDuplicateFunction", - "path": os.fspath(test_add_path), + "path": test_add_path, "type_": "class", "children": [ { "name": "test_dup_a", - "path": os.fspath(test_add_path), + "path": test_add_path, "lineno": find_test_line_number( "test_dup_a", - os.fspath(test_add_path), - ), - "type_": "test", - "id_": get_absolute_test_id( - "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", - test_add_path, - ), - "runID": get_absolute_test_id( - "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", test_add_path, ), + "type_": "test", + "id_": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + "runID": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", }, ], "id_": "unittest_folder/test_add.py::TestDuplicateFunction", @@ -232,73 +197,55 @@ }, { "name": "test_subtract.py", - "path": os.fspath(test_subtract_path), + "path": test_subtract_path, "type_": "file", - "id_": os.fspath(test_subtract_path), + "id_": test_subtract_path, "children": [ { "name": "TestSubtractFunction", - "path": os.fspath(test_subtract_path), + "path": test_subtract_path, "type_": "class", "children": [ { "name": "test_subtract_negative_numbers", - "path": os.fspath(test_subtract_path), + "path": test_subtract_path, "lineno": find_test_line_number( "test_subtract_negative_numbers", - os.fspath(test_subtract_path), - ), - "type_": "test", - "id_": get_absolute_test_id( - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", - test_subtract_path, - ), - "runID": get_absolute_test_id( - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", test_subtract_path, ), + "type_": "test", + "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", }, { "name": "test_subtract_positive_numbers", - "path": os.fspath(test_subtract_path), + "path": test_subtract_path, "lineno": find_test_line_number( "test_subtract_positive_numbers", - os.fspath(test_subtract_path), - ), - "type_": "test", - "id_": get_absolute_test_id( - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", - test_subtract_path, - ), - "runID": get_absolute_test_id( - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", test_subtract_path, ), + "type_": "test", + "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", }, ], "id_": "unittest_folder/test_subtract.py::TestSubtractFunction", }, { "name": "TestDuplicateFunction", - "path": os.fspath(test_subtract_path), + "path": test_subtract_path, "type_": "class", "children": [ { "name": "test_dup_s", - "path": os.fspath(test_subtract_path), + "path": test_subtract_path, "lineno": find_test_line_number( "test_dup_s", - os.fspath(test_subtract_path), - ), - "type_": "test", - "id_": get_absolute_test_id( - "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", - test_subtract_path, - ), - "runID": get_absolute_test_id( - "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", test_subtract_path, ), + "type_": "test", + "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + "runID": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", }, ], "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction", @@ -321,23 +268,20 @@ # └── test_bottom_folder.py # └── test_bottom_function_t # └── test_bottom_function_f -dual_level_nested_folder_path = TEST_DATA_PATH / "dual_level_nested_folder" -test_top_folder_path = ( +dual_level_nested_folder_path = os.fspath(TEST_DATA_PATH / "dual_level_nested_folder") +test_top_folder_path = os.fspath( TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" ) - -test_nested_folder_one_path = ( +test_nested_folder_one_path = os.fspath( TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" ) - -test_bottom_folder_path = ( +test_bottom_folder_path = os.fspath( TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" ) - dual_level_nested_folder_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -345,97 +289,73 @@ "children": [ { "name": "dual_level_nested_folder", - "path": os.fspath(dual_level_nested_folder_path), + "path": dual_level_nested_folder_path, "type_": "folder", - "id_": os.fspath(dual_level_nested_folder_path), + "id_": dual_level_nested_folder_path, "children": [ { "name": "test_top_folder.py", - "path": os.fspath(test_top_folder_path), + "path": test_top_folder_path, "type_": "file", - "id_": os.fspath(test_top_folder_path), + "id_": test_top_folder_path, "children": [ { "name": "test_top_function_t", - "path": os.fspath(test_top_folder_path), + "path": test_top_folder_path, "lineno": find_test_line_number( "test_top_function_t", test_top_folder_path, ), "type_": "test", - "id_": get_absolute_test_id( - "dual_level_nested_folder/test_top_folder.py::test_top_function_t", - test_top_folder_path, - ), - "runID": get_absolute_test_id( - "dual_level_nested_folder/test_top_folder.py::test_top_function_t", - test_top_folder_path, - ), + "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", }, { "name": "test_top_function_f", - "path": os.fspath(test_top_folder_path), + "path": test_top_folder_path, "lineno": find_test_line_number( "test_top_function_f", test_top_folder_path, ), "type_": "test", - "id_": get_absolute_test_id( - "dual_level_nested_folder/test_top_folder.py::test_top_function_f", - test_top_folder_path, - ), - "runID": get_absolute_test_id( - "dual_level_nested_folder/test_top_folder.py::test_top_function_f", - test_top_folder_path, - ), + "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", }, ], }, { "name": "nested_folder_one", - "path": os.fspath(test_nested_folder_one_path), + "path": test_nested_folder_one_path, "type_": "folder", - "id_": os.fspath(test_nested_folder_one_path), + "id_": test_nested_folder_one_path, "children": [ { "name": "test_bottom_folder.py", - "path": os.fspath(test_bottom_folder_path), + "path": test_bottom_folder_path, "type_": "file", - "id_": os.fspath(test_bottom_folder_path), + "id_": test_bottom_folder_path, "children": [ { "name": "test_bottom_function_t", - "path": os.fspath(test_bottom_folder_path), + "path": test_bottom_folder_path, "lineno": find_test_line_number( "test_bottom_function_t", test_bottom_folder_path, ), "type_": "test", - "id_": get_absolute_test_id( - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - test_bottom_folder_path, - ), - "runID": get_absolute_test_id( - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - test_bottom_folder_path, - ), + "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", }, { "name": "test_bottom_function_f", - "path": os.fspath(test_bottom_folder_path), + "path": test_bottom_folder_path, "lineno": find_test_line_number( "test_bottom_function_f", test_bottom_folder_path, ), "type_": "test", - "id_": get_absolute_test_id( - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - test_bottom_folder_path, - ), - "runID": get_absolute_test_id( - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - test_bottom_folder_path, - ), + "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", }, ], } @@ -454,10 +374,12 @@ # └── test_nest.py # └── test_function -folder_a_path = TEST_DATA_PATH / "folder_a" -folder_b_path = TEST_DATA_PATH / "folder_a" / "folder_b" -folder_a_nested_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" -test_nest_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +folder_a_path = os.fspath(TEST_DATA_PATH / "folder_a") +folder_b_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b") +folder_a_nested_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a") +test_nest_path = os.fspath( + TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +) double_nested_folder_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -465,44 +387,38 @@ "children": [ { "name": "folder_a", - "path": os.fspath(folder_a_path), + "path": folder_a_path, "type_": "folder", - "id_": os.fspath(folder_a_path), + "id_": folder_a_path, "children": [ { "name": "folder_b", - "path": os.fspath(folder_b_path), + "path": folder_b_path, "type_": "folder", - "id_": os.fspath(folder_b_path), + "id_": folder_b_path, "children": [ { "name": "folder_a", - "path": os.fspath(folder_a_nested_path), + "path": folder_a_nested_path, "type_": "folder", - "id_": os.fspath(folder_a_nested_path), + "id_": folder_a_nested_path, "children": [ { "name": "test_nest.py", - "path": os.fspath(test_nest_path), + "path": test_nest_path, "type_": "file", - "id_": os.fspath(test_nest_path), + "id_": test_nest_path, "children": [ { "name": "test_function", - "path": os.fspath(test_nest_path), + "path": test_nest_path, "lineno": find_test_line_number( "test_function", test_nest_path, ), "type_": "test", - "id_": get_absolute_test_id( - "folder_a/folder_b/folder_a/test_nest.py::test_function", - test_nest_path, - ), - "runID": get_absolute_test_id( - "folder_a/folder_b/folder_a/test_nest.py::test_function", - test_nest_path, - ), + "id_": "folder_a/folder_b/folder_a/test_nest.py::test_function", + "runID": "folder_a/folder_b/folder_a/test_nest.py::test_function", } ], } @@ -522,7 +438,7 @@ # └── [3+5-8] # └── [2+4-6] # └── [6+9-16] -parameterize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" +parameterize_tests_path = os.fspath(TEST_DATA_PATH / "parametrize_tests.py") parametrize_tests_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -530,107 +446,77 @@ "children": [ { "name": "parametrize_tests.py", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "type_": "file", - "id_": os.fspath(parameterize_tests_path), + "id_": parameterize_tests_path, "children": [ { "name": "test_adding", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "type_": "function", "id_": "parametrize_tests.py::test_adding", "children": [ { "name": "[3+5-8]", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "lineno": find_test_line_number( "test_adding[3+5-8]", parameterize_tests_path, ), "type_": "test", - "id_": get_absolute_test_id( - "parametrize_tests.py::test_adding[3+5-8]", - parameterize_tests_path, - ), - "runID": get_absolute_test_id( - "parametrize_tests.py::test_adding[3+5-8]", - parameterize_tests_path, - ), + "id_": "parametrize_tests.py::test_adding[3+5-8]", + "runID": "parametrize_tests.py::test_adding[3+5-8]", }, { "name": "[2+4-6]", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "lineno": find_test_line_number( "test_adding[2+4-6]", parameterize_tests_path, ), "type_": "test", - "id_": get_absolute_test_id( - "parametrize_tests.py::test_adding[2+4-6]", - parameterize_tests_path, - ), - "runID": get_absolute_test_id( - "parametrize_tests.py::test_adding[2+4-6]", - parameterize_tests_path, - ), + "id_": "parametrize_tests.py::test_adding[2+4-6]", + "runID": "parametrize_tests.py::test_adding[2+4-6]", }, { "name": "[6+9-16]", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "lineno": find_test_line_number( "test_adding[6+9-16]", parameterize_tests_path, ), "type_": "test", - "id_": get_absolute_test_id( - "parametrize_tests.py::test_adding[6+9-16]", - parameterize_tests_path, - ), - "runID": get_absolute_test_id( - "parametrize_tests.py::test_adding[6+9-16]", - parameterize_tests_path, - ), + "id_": "parametrize_tests.py::test_adding[6+9-16]", + "runID": "parametrize_tests.py::test_adding[6+9-16]", }, ], }, { "name": "test_under_ten", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "type_": "function", "children": [ { "name": "[1]", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "lineno": find_test_line_number( "test_under_ten[1]", parameterize_tests_path, ), "type_": "test", - "id_": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[1]", - parameterize_tests_path, - ), - "runID": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[1]", - parameterize_tests_path, - ), + "id_": "parametrize_tests.py::test_under_ten[1]", + "runID": "parametrize_tests.py::test_under_ten[1]", }, { "name": "[2]", - "path": os.fspath(parameterize_tests_path), + "path": parameterize_tests_path, "lineno": find_test_line_number( "test_under_ten[2]", parameterize_tests_path, ), "type_": "test", - "id_": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[2]", - parameterize_tests_path, - ), - "runID": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[2]", - parameterize_tests_path, - ), + "id_": "parametrize_tests.py::test_under_ten[2]", + "runID": "parametrize_tests.py::test_under_ten[2]", }, ], "id_": "parametrize_tests.py::test_under_ten", @@ -643,7 +529,7 @@ # This is the expected output for the text_docstring.txt tests. # └── text_docstring.txt -text_docstring_path = TEST_DATA_PATH / "text_docstring.txt" +text_docstring_path = os.fspath(TEST_DATA_PATH / "text_docstring.txt") doctest_pytest_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -651,24 +537,20 @@ "children": [ { "name": "text_docstring.txt", - "path": os.fspath(text_docstring_path), + "path": text_docstring_path, "type_": "file", - "id_": os.fspath(text_docstring_path), + "id_": text_docstring_path, "children": [ { "name": "text_docstring.txt", - "path": os.fspath(text_docstring_path), + "path": text_docstring_path, "lineno": find_test_line_number( "text_docstring.txt", - os.fspath(text_docstring_path), + text_docstring_path, ), "type_": "test", - "id_": get_absolute_test_id( - "text_docstring.txt::text_docstring.txt", text_docstring_path - ), - "runID": get_absolute_test_id( - "text_docstring.txt::text_docstring.txt", text_docstring_path - ), + "id_": "text_docstring.txt::text_docstring.txt", + "runID": "text_docstring.txt::text_docstring.txt", } ], } @@ -688,8 +570,8 @@ # └── [1] # └── [2] # └── [3] -param1_path = TEST_DATA_PATH / "param_same_name" / "test_param1.py" -param2_path = TEST_DATA_PATH / "param_same_name" / "test_param2.py" +param1_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param1.py") +param2_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param2.py") param_same_name_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -703,56 +585,38 @@ "children": [ { "name": "test_param1.py", - "path": os.fspath(param1_path), + "path": param1_path, "type_": "file", - "id_": os.fspath(param1_path), + "id_": param1_path, "children": [ { "name": "test_odd_even", - "path": os.fspath(param1_path), + "path": param1_path, "type_": "function", "children": [ { "name": "[a]", - "path": os.fspath(param1_path), + "path": param1_path, "lineno": "6", "type_": "test", - "id_": get_absolute_test_id( - "param_same_name/test_param1.py::test_odd_even[a]", - param1_path, - ), - "runID": get_absolute_test_id( - "param_same_name/test_param1.py::test_odd_even[a]", - param1_path, - ), + "id_": "param_same_name/test_param1.py::test_odd_even[a]", + "runID": "param_same_name/test_param1.py::test_odd_even[a]", }, { "name": "[b]", - "path": os.fspath(param1_path), + "path": param1_path, "lineno": "6", "type_": "test", - "id_": get_absolute_test_id( - "param_same_name/test_param1.py::test_odd_even[b]", - param1_path, - ), - "runID": get_absolute_test_id( - "param_same_name/test_param1.py::test_odd_even[b]", - param1_path, - ), + "id_": "param_same_name/test_param1.py::test_odd_even[b]", + "runID": "param_same_name/test_param1.py::test_odd_even[b]", }, { "name": "[c]", - "path": os.fspath(param1_path), + "path": param1_path, "lineno": "6", "type_": "test", - "id_": get_absolute_test_id( - "param_same_name/test_param1.py::test_odd_even[c]", - param1_path, - ), - "runID": get_absolute_test_id( - "param_same_name/test_param1.py::test_odd_even[c]", - param1_path, - ), + "id_": "param_same_name/test_param1.py::test_odd_even[c]", + "runID": "param_same_name/test_param1.py::test_odd_even[c]", }, ], "id_": "param_same_name/test_param1.py::test_odd_even", @@ -761,56 +625,38 @@ }, { "name": "test_param2.py", - "path": os.fspath(param2_path), + "path": param2_path, "type_": "file", - "id_": os.fspath(param2_path), + "id_": param2_path, "children": [ { "name": "test_odd_even", - "path": os.fspath(param2_path), + "path": param2_path, "type_": "function", "children": [ { "name": "[1]", - "path": os.fspath(param2_path), + "path": param2_path, "lineno": "6", "type_": "test", - "id_": get_absolute_test_id( - "param_same_name/test_param2.py::test_odd_even[1]", - param2_path, - ), - "runID": get_absolute_test_id( - "param_same_name/test_param2.py::test_odd_even[1]", - param2_path, - ), + "id_": "param_same_name/test_param2.py::test_odd_even[1]", + "runID": "param_same_name/test_param2.py::test_odd_even[1]", }, { "name": "[2]", - "path": os.fspath(param2_path), + "path": param2_path, "lineno": "6", "type_": "test", - "id_": get_absolute_test_id( - "param_same_name/test_param2.py::test_odd_even[2]", - param2_path, - ), - "runID": get_absolute_test_id( - "param_same_name/test_param2.py::test_odd_even[2]", - param2_path, - ), + "id_": "param_same_name/test_param2.py::test_odd_even[2]", + "runID": "param_same_name/test_param2.py::test_odd_even[2]", }, { "name": "[3]", - "path": os.fspath(param2_path), + "path": param2_path, "lineno": "6", "type_": "test", - "id_": get_absolute_test_id( - "param_same_name/test_param2.py::test_odd_even[3]", - param2_path, - ), - "runID": get_absolute_test_id( - "param_same_name/test_param2.py::test_odd_even[3]", - param2_path, - ), + "id_": "param_same_name/test_param2.py::test_odd_even[3]", + "runID": "param_same_name/test_param2.py::test_odd_even[3]", }, ], "id_": "param_same_name/test_param2.py::test_odd_even", @@ -822,67 +668,3 @@ ], "id_": TEST_DATA_PATH_STR, } - -tests_path = TEST_DATA_PATH / "root" / "tests" -tests_a_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" -tests_b_path = TEST_DATA_PATH / "root" / "tests" / "test_b.py" -# This is the expected output for the root folder tests. -# └── tests -# └── test_a.py -# └── test_a_function -# └── test_b.py -# └── test_b_function -root_with_config_expected_output = { - "name": "tests", - "path": os.fspath(tests_path), - "type_": "folder", - "children": [ - { - "name": "test_a.py", - "path": os.fspath(tests_a_path), - "type_": "file", - "id_": os.fspath(tests_a_path), - "children": [ - { - "name": "test_a_function", - "path": os.fspath(os.path.join(tests_path, "test_a.py")), - "lineno": find_test_line_number( - "test_a_function", - os.path.join(tests_path, "test_a.py"), - ), - "type_": "test", - "id_": get_absolute_test_id( - "tests/test_a.py::test_a_function", tests_a_path - ), - "runID": get_absolute_test_id( - "tests/test_a.py::test_a_function", tests_a_path - ), - } - ], - }, - { - "name": "test_b.py", - "path": os.fspath(tests_b_path), - "type_": "file", - "id_": os.fspath(tests_b_path), - "children": [ - { - "name": "test_b_function", - "path": os.fspath(os.path.join(tests_path, "test_b.py")), - "lineno": find_test_line_number( - "test_b_function", - os.path.join(tests_path, "test_b.py"), - ), - "type_": "test", - "id_": get_absolute_test_id( - "tests/test_b.py::test_b_function", tests_b_path - ), - "runID": get_absolute_test_id( - "tests/test_b.py::test_b_function", tests_b_path - ), - } - ], - }, - ], - "id_": os.fspath(tests_path), -} diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index 0a7e737dfc0e..fe1d40a55b43 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -6,7 +6,6 @@ SUCCESS = "success" FAILURE = "failure" TEST_SUBTRACT_FUNCTION_NEGATIVE_NUMBERS_ERROR = "self = \n\n def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers\n self,\n ):\n result = subtract(-2, -3)\n> self.assertEqual(result, 100000)\nE AssertionError: 1 != 100000\n\nunittest_folder/test_subtract.py:25: AssertionError" -from .helpers import TEST_DATA_PATH, get_absolute_test_id # This is the expected output for the unittest_folder execute tests # └── unittest_folder @@ -18,52 +17,30 @@ # └── TestSubtractFunction # ├── test_subtract_negative_numbers: failure # └── test_subtract_positive_numbers: success -test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" -test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" uf_execution_expected_output = { - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path - ): { - "test": get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path - ), + f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ): { - "test": get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ), + f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", - test_subtract_path, - ): { - "test": get_absolute_test_id( - f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", - test_subtract_path, - ), + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers": { + "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", "outcome": FAILURE, "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - get_absolute_test_id( - f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", - test_subtract_path, - ): { - "test": get_absolute_test_id( - f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", - test_subtract_path, - ), + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers": { + "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", "outcome": SUCCESS, "message": None, "traceback": None, @@ -78,26 +55,16 @@ # │ └── TestAddFunction # │ ├── test_add_negative_numbers: success # │ └── test_add_positive_numbers: success -test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" - uf_single_file_expected_output = { - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path - ): { - "test": get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path - ), + f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ): { - "test": get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ), + f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", "outcome": SUCCESS, "message": None, "traceback": None, @@ -105,24 +72,19 @@ }, } - # This is the expected output for the unittest_folder execute only signle method # └── unittest_folder # ├── test_add.py # │ └── TestAddFunction # │ └── test_add_positive_numbers: success uf_single_method_execution_expected_output = { - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ): { - "test": get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ), + f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, - }, + } } # This is the expected output for the unittest_folder tests run where two tests @@ -134,28 +96,18 @@ # └── test_subtract.py # └── TestSubtractFunction # └── test_subtract_positive_numbers: success -test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" -test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" - uf_non_adjacent_tests_execution_expected_output = { - get_absolute_test_id( - f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", test_subtract_path - ): { - "test": get_absolute_test_id( - f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", - test_subtract_path, - ), + TEST_SUBTRACT_FUNCTION + + "test_subtract_positive_numbers": { + "test": TEST_SUBTRACT_FUNCTION + "test_subtract_positive_numbers", "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ): { - "test": get_absolute_test_id( - f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path - ), + TEST_ADD_FUNCTION + + "test_add_positive_numbers": { + "test": TEST_ADD_FUNCTION + "test_add_positive_numbers", "outcome": SUCCESS, "message": None, "traceback": None, @@ -163,15 +115,12 @@ }, } - # This is the expected output for the simple_pytest.py file. # └── simple_pytest.py # └── test_function: success -simple_pytest_path = TEST_DATA_PATH / "unittest_folder" / "simple_pytest.py" - simple_execution_pytest_expected_output = { - get_absolute_test_id("test_function", simple_pytest_path): { - "test": get_absolute_test_id("test_function", simple_pytest_path), + "simple_pytest.py::test_function": { + "test": "simple_pytest.py::test_function", "outcome": "success", "message": None, "traceback": None, @@ -179,34 +128,21 @@ } } - # This is the expected output for the unittest_pytest_same_file.py file. # ├── unittest_pytest_same_file.py # ├── TestExample # │ └── test_true_unittest: success # └── test_true_pytest: success -unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" unit_pytest_same_file_execution_expected_output = { - get_absolute_test_id( - "unittest_pytest_same_file.py::TestExample::test_true_unittest", - unit_pytest_same_file_path, - ): { - "test": get_absolute_test_id( - "unittest_pytest_same_file.py::TestExample::test_true_unittest", - unit_pytest_same_file_path, - ), + "unittest_pytest_same_file.py::TestExample::test_true_unittest": { + "test": "unittest_pytest_same_file.py::TestExample::test_true_unittest", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "unittest_pytest_same_file.py::test_true_pytest", unit_pytest_same_file_path - ): { - "test": get_absolute_test_id( - "unittest_pytest_same_file.py::test_true_pytest", - unit_pytest_same_file_path, - ), + "unittest_pytest_same_file.py::test_true_pytest": { + "test": "unittest_pytest_same_file.py::test_true_pytest", "outcome": "success", "message": None, "traceback": None, @@ -218,15 +154,9 @@ # └── error_raise_exception.py # ├── TestSomething # │ └── test_a: failure -error_raised_exception_path = TEST_DATA_PATH / "error_raise_exception.py" error_raised_exception_execution_expected_output = { - get_absolute_test_id( - "error_raise_exception.py::TestSomething::test_a", error_raised_exception_path - ): { - "test": get_absolute_test_id( - "error_raise_exception.py::TestSomething::test_a", - error_raised_exception_path, - ), + "error_raise_exception.py::TestSomething::test_a": { + "test": "error_raise_exception.py::TestSomething::test_a", "outcome": "error", "message": "ERROR MESSAGE", "traceback": "TRACEBACK", @@ -242,60 +172,44 @@ # ├── TestClass # │ └── test_class_function_a: skipped # │ └── test_class_function_b: skipped - -skip_tests_path = TEST_DATA_PATH / "skip_tests.py" skip_tests_execution_expected_output = { - get_absolute_test_id("skip_tests.py::test_something", skip_tests_path): { - "test": get_absolute_test_id("skip_tests.py::test_something", skip_tests_path), + "skip_tests.py::test_something": { + "test": "skip_tests.py::test_something", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id("skip_tests.py::test_another_thing", skip_tests_path): { - "test": get_absolute_test_id( - "skip_tests.py::test_another_thing", skip_tests_path - ), + "skip_tests.py::test_another_thing": { + "test": "skip_tests.py::test_another_thing", "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id("skip_tests.py::test_decorator_thing", skip_tests_path): { - "test": get_absolute_test_id( - "skip_tests.py::test_decorator_thing", skip_tests_path - ), + "skip_tests.py::test_decorator_thing": { + "test": "skip_tests.py::test_decorator_thing", "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id("skip_tests.py::test_decorator_thing_2", skip_tests_path): { - "test": get_absolute_test_id( - "skip_tests.py::test_decorator_thing_2", skip_tests_path - ), + "skip_tests.py::test_decorator_thing_2": { + "test": "skip_tests.py::test_decorator_thing_2", "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "skip_tests.py::TestClass::test_class_function_a", skip_tests_path - ): { - "test": get_absolute_test_id( - "skip_tests.py::TestClass::test_class_function_a", skip_tests_path - ), + "skip_tests.py::TestClass::test_class_function_a": { + "test": "skip_tests.py::TestClass::test_class_function_a", "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "skip_tests.py::TestClass::test_class_function_b", skip_tests_path - ): { - "test": get_absolute_test_id( - "skip_tests.py::TestClass::test_class_function_b", skip_tests_path - ), + "skip_tests.py::TestClass::test_class_function_b": { + "test": "skip_tests.py::TestClass::test_class_function_b", "outcome": "skipped", "message": None, "traceback": None, @@ -313,59 +227,30 @@ # └── test_bottom_folder.py # └── test_bottom_function_t: success # └── test_bottom_function_f: failure -dual_level_nested_folder_top_path = ( - TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" -) -dual_level_nested_folder_bottom_path = ( - TEST_DATA_PATH - / "dual_level_nested_folder" - / "nested_folder_one" - / "test_bottom_folder.py" -) dual_level_nested_folder_execution_expected_output = { - get_absolute_test_id( - "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path - ): { - "test": get_absolute_test_id( - "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path - ), + "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path - ): { - "test": get_absolute_test_id( - "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path - ), + "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - get_absolute_test_id( - "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - dual_level_nested_folder_bottom_path, - ): { - "test": get_absolute_test_id( - "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - dual_level_nested_folder_bottom_path, - ), + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - dual_level_nested_folder_bottom_path, - ): { - "test": get_absolute_test_id( - "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - dual_level_nested_folder_bottom_path, - ), + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, @@ -379,59 +264,38 @@ # └── folder_a # └── test_nest.py # └── test_function: success - -nested_folder_path = ( - TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" -) double_nested_folder_expected_execution_output = { - get_absolute_test_id( - "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path - ): { - "test": get_absolute_test_id( - "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path - ), + "folder_a/folder_b/folder_a/test_nest.py::test_function": { + "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", "outcome": "success", "message": None, "traceback": None, "subtest": None, } } + # This is the expected output for the nested_folder tests. # └── parametrize_tests.py # └── test_adding[3+5-8]: success # └── test_adding[2+4-6]: success # └── test_adding[6+9-16]: failure -parametrize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" - parametrize_tests_expected_execution_output = { - get_absolute_test_id( - "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path - ): { - "test": get_absolute_test_id( - "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path - ), + "parametrize_tests.py::test_adding[3+5-8]": { + "test": "parametrize_tests.py::test_adding[3+5-8]", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path - ): { - "test": get_absolute_test_id( - "parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path - ), + "parametrize_tests.py::test_adding[2+4-6]": { + "test": "parametrize_tests.py::test_adding[2+4-6]", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path - ): { - "test": get_absolute_test_id( - "parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path - ), + "parametrize_tests.py::test_adding[6+9-16]": { + "test": "parametrize_tests.py::test_adding[6+9-16]", "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, @@ -443,12 +307,8 @@ # └── parametrize_tests.py # └── test_adding[3+5-8]: success single_parametrize_tests_expected_execution_output = { - get_absolute_test_id( - "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path - ): { - "test": get_absolute_test_id( - "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path - ), + "parametrize_tests.py::test_adding[3+5-8]": { + "test": "parametrize_tests.py::test_adding[3+5-8]", "outcome": "success", "message": None, "traceback": None, @@ -459,12 +319,9 @@ # This is the expected output for the single parameterized tests. # └── text_docstring.txt # └── text_docstring: success -doc_test_path = TEST_DATA_PATH / "text_docstring.txt" doctest_pytest_expected_execution_output = { - get_absolute_test_id("text_docstring.txt::text_docstring.txt", doc_test_path): { - "test": get_absolute_test_id( - "text_docstring.txt::text_docstring.txt", doc_test_path - ), + "text_docstring.txt::text_docstring.txt": { + "test": "text_docstring.txt::text_docstring.txt", "outcome": "success", "message": None, "traceback": None, @@ -473,127 +330,68 @@ } # Will run all tests in the cwd that fit the test file naming pattern. -folder_a_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" -dual_level_nested_folder_top_path = ( - TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" -) -dual_level_nested_folder_bottom_path = ( - TEST_DATA_PATH - / "dual_level_nested_folder" - / "nested_folder_one" - / "test_bottom_folder.py" -) -unittest_folder_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" -unittest_folder_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" - no_test_ids_pytest_execution_expected_output = { - get_absolute_test_id("test_function", folder_a_path): { - "test": get_absolute_test_id("test_function", folder_a_path), + "folder_a/folder_b/folder_a/test_nest.py::test_function": { + "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id("test_top_function_t", dual_level_nested_folder_top_path): { - "test": get_absolute_test_id( - "test_top_function_t", dual_level_nested_folder_top_path - ), + "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id("test_top_function_f", dual_level_nested_folder_top_path): { - "test": get_absolute_test_id( - "test_top_function_f", dual_level_nested_folder_top_path - ), + "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - get_absolute_test_id( - "test_bottom_function_t", dual_level_nested_folder_bottom_path - ): { - "test": get_absolute_test_id( - "test_bottom_function_t", dual_level_nested_folder_bottom_path - ), + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "test_bottom_function_f", dual_level_nested_folder_bottom_path - ): { - "test": get_absolute_test_id( - "test_bottom_function_f", dual_level_nested_folder_bottom_path - ), + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - get_absolute_test_id( - "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path - ): { - "test": get_absolute_test_id( - "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path - ), + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers": { + "test": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path - ): { - "test": get_absolute_test_id( - "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path - ), + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers": { + "test": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - get_absolute_test_id( - "TestSubtractFunction::test_subtract_negative_numbers", - unittest_folder_subtract_path, - ): { - "test": get_absolute_test_id( - "TestSubtractFunction::test_subtract_negative_numbers", - unittest_folder_subtract_path, - ), + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers": { + "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - get_absolute_test_id( - "TestSubtractFunction::test_subtract_positive_numbers", - unittest_folder_subtract_path, - ): { - "test": get_absolute_test_id( - "TestSubtractFunction::test_subtract_positive_numbers", - unittest_folder_subtract_path, - ), + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers": { + "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", "outcome": "success", "message": None, "traceback": None, "subtest": None, }, } - -# This is the expected output for the root folder with the config file referenced. -# └── test_a.py -# └── test_a_function: success -test_add_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" -config_file_pytest_expected_execution_output = { - get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path): { - "test": get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path), - "outcome": "success", - "message": None, - "traceback": None, - "subtest": None, - } -} diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index 28feb6282b92..c3e01d52170a 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -16,13 +16,6 @@ from typing_extensions import TypedDict -def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: - split_id = test_id.split("::")[1:] - absolute_test_id = "::".join([str(testPath), *split_id]) - print("absolute path", absolute_test_id) - return absolute_test_id - - def create_server( host: str = "127.0.0.1", port: int = 0, @@ -111,13 +104,6 @@ def process_rpc_json(data: str) -> List[Dict[str, Any]]: def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: - """Run the pytest discovery and return the JSON data from the server.""" - return runner_with_cwd(args, TEST_DATA_PATH) - - -def runner_with_cwd( - args: List[str], path: pathlib.Path -) -> Optional[List[Dict[str, Any]]]: """Run the pytest discovery and return the JSON data from the server.""" process_args: List[str] = [ sys.executable, @@ -148,7 +134,7 @@ def runner_with_cwd( t2 = threading.Thread( target=_run_test_code, - args=(process_args, env, path, completed), + args=(process_args, env, TEST_DATA_PATH, completed), ) t2.start() diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 8d785be27c8b..5288c7ad769e 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -7,7 +7,7 @@ import pytest from . import expected_discovery_test_output -from .helpers import TEST_DATA_PATH, runner, runner_with_cwd +from .helpers import TEST_DATA_PATH, runner def test_import_error(tmp_path): @@ -153,53 +153,3 @@ def test_pytest_collect(file, expected_const): assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) assert actual["tests"] == expected_const - - -def test_pytest_root_dir(): - """ - Test to test pytest discovery with the command line arg --rootdir specified to be a subfolder - of the workspace root. Discovery should succeed and testids should be relative to workspace root. - """ - rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" - actual = runner_with_cwd( - [ - "--collect-only", - rd, - ], - TEST_DATA_PATH / "root", - ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") - assert ( - actual["tests"] - == expected_discovery_test_output.root_with_config_expected_output - ) - - -def test_pytest_config_file(): - """ - Test to test pytest discovery with the command line arg -c with a specified config file which - changes the workspace root. Discovery should succeed and testids should be relative to workspace root. - """ - actual = runner_with_cwd( - [ - "--collect-only", - "-c", - "tests/pytest.ini", - ], - TEST_DATA_PATH / "root", - ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") - assert ( - actual["tests"] - == expected_discovery_test_output.root_with_config_expected_output - ) diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index 2be4886c24c1..ffc84955bf54 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -1,56 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import json import os import shutil import pytest from tests.pytestadapter import expected_execution_test_output -from .helpers import TEST_DATA_PATH, runner, runner_with_cwd - - -def test_config_file(): - """Test pytest execution when a config file is specified.""" - args = [ - "-c", - "tests/pytest.ini", - str(TEST_DATA_PATH / "root" / "tests" / "test_a.py::test_a_function"), - ] - new_cwd = TEST_DATA_PATH / "root" - actual = runner_with_cwd(args, new_cwd) - expected_const = ( - expected_execution_test_output.config_file_pytest_expected_execution_output - ) - assert actual - assert len(actual) == len(expected_const) - actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(new_cwd) - actual_result_dict.update(a["result"]) - assert actual_result_dict == expected_const - - -def test_rootdir_specified(): - """Test pytest execution when a --rootdir is specified.""" - rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" - args = [rd, "tests/test_a.py::test_a_function"] - new_cwd = TEST_DATA_PATH / "root" - actual = runner_with_cwd(args, new_cwd) - expected_const = ( - expected_execution_test_output.config_file_pytest_expected_execution_output - ) - assert actual - assert len(actual) == len(expected_const) - actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(new_cwd) - actual_result_dict.update(a["result"]) - assert actual_result_dict == expected_const +from .helpers import TEST_DATA_PATH, runner def test_syntax_error_execution(tmp_path): diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 49d429662e3a..1ac287a8410a 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -69,7 +69,8 @@ def pytest_exception_interact(node, call, report): """ # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. # call.excinfo.exconly() returns the exception as a string. - # If it is during discovery, then add the error to error logs. + # See if it is during discovery or execution. + # if discovery, then add the error to error logs. if type(report) == pytest.CollectReport: if call.excinfo and call.excinfo.typename != "AssertionError": if report.outcome == "skipped" and "SkipTest" in str(call): @@ -82,11 +83,11 @@ def pytest_exception_interact(node, call, report): report.longreprtext + "\n Check Python Test Logs for more details." ) else: - # If during execution, send this data that the given node failed. + # if execution, send this data that the given node failed. report_value = "error" if call.excinfo.typename == "AssertionError": report_value = "failure" - node_id = get_absolute_test_id(node.nodeid, get_node_path(node)) + node_id = str(node.nodeid) if node_id not in collected_tests_so_far: collected_tests_so_far.append(node_id) item_result = create_test_outcome( @@ -105,22 +106,6 @@ def pytest_exception_interact(node, call, report): ) -def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: - """A function that returns the absolute test id. This is necessary because testIds are relative to the rootdir. - This does not work for our case since testIds when referenced during run time are relative to the instantiation - location. Absolute paths for testIds are necessary for the test tree ensures configurations that change the rootdir - of pytest are handled correctly. - - Keyword arguments: - test_id -- the pytest id of the test which is relative to the rootdir. - testPath -- the path to the file the test is located in, as a pathlib.Path object. - """ - split_id = test_id.split("::")[1:] - absolute_test_id = "::".join([str(testPath), *split_id]) - print("absolute path", absolute_test_id) - return absolute_test_id - - def pytest_keyboard_interrupt(excinfo): """A pytest hook that is called when a keyboard interrupt is raised. @@ -145,7 +130,7 @@ class TestOutcome(Dict): def create_test_outcome( - testid: str, + test: str, outcome: str, message: Union[str, None], traceback: Union[str, None], @@ -153,7 +138,7 @@ def create_test_outcome( ) -> TestOutcome: """A function that creates a TestOutcome object.""" return TestOutcome( - test=testid, + test=test, outcome=outcome, message=message, traceback=traceback, # TODO: traceback @@ -169,7 +154,6 @@ class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): IS_DISCOVERY = False -map_id_to_path = dict() def pytest_load_initial_conftests(early_config, parser, args): @@ -200,21 +184,17 @@ def pytest_report_teststatus(report, config): elif report.failed: report_value = "failure" message = report.longreprtext - node_path = map_id_to_path[report.nodeid] - if not node_path: - node_path = cwd - # Calculate the absolute test id and use this as the ID moving forward. - absolute_node_id = get_absolute_test_id(report.nodeid, node_path) - if absolute_node_id not in collected_tests_so_far: - collected_tests_so_far.append(absolute_node_id) + node_id = str(report.nodeid) + if node_id not in collected_tests_so_far: + collected_tests_so_far.append(node_id) item_result = create_test_outcome( - absolute_node_id, + node_id, report_value, message, traceback, ) collected_test = testRunResultDict() - collected_test[absolute_node_id] = item_result + collected_test[node_id] = item_result execution_post( os.fsdecode(cwd), "success", @@ -231,22 +211,21 @@ def pytest_report_teststatus(report, config): def pytest_runtest_protocol(item, nextitem): - map_id_to_path[item.nodeid] = get_node_path(item) skipped = check_skipped_wrapper(item) if skipped: - absolute_node_id = get_absolute_test_id(item.nodeid, get_node_path(item)) + node_id = str(item.nodeid) report_value = "skipped" cwd = pathlib.Path.cwd() - if absolute_node_id not in collected_tests_so_far: - collected_tests_so_far.append(absolute_node_id) + if node_id not in collected_tests_so_far: + collected_tests_so_far.append(node_id) item_result = create_test_outcome( - absolute_node_id, + node_id, report_value, None, None, ) collected_test = testRunResultDict() - collected_test[absolute_node_id] = item_result + collected_test[node_id] = item_result execution_post( os.fsdecode(cwd), "success", @@ -492,14 +471,13 @@ def create_test_node( test_case_loc: str = ( str(test_case.location[1] + 1) if (test_case.location[1] is not None) else "" ) - absolute_test_id = get_absolute_test_id(test_case.nodeid, get_node_path(test_case)) return { "name": test_case.name, "path": get_node_path(test_case), "lineno": test_case_loc, "type_": "test", - "id_": absolute_test_id, - "runID": absolute_test_id, + "id_": test_case.nodeid, + "runID": test_case.nodeid, } From f454515463845a6c230d76164d9c9a06a44f0d98 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 2 Aug 2023 17:14:01 -0700 Subject: [PATCH 0121/1136] Update release plan to document what to do with `main` during endgame week (#21743) --- .github/release_plan.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/release_plan.md b/.github/release_plan.md index e02d7ee45abf..b4ceef69abe4 100644 --- a/.github/release_plan.md +++ b/.github/release_plan.md @@ -1,6 +1,6 @@ All dates should align with VS Code's [iteration](https://github.com/microsoft/vscode/labels/iteration-plan) and [endgame](https://github.com/microsoft/vscode/labels/endgame-plan) plans. -Feature freeze is Monday @ 17:00 America/Vancouver, XXX XX. +Feature freeze is Monday @ 17:00 America/Vancouver, XXX XX. At that point, commits to `main` should only be in response to bugs found during endgame testing until the release candidate is ready. NOTE: the number of this release is in the issue title and can be substituted in wherever you see [YYYY.minor]. @@ -51,6 +51,7 @@ NOTE: this PR should make all CI relating to `main` be passing again (such as th - [ ] Manually add/fix any 3rd-party licenses as appropriate based on what the internal build pipeline detects. - [ ] Open appropriate [documentation issues](https://github.com/microsoft/vscode-docs/issues?q=is%3Aissue+is%3Aopen+label%3Apython). - [ ] Contact the PM team to begin drafting a blog post. +- [ ] Announce to the development team that `main` is open again. # Release (Wednesday, XXX XX) From 40bb62ae6af4ddfdf94931f3e11738b262e16684 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 3 Aug 2023 13:21:53 -0700 Subject: [PATCH 0122/1136] fix spelling for get-pip.py (#21752) fix spelling from get_pip to get-pip as advised. --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ec46f481e79b..046d01588573 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,7 @@ cucumber-report.json port.txt precommit.hook pythonFiles/lib/** -pythonFiles/get_pip.py +pythonFiles/get-pip.py debug_coverage*/** languageServer/** languageServer.*/** From 23353bbd1bbe49a2a95617f69933ee99bf7d04f5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 4 Aug 2023 08:52:05 -0700 Subject: [PATCH 0123/1136] Improvements to `pythonTerminalEnvVarActivation` experiment (#21751) --- src/client/interpreter/activation/service.ts | 51 ++++++++-- .../terminalEnvVarCollectionService.ts | 58 ++++++----- src/client/interpreter/activation/types.ts | 2 + ...rminalEnvVarCollectionService.unit.test.ts | 97 ++++++++----------- 4 files changed, 118 insertions(+), 90 deletions(-) diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index e5da57227b19..4364cc825f78 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -19,8 +19,8 @@ import { ICurrentProcess, IDisposable, Resource } from '../../common/types'; import { sleep } from '../../common/utils/async'; import { InMemoryCache } from '../../common/utils/cacheUtils'; import { OSType } from '../../common/utils/platform'; -import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { EnvironmentType, PythonEnvironment, virtualEnvTypes } from '../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { IInterpreterService } from '../contracts'; @@ -38,6 +38,7 @@ import { Conda } from '../../pythonEnvironments/common/environmentManagers/conda import { StopWatch } from '../../common/utils/stopWatch'; import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; +import { cache } from '../../common/utils/decorators'; const ENVIRONMENT_PREFIX = 'e8b39361-0157-4923-80e1-22d70d46dee6'; const CACHE_DURATION = 10 * 60 * 1000; @@ -154,11 +155,11 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } // Cache only if successful, else keep trying & failing if necessary. - const cache = new InMemoryCache(CACHE_DURATION); + const memCache = new InMemoryCache(CACHE_DURATION); return this.getActivatedEnvironmentVariablesImpl(resource, interpreter, allowExceptions, shell) .then((vars) => { - cache.data = vars; - this.activatedEnvVariablesCache.set(cacheKey, cache); + memCache.data = vars; + this.activatedEnvVariablesCache.set(cacheKey, memCache); sendTelemetryEvent( EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, stopWatch.elapsedTime, @@ -176,6 +177,35 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi }); } + @cache(-1, true) + public async getProcessEnvironmentVariables(resource: Resource, shell?: string): Promise { + // Try to get the process environment variables using Python by printing variables, that can be little different + // from `process.env` and is preferred when calculating diff. + const globalInterpreters = this.interpreterService + .getInterpreters() + .filter((i) => !virtualEnvTypes.includes(i.envType)); + const interpreterPath = + globalInterpreters.length > 0 && globalInterpreters[0] ? globalInterpreters[0].path : 'python'; + try { + const [args, parse] = internalScripts.printEnvVariables(); + args.forEach((arg, i) => { + args[i] = arg.toCommandArgumentForPythonExt(); + }); + const command = `${interpreterPath} ${args.join(' ')}`; + const processService = await this.processServiceFactory.create(resource); + const result = await processService.shellExec(command, { + shell, + timeout: ENVIRONMENT_TIMEOUT, + maxBuffer: 1000 * 1000, + throwOnStdErr: false, + }); + const returnedEnv = this.parseEnvironmentOutput(result.stdout, parse); + return returnedEnv ?? process.env; + } catch (ex) { + return process.env; + } + } + public async getEnvironmentActivationShellCommands( resource: Resource, interpreter?: PythonEnvironment, @@ -231,7 +261,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi ); traceVerbose(`Activation Commands received ${activationCommands} for shell ${shellInfo.shell}`); if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) { - if (interpreter?.envType === EnvironmentType.Venv) { + if (interpreter && [EnvironmentType.Venv, EnvironmentType.Pyenv].includes(interpreter?.envType)) { const key = getSearchPathEnvVarNames()[0]; if (env[key]) { env[key] = `${path.dirname(interpreter.path)}${path.delimiter}${env[key]}`; @@ -247,7 +277,14 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi const activationCommand = fixActivationCommands(activationCommands).join(' && '); // In order to make sure we know where the environment output is, // put in a dummy echo we can look for - command = `${activationCommand} && echo '${ENVIRONMENT_PREFIX}' && python ${args.join(' ')}`; + const commandSeparator = [TerminalShellType.powershell, TerminalShellType.powershellCore].includes( + shellInfo.shellType, + ) + ? ';' + : '&&'; + command = `${activationCommand} ${commandSeparator} echo '${ENVIRONMENT_PREFIX}' ${commandSeparator} python ${args.join( + ' ', + )}`; } // Make sure python warnings don't interfere with getting the environment. However diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 85b393425836..f4c29f03707d 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -33,6 +33,7 @@ import { defaultShells } from './service'; import { IEnvironmentActivationService } from './types'; import { EnvironmentType } from '../../pythonEnvironments/info'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; +import { EnvironmentVariables } from '../../common/variables/types'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService { @@ -45,7 +46,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ private registeredOnce = false; - private previousEnvVars = _normCaseKeys(process.env); + /** + * Carries default environment variables for the currently selected shell. + */ + private processEnvVars: EnvironmentVariables | undefined; constructor( @inject(IPlatformService) private readonly platform: IPlatformService, @@ -90,6 +94,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ this.applicationEnvironment.onDidChangeShell( async (shell: string) => { this.showProgress(); + this.processEnvVars = undefined; // Pass in the shell where known instead of relying on the application environment, because of bug // on VSCode: https://github.com/microsoft/vscode/issues/160694 await this._applyCollection(undefined, shell).ignoreErrors(); @@ -106,6 +111,9 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { const workspaceFolder = this.getWorkspaceFolder(resource); const settings = this.configurationService.getSettings(resource); + const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder); + // Clear any previously set env vars from collection. + envVarCollection.clear(); if (!settings.terminal.activateEnvironment) { traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); return; @@ -116,7 +124,6 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ undefined, shell, ); - const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder); if (!env) { const shellType = identifyShellFromShellPath(shell); const defaultShell = defaultShells[this.platform.osType]; @@ -126,32 +133,38 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ await this._applyCollection(resource, defaultShell?.shell); return; } - envVarCollection.clear(); - this.previousEnvVars = _normCaseKeys(process.env); + this.processEnvVars = undefined; return; } - const previousEnv = this.previousEnvVars; - this.previousEnvVars = env; + if (!this.processEnvVars) { + this.processEnvVars = await this.environmentActivationService.getProcessEnvironmentVariables( + resource, + shell, + ); + } + const processEnv = this.processEnvVars; Object.keys(env).forEach((key) => { + if (shouldSkip(key)) { + return; + } const value = env[key]; - const prevValue = previousEnv[key]; + const prevValue = processEnv[key]; if (prevValue !== value) { if (value !== undefined) { + if (key === 'PS1') { + traceVerbose(`Prepending environment variable ${key} in collection with ${value}`); + envVarCollection.prepend(key, value, { + applyAtShellIntegration: true, + applyAtProcessCreation: false, + }); + return; + } traceVerbose(`Setting environment variable ${key} in collection to ${value}`); envVarCollection.replace(key, value, { applyAtShellIntegration: true }); - } else { - traceVerbose(`Clearing environment variable ${key} from collection`); - envVarCollection.delete(key); } } }); - Object.keys(previousEnv).forEach((key) => { - // If the previous env var is not in the current env, clear it from collection. - if (!(key in env)) { - traceVerbose(`Clearing environment variable ${key} from collection`); - envVarCollection.delete(key); - } - }); + const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); envVarCollection.description = description; @@ -224,13 +237,6 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } -export function _normCaseKeys(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { - const result: NodeJS.ProcessEnv = {}; - Object.keys(env).forEach((key) => { - // `os.environ` script used to get env vars normalizes keys to upper case: - // https://github.com/python/cpython/issues/101754 - // So convert `process.env` keys to upper case to match. - result[key.toUpperCase()] = env[key]; - }); - return result; +function shouldSkip(env: string) { + return ['_', 'SHLVL'].includes(env); } diff --git a/src/client/interpreter/activation/types.ts b/src/client/interpreter/activation/types.ts index d8e4ae16dbca..e00ef9b62b3f 100644 --- a/src/client/interpreter/activation/types.ts +++ b/src/client/interpreter/activation/types.ts @@ -4,10 +4,12 @@ 'use strict'; import { Resource } from '../../common/types'; +import { EnvironmentVariables } from '../../common/variables/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; export const IEnvironmentActivationService = Symbol('IEnvironmentActivationService'); export interface IEnvironmentActivationService { + getProcessEnvironmentVariables(resource: Resource, shell?: string): Promise; getActivatedEnvironmentVariables( resource: Resource, interpreter?: PythonEnvironment, diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index cb0b6b02f288..458e2b28c181 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -5,10 +5,10 @@ import * as sinon from 'sinon'; import { assert, expect } from 'chai'; -import { cloneDeep } from 'lodash'; import { mock, instance, when, anything, verify, reset } from 'ts-mockito'; import { EnvironmentVariableCollection, + EnvironmentVariableMutatorOptions, EnvironmentVariableScope, ProgressLocation, Uri, @@ -31,10 +31,7 @@ import { import { Interpreters } from '../../../client/common/utils/localize'; import { OSType, getOSType } from '../../../client/common/utils/platform'; import { defaultShells } from '../../../client/interpreter/activation/service'; -import { - TerminalEnvVarCollectionService, - _normCaseKeys, -} from '../../../client/interpreter/activation/terminalEnvVarCollectionService'; +import { TerminalEnvVarCollectionService } from '../../../client/interpreter/activation/terminalEnvVarCollectionService'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PathUtils } from '../../../client/common/platform/pathUtils'; @@ -89,11 +86,16 @@ suite('Terminal Environment Variable Collection Service', () => { }) .thenResolve(); environmentActivationService = mock(); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + process.env, + ); configService = mock(); when(configService.getSettings(anything())).thenReturn(({ terminal: { activateEnvironment: true }, pythonPath: displayPath, } as unknown) as IPythonSettings); + when(collection.clear()).thenResolve(); + when(scopedCollection.clear()).thenResolve(); terminalEnvVarCollectionService = new TerminalEnvVarCollectionService( instance(platform), instance(interpreterService), @@ -169,8 +171,27 @@ suite('Terminal Environment Variable Collection Service', () => { }); test('If activated variables are returned for custom shell, apply it correctly to the collection', async () => { - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; - delete envVars.PATH; + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + }); + + test('If activated variables contain PS1, prefix it using shell integration', async () => { + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env, PS1: '(prompt)' }; when( environmentActivationService.getActivatedEnvironmentVariables( anything(), @@ -182,16 +203,20 @@ suite('Terminal Environment Variable Collection Service', () => { when(collection.replace(anything(), anything(), anything())).thenResolve(); when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PS1', '(prompt)', anything())).thenCall((_, _v, o) => { + opts = o; + }); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + verify(collection.clear()).once(); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.delete('PATH')).once(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); test('Verify envs are not applied if env activation is disabled', async () => { - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; - delete envVars.PATH; + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; when( environmentActivationService.getActivatedEnvironmentVariables( anything(), @@ -211,13 +236,12 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + verify(collection.clear()).once(); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).never(); - verify(collection.delete('PATH')).never(); }); test('Verify correct options are used when applying envs and setting description', async () => { - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; - delete envVars.PATH; + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; const resource = Uri.file('a'); const workspaceFolder: WorkspaceFolder = { uri: Uri.file('workspacePath'), @@ -233,51 +257,11 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(options, { applyAtShellIntegration: true }); return Promise.resolve(); }); - when(scopedCollection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(resource, customShell); + verify(scopedCollection.clear()).once(); verify(scopedCollection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(scopedCollection.delete('PATH')).once(); - }); - - test('Only relative changes to previously applied variables are applied to the collection', async () => { - const envVars: NodeJS.ProcessEnv = { - RANDOM_VAR: 'random', - CONDA_PREFIX: 'prefix/to/conda', - ..._normCaseKeys(process.env), - }; - when( - environmentActivationService.getActivatedEnvironmentVariables( - anything(), - undefined, - undefined, - customShell, - ), - ).thenResolve(envVars); - - when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything())).thenResolve(); - - await terminalEnvVarCollectionService._applyCollection(undefined, customShell); - - const newEnvVars = cloneDeep(envVars); - delete newEnvVars.CONDA_PREFIX; - newEnvVars.RANDOM_VAR = undefined; // Deleting the variable from the collection is the same as setting it to undefined. - reset(environmentActivationService); - when( - environmentActivationService.getActivatedEnvironmentVariables( - anything(), - undefined, - undefined, - customShell, - ), - ).thenResolve(newEnvVars); - - await terminalEnvVarCollectionService._applyCollection(undefined, customShell); - - verify(collection.delete('CONDA_PREFIX')).once(); - verify(collection.delete('RANDOM_VAR')).once(); }); test('If no activated variables are returned for custom shell, fallback to using default shell', async () => { @@ -289,7 +273,7 @@ suite('Terminal Environment Variable Collection Service', () => { customShell, ), ).thenResolve(undefined); - const envVars = { CONDA_PREFIX: 'prefix/to/conda', ..._normCaseKeys(process.env) }; + const envVars = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; when( environmentActivationService.getActivatedEnvironmentVariables( anything(), @@ -300,12 +284,11 @@ suite('Terminal Environment Variable Collection Service', () => { ).thenResolve(envVars); when(collection.replace(anything(), anything(), anything())).thenResolve(); - when(collection.delete(anything())).thenResolve(); await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.delete(anything())).never(); + verify(collection.clear()).twice(); }); test('If no activated variables are returned for default shell, clear collection', async () => { From 40ff6e9fc1b07fef713ca552d94c86bddf2dd249 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 4 Aug 2023 10:22:31 -0700 Subject: [PATCH 0124/1136] Remove private Jupyter APIs from public API types (#21761) For https://github.com/microsoft/vscode-jupyter/issues/13986 --- pythonExtensionApi/package-lock.json | 4 +-- pythonExtensionApi/package.json | 2 +- src/client/api.ts | 7 +++++ src/client/api/types.ts | 46 +--------------------------- 4 files changed, 11 insertions(+), 48 deletions(-) diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index a03c33ab5c0d..4c5e98adbc16 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vscode/python-extension", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vscode/python-extension", - "version": "1.0.2", + "version": "1.0.3", "license": "MIT", "devDependencies": { "@types/vscode": "^1.78.0", diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index dd0e921f8abe..271542f161ec 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -1,7 +1,7 @@ { "name": "@vscode/python-extension", "description": "An API facade for the Python extension in VS Code", - "version": "1.0.2", + "version": "1.0.3", "author": { "name": "Microsoft Corporation" }, diff --git a/src/client/api.ts b/src/client/api.ts index 2371a4d88de1..23b2553c93d2 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -35,6 +35,13 @@ export function buildApi( const outputChannel = serviceContainer.get(ILanguageServerOutputChannel); const api: PythonExtension & { + /** + * Internal API just for Jupyter, hence don't include in the official types. + */ + jupyter: { + registerHooks(): void; + }; + } & { /** * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an * iteration or two. diff --git a/src/client/api/types.ts b/src/client/api/types.ts index 63954a16d868..4de554bf5a24 100644 --- a/src/client/api/types.ts +++ b/src/client/api/types.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, Event, Uri, WorkspaceFolder, QuickPickItem, extensions } from 'vscode'; +import { CancellationToken, Event, Uri, WorkspaceFolder, extensions } from 'vscode'; /* * Do not introduce any breaking changes to this API. @@ -12,9 +12,6 @@ export interface PythonExtension { * Promise indicating whether all parts of the extension have completed loading or not. */ ready: Promise; - jupyter: { - registerHooks(): void; - }; debug: { /** * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. @@ -109,47 +106,6 @@ export interface PythonExtension { }; } -interface IJupyterServerUri { - baseUrl: string; - token: string; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorizationHeader: any; // JSON object for authorization header. - expiration?: Date; // Date/time when header expires and should be refreshed. - displayName: string; -} - -type JupyterServerUriHandle = string; - -export interface IJupyterUriProvider { - readonly id: string; // Should be a unique string (like a guid) - getQuickPickEntryItems(): QuickPickItem[]; - handleQuickPick(item: QuickPickItem, backEnabled: boolean): Promise; - getServerUri(handle: JupyterServerUriHandle): Promise; -} - -interface IDataFrameInfo { - columns?: { key: string; type: ColumnType }[]; - indexColumn?: string; - rowCount?: number; -} - -export interface IDataViewerDataProvider { - dispose(): void; - getDataFrameInfo(): Promise; - getAllRows(): Promise; - getRows(start: number, end: number): Promise; -} - -enum ColumnType { - String = 'string', - Number = 'number', - Bool = 'bool', -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type IRowsResponse = any[]; - export type RefreshOptions = { /** * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so From dd20561bf36a60938e848d1bf6e058883ec7ae69 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 4 Aug 2023 10:31:15 -0700 Subject: [PATCH 0125/1136] revert due to buffer overflow on subprocess (#21762) revert https://github.com/microsoft/vscode-python/pull/21667 because it causes buffer overflow in the python testing subprocess when larger repos are used. Specifically seen on pytest discovery with >200 tests. Revert to align with the stable release and put in a fix next week. --- .../testing/testController/common/server.ts | 27 +- .../testing/testController/common/types.ts | 7 +- .../pytest/pytestDiscoveryAdapter.ts | 27 +- .../pytest/pytestExecutionAdapter.ts | 45 ++-- .../unittest/testDiscoveryAdapter.ts | 11 +- .../unittest/testExecutionAdapter.ts | 21 +- src/test/mocks/mockChildProcess.ts | 238 ----------------- .../pytestDiscoveryAdapter.unit.test.ts | 76 ++---- .../pytestExecutionAdapter.unit.test.ts | 118 ++------- .../testController/server.unit.test.ts | 248 +++++------------- .../workspaceTestAdapter.unit.test.ts | 3 +- 11 files changed, 157 insertions(+), 664 deletions(-) delete mode 100644 src/test/mocks/mockChildProcess.ts diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 564bd82f2ef6..f854371ffc35 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -3,11 +3,10 @@ import * as net from 'net'; import * as crypto from 'crypto'; -import { Disposable, Event, EventEmitter, TestRun } from 'vscode'; +import { Disposable, Event, EventEmitter } from 'vscode'; import * as path from 'path'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -16,7 +15,6 @@ import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER } from './utils'; -import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { private _onDataReceived: EventEmitter = new EventEmitter(); @@ -142,12 +140,7 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDataReceived.event; } - async sendCommand( - options: TestCommandOptions, - runTestIdPort?: string, - runInstance?: TestRun, - callback?: () => void, - ): Promise { + async sendCommand(options: TestCommandOptions, runTestIdPort?: string, callback?: () => void): Promise { const { uuid } = options; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; @@ -161,7 +154,7 @@ export class PythonTestServer implements ITestServer, Disposable { }; if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; - const isRun = runTestIdPort !== undefined; + const isRun = !options.testIds; // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, @@ -202,19 +195,7 @@ export class PythonTestServer implements ITestServer, Disposable { // This means it is running discovery traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); } - const deferred = createDeferred>(); - - const result = execService.execObservable(args, spawnOptions); - - runInstance?.token.onCancellationRequested(() => { - result?.proc?.kill(); - }); - result?.proc?.on('close', () => { - traceLog('Exec server closed.', uuid); - deferred.resolve({ stdout: '', stderr: '' }); - callback?.(); - }); - await deferred.promise; + await execService.exec(args, spawnOptions); } } catch (ex) { this.uuids = this.uuids.filter((u) => u !== uuid); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 16c0bd0e3cee..d4e54951bfd7 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -174,12 +174,7 @@ export interface ITestServer { readonly onDataReceived: Event; readonly onRunDataReceived: Event; readonly onDiscoveryDataReceived: Event; - sendCommand( - options: TestCommandOptions, - runTestIdsPort?: string, - runInstance?: TestRun, - callback?: () => void, - ): Promise; + sendCommand(options: TestCommandOptions, runTestIdsPort?: string, callback?: () => void): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 810fae0fa11c..b83224d4161b 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -4,14 +4,13 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceVerbose } from '../../../logging'; +import { traceError, traceVerbose } from '../../../logging'; import { DataReceivedEvent, DiscoveredTestPayload, @@ -49,7 +48,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - async runPytestDiscovery(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { + async runPytestDiscovery(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); @@ -79,15 +78,17 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); // delete UUID following entire discovery finishing. - const deferredExec = createDeferred>(); - const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); - const result = execService?.execObservable(execArgs, spawnOptions); - - result?.proc?.on('close', () => { - deferredExec.resolve({ stdout: '', stderr: '' }); - this.testServer.deleteUUID(uuid); - deferred.resolve(); - }); - await deferredExec.promise; + execService + ?.exec(['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), spawnOptions) + .then(() => { + this.testServer.deleteUUID(uuid); + return deferred.resolve(); + }) + .catch((err) => { + traceError(`Error while trying to run pytest discovery, \n${err}\r\n\r\n`); + this.testServer.deleteUUID(uuid); + return deferred.reject(err); + }); + return deferred.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index b05fa21fc046..a75a6089627c 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -15,7 +15,6 @@ import { } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -23,7 +22,13 @@ import { removePositionalFoldersAndFiles } from './arguments'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { PYTEST_PROVIDER } from '../../common/constants'; import { EXTENSION_ROOT_DIR } from '../../../common/constants'; -import * as utils from '../common/utils'; +import { startTestIdServer } from '../common/utils'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +// (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; +/** + * Wrapper Class for pytest test execution.. + */ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { constructor( @@ -43,20 +48,18 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); traceVerbose(uri, testIds, debugBool); - const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); - const dispose = function (testServer: ITestServer) { - testServer.deleteUUID(uuid); - disposedDataReceived.dispose(); - }; - runInstance?.token.onCancellationRequested(() => { - dispose(this.testServer); - }); - await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, executionFactory, debugLauncher); - + try { + await this.runTestsNew(uri, testIds, uuid, debugBool, executionFactory, debugLauncher); + } finally { + this.testServer.deleteUUID(uuid); + disposable.dispose(); + // confirm with testing that this gets called (it must clean this up) + } // placeholder until after the rewrite is adopted // TODO: remove after adoption. const executionPayload: ExecutionTestPayload = { @@ -71,7 +74,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], uuid: string, - runInstance?: TestRun, debugBool?: boolean, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, @@ -122,7 +124,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } traceLog(`Running PYTEST execution for the following test ids: ${testIds}`); - const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); + const pytestRunTestIdsPort = await startTestIdServer(testIds); if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); @@ -141,7 +143,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions, () => { deferred.resolve(); - this.testServer.deleteUUID(uuid); }); } else { // combine path to run script with run args @@ -149,19 +150,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const runArgs = [scriptPath, ...testArgs]; traceInfo(`Running pytests with arguments: ${runArgs.join(' ')}\r\n`); - const deferredExec = createDeferred>(); - const result = execService?.execObservable(runArgs, spawnOptions); - - runInstance?.token.onCancellationRequested(() => { - result?.proc?.kill(); - }); - - result?.proc?.on('close', () => { - deferredExec.resolve({ stdout: '', stderr: '' }); - this.testServer.deleteUUID(uuid); - deferred.resolve(); - }); - await deferredExec.promise; + await execService?.exec(runArgs, spawnOptions); } } catch (ex) { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index b49ac3dabd0e..6deca55117ea 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -46,11 +46,12 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); }); - - await this.callSendCommand(options, () => { + try { + await this.callSendCommand(options); + } finally { this.testServer.deleteUUID(uuid); disposable.dispose(); - }); + } // placeholder until after the rewrite is adopted // TODO: remove after adoption. const discoveryPayload: DiscoveredTestPayload = { @@ -60,8 +61,8 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - private async callSendCommand(options: TestCommandOptions, callback: () => void): Promise { - await this.testServer.sendCommand(options, undefined, undefined, callback); + private async callSendCommand(options: TestCommandOptions): Promise { + await this.testServer.sendCommand(options); const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; return discoveryPayload; } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 4cd392f93a43..4cab941c2608 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -37,19 +37,18 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance?: TestRun, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); - const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); - const dispose = function () { - disposedDataReceived.dispose(); - }; - runInstance?.token.onCancellationRequested(() => { + try { + await this.runTestsNew(uri, testIds, uuid, debugBool); + } finally { this.testServer.deleteUUID(uuid); - dispose(); - }); - await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, dispose); + disposable.dispose(); + // confirm with testing that this gets called (it must clean this up) + } const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; return executionPayload; } @@ -58,9 +57,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], uuid: string, - runInstance?: TestRun, debugBool?: boolean, - dispose?: () => void, ): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -83,10 +80,8 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const runTestIdsPort = await startTestIdServer(testIds); - await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, () => { - this.testServer.deleteUUID(uuid); + await this.testServer.sendCommand(options, runTestIdsPort.toString(), () => { deferred.resolve(); - dispose?.(); }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. diff --git a/src/test/mocks/mockChildProcess.ts b/src/test/mocks/mockChildProcess.ts deleted file mode 100644 index c038c0f845ab..000000000000 --- a/src/test/mocks/mockChildProcess.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Serializable, SendHandle, MessageOptions } from 'child_process'; -import { Writable, Readable, Pipe } from 'stream'; -import { EventEmitter } from 'node:events'; - -export class MockChildProcess extends EventEmitter { - constructor(spawnfile: string, spawnargs: string[]) { - super(); - this.spawnfile = spawnfile; - this.spawnargs = spawnargs; - this.stdin = new Writable(); - this.stdout = new Readable(); - this.stderr = null; - this.channel = null; - this.stdio = [this.stdin, this.stdout, this.stdout, this.stderr, null]; - this.killed = false; - this.connected = false; - this.exitCode = null; - this.signalCode = null; - this.eventMap = new Map(); - } - - stdin: Writable | null; - - stdout: Readable | null; - - stderr: Readable | null; - - eventMap: Map; - - readonly channel?: Pipe | null | undefined; - - readonly stdio: [ - Writable | null, - // stdin - Readable | null, - // stdout - Readable | null, - // stderr - Readable | Writable | null | undefined, - // extra - Readable | Writable | null | undefined, // extra - ]; - - readonly killed: boolean; - - readonly pid?: number | undefined; - - readonly connected: boolean; - - readonly exitCode: number | null; - - readonly signalCode: NodeJS.Signals | null; - - readonly spawnargs: string[]; - - readonly spawnfile: string; - - signal?: NodeJS.Signals | number; - - send(message: Serializable, callback?: (error: Error | null) => void): boolean; - - send(message: Serializable, sendHandle?: SendHandle, callback?: (error: Error | null) => void): boolean; - - send( - message: Serializable, - sendHandle?: SendHandle, - options?: MessageOptions, - callback?: (error: Error | null) => void, - ): boolean; - - send( - message: Serializable, - _sendHandleOrCallback?: SendHandle | ((error: Error | null) => void), - _optionsOrCallback?: MessageOptions | ((error: Error | null) => void), - _callback?: (error: Error | null) => void, - ): boolean { - // Implementation of the send method - // For example, you might want to emit a 'message' event - this.stdout?.push(message.toString()); - return true; - } - - // eslint-disable-next-line class-methods-use-this - disconnect(): void { - /* noop */ - } - - // eslint-disable-next-line class-methods-use-this - unref(): void { - /* noop */ - } - - // eslint-disable-next-line class-methods-use-this - ref(): void { - /* noop */ - } - - addListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - addListener(event: 'disconnect', listener: () => void): this; - - addListener(event: 'error', listener: (err: Error) => void): this; - - addListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - addListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; - - addListener(event: 'spawn', listener: () => void): this; - - addListener(event: string, listener: (...args: any[]) => void): this { - if (this.eventMap.has(event)) { - this.eventMap.get(event).push(listener); - } else { - this.eventMap.set(event, [listener]); - } - return this; - } - - emit(event: 'close', code: number | null, signal: NodeJS.Signals | null): boolean; - - emit(event: 'disconnect'): boolean; - - emit(event: 'error', err: Error): boolean; - - emit(event: 'exit', code: number | null, signal: NodeJS.Signals | null): boolean; - - emit(event: 'message', message: Serializable, sendHandle: SendHandle): boolean; - - emit(event: 'spawn', listener: () => void): boolean; - - emit(event: string | symbol, ...args: unknown[]): boolean { - if (this.eventMap.has(event.toString())) { - this.eventMap.get(event.toString()).forEach((listener: (arg0: unknown) => void) => { - const argsArray = Array.isArray(args) ? args : [args]; - listener(argsArray); - }); - } - return true; - } - - on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - on(event: 'disconnect', listener: () => void): this; - - on(event: 'error', listener: (err: Error) => void): this; - - on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - on(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; - - on(event: 'spawn', listener: () => void): this; - - on(event: string, listener: (...args: any[]) => void): this { - if (this.eventMap.has(event)) { - this.eventMap.get(event).push(listener); - } else { - this.eventMap.set(event, [listener]); - } - return this; - } - - once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - once(event: 'disconnect', listener: () => void): this; - - once(event: 'error', listener: (err: Error) => void): this; - - once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - once(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; - - once(event: 'spawn', listener: () => void): this; - - once(event: string, listener: (...args: any[]) => void): this { - if (this.eventMap.has(event)) { - this.eventMap.get(event).push(listener); - } else { - this.eventMap.set(event, [listener]); - } - return this; - } - - prependListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - prependListener(event: 'disconnect', listener: () => void): this; - - prependListener(event: 'error', listener: (err: Error) => void): this; - - prependListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - prependListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; - - prependListener(event: 'spawn', listener: () => void): this; - - prependListener(event: string, listener: (...args: any[]) => void): this { - if (this.eventMap.has(event)) { - this.eventMap.get(event).push(listener); - } else { - this.eventMap.set(event, [listener]); - } - return this; - } - - prependOnceListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - prependOnceListener(event: 'disconnect', listener: () => void): this; - - prependOnceListener(event: 'error', listener: (err: Error) => void): this; - - prependOnceListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; - - prependOnceListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; - - prependOnceListener(event: 'spawn', listener: () => void): this; - - prependOnceListener(event: string, listener: (...args: any[]) => void): this { - if (this.eventMap.has(event)) { - this.eventMap.get(event).push(listener); - } else { - this.eventMap.set(event, [listener]); - } - return this; - } - - trigger(event: string): Array { - if (this.eventMap.has(event)) { - return this.eventMap.get(event); - } - return []; - } - - kill(_signal?: NodeJS.Signals | number): boolean { - this.stdout?.destroy(); - return true; - } -} diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 8ba7dd9a6f00..18212b2d1032 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -5,7 +5,6 @@ import * as assert from 'assert'; import { Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as path from 'path'; -import { Observable } from 'rxjs/Observable'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; import { ITestServer } from '../../../../client/testing/testController/common/types'; @@ -13,11 +12,9 @@ import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions, - Output, } from '../../../../client/common/process/types'; +import { createDeferred, Deferred } from '../../../../client/common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -import { MockChildProcess } from '../../../mocks/mockChildProcess'; -import { Deferred, createDeferred } from '../../../../client/common/utils/async'; suite('pytest test discovery adapter', () => { let testServer: typeMoq.IMock; @@ -32,7 +29,6 @@ suite('pytest test discovery adapter', () => { let expectedPath: string; let uri: Uri; let expectedExtraVariables: Record; - let mockProc: MockChildProcess; setup(() => { const mockExtensionRootDir = typeMoq.Mock.ofType(); @@ -70,46 +66,32 @@ suite('pytest test discovery adapter', () => { }), } as unknown) as IConfigurationService; - // set up exec service with child process - mockProc = new MockChildProcess('', ['']); - execService = typeMoq.Mock.ofType(); - const output = new Observable>(() => { - /* no op */ - }); - execService - .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => ({ - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - })); - execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); - outputChannel = typeMoq.Mock.ofType(); - }); - test('Discovery should call exec with correct basic args', async () => { - // set up exec mock - deferred = createDeferred(); + // set up exec factory execFactory = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + + // set up exec service + execService = typeMoq.Mock.ofType(); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => { deferred.resolve(); - return Promise.resolve(execService.object); + return Promise.resolve({ stdout: '{}' }); }); - + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + outputChannel = typeMoq.Mock.ofType(); + }); + test('Discovery should call exec with correct basic args', async () => { adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); - adapter.discoverTests(uri, execFactory.object); - // add in await and trigger - await deferred.promise; - mockProc.trigger('close'); - - // verification + await adapter.discoverTests(uri, execFactory.object); const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; + execService.verify( (x) => - x.execObservable( + x.exec( expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); @@ -126,34 +108,16 @@ suite('pytest test discovery adapter', () => { const expectedPathNew = path.join('other', 'path'); const configServiceNew: IConfigurationService = ({ getSettings: () => ({ - testing: { - pytestArgs: ['.', 'abc', 'xyz'], - cwd: expectedPathNew, - }, + testing: { pytestArgs: ['.', 'abc', 'xyz'], cwd: expectedPathNew }, }), } as unknown) as IConfigurationService; - // set up exec mock - deferred = createDeferred(); - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => { - deferred.resolve(); - return Promise.resolve(execService.object); - }); - adapter = new PytestTestDiscoveryAdapter(testServer.object, configServiceNew, outputChannel.object); - adapter.discoverTests(uri, execFactory.object); - // add in await and trigger - await deferred.promise; - mockProc.trigger('close'); - - // verification + await adapter.discoverTests(uri, execFactory.object); const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.', 'abc', 'xyz']; execService.verify( (x) => - x.execObservable( + x.exec( expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 43b763f56e6c..44116fd753b0 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -6,13 +6,11 @@ import { TestRun, Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as sinon from 'sinon'; import * as path from 'path'; -import { Observable } from 'rxjs/Observable'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { ITestServer } from '../../../../client/testing/testController/common/types'; import { IPythonExecutionFactory, IPythonExecutionService, - Output, SpawnOptions, } from '../../../../client/common/process/types'; import { createDeferred, Deferred } from '../../../../client/common/utils/async'; @@ -20,7 +18,6 @@ import { PytestTestExecutionAdapter } from '../../../../client/testing/testContr import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; import * as util from '../../../../client/testing/testController/common/utils'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -import { MockChildProcess } from '../../../mocks/mockChildProcess'; suite('pytest test execution adapter', () => { let testServer: typeMoq.IMock; @@ -32,8 +29,8 @@ suite('pytest test execution adapter', () => { let debugLauncher: typeMoq.IMock; (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; let myTestPath: string; - let mockProc: MockChildProcess; - let utilsStub: sinon.SinonStub; + let startTestIdServerStub: sinon.SinonStub; + setup(() => { testServer = typeMoq.Mock.ofType(); testServer.setup((t) => t.getPort()).returns(() => 12345); @@ -50,24 +47,8 @@ suite('pytest test execution adapter', () => { }), isTestExecution: () => false, } as unknown) as IConfigurationService; - - // set up exec service with child process - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - execService = typeMoq.Mock.ofType(); - execService - .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => ({ - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - })); execFactory = typeMoq.Mock.ofType(); - utilsStub = sinon.stub(util, 'startTestIdServer'); + execService = typeMoq.Mock.ofType(); debugLauncher = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) @@ -85,6 +66,7 @@ suite('pytest test execution adapter', () => { deferred.resolve(); return Promise.resolve(); }); + startTestIdServerStub = sinon.stub(util, 'startTestIdServer').returns(Promise.resolve(54321)); execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); @@ -95,25 +77,10 @@ suite('pytest test execution adapter', () => { sinon.restore(); }); test('startTestIdServer called with correct testIds', async () => { - const deferred2 = createDeferred(); - const deferred3 = createDeferred(); - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => { - deferred2.resolve(); - return Promise.resolve(execService.object); - }); - utilsStub.callsFake(() => { - deferred3.resolve(); - return Promise.resolve(54321); - }); - const testRun = typeMoq.Mock.ofType(); - testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -121,38 +88,19 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - const testIds = ['test1id', 'test2id']; - adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); - // add in await and trigger - await deferred2.promise; - await deferred3.promise; - mockProc.trigger('close'); + const testIds = ['test1id', 'test2id']; + await adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); - // assert - sinon.assert.calledWithExactly(utilsStub, testIds); + sinon.assert.calledWithExactly(startTestIdServerStub, testIds); }); test('pytest execution called with correct args', async () => { - const deferred2 = createDeferred(); - const deferred3 = createDeferred(); - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => { - deferred2.resolve(); - return Promise.resolve(execService.object); - }); - utilsStub.callsFake(() => { - deferred3.resolve(); - return Promise.resolve(54321); - }); - const testRun = typeMoq.Mock.ofType(); - testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -160,12 +108,9 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - adapter.runTests(uri, [], false, testRun.object, execFactory.object); - - await deferred2.promise; - await deferred3.promise; - mockProc.trigger('close'); + await adapter.runTests(uri, [], false, testRun.object, execFactory.object); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); @@ -178,7 +123,7 @@ suite('pytest test execution adapter', () => { // execService.verify((x) => x.exec(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); execService.verify( (x) => - x.execObservable( + x.exec( expectedArgs, typeMoq.It.is((options) => { assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); @@ -194,21 +139,6 @@ suite('pytest test execution adapter', () => { ); }); test('pytest execution respects settings.testing.cwd when present', async () => { - const deferred2 = createDeferred(); - const deferred3 = createDeferred(); - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => { - deferred2.resolve(); - return Promise.resolve(execService.object); - }); - utilsStub.callsFake(() => { - deferred3.resolve(); - return Promise.resolve(54321); - }); - const testRun = typeMoq.Mock.ofType(); - testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const newCwd = path.join('new', 'path'); configService = ({ getSettings: () => ({ @@ -219,7 +149,7 @@ suite('pytest test execution adapter', () => { const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -227,12 +157,9 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - adapter.runTests(uri, [], false, testRun.object, execFactory.object); - - await deferred2.promise; - await deferred3.promise; - mockProc.trigger('close'); + await adapter.runTests(uri, [], false, testRun.object, execFactory.object); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); @@ -245,7 +172,7 @@ suite('pytest test execution adapter', () => { execService.verify( (x) => - x.execObservable( + x.exec( expectedArgs, typeMoq.It.is((options) => { assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); @@ -261,17 +188,10 @@ suite('pytest test execution adapter', () => { ); }); test('Debug launched correctly for pytest', async () => { - const deferred3 = createDeferred(); - utilsStub.callsFake(() => { - deferred3.resolve(); - return Promise.resolve(54321); - }); - const testRun = typeMoq.Mock.ofType(); - testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -279,9 +199,9 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); + const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); await adapter.runTests(uri, [], true, testRun.object, execFactory.object, debugLauncher.object); - await deferred3.promise; debugLauncher.verify( (x) => x.launchDebugger( diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 53c2b72e40f7..1131c26c6444 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -1,24 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -/* eslint-disable @typescript-eslint/no-explicit-any */ import * as assert from 'assert'; import * as net from 'net'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; import { OutputChannel, Uri } from 'vscode'; -import { Observable } from 'rxjs'; -import * as typeMoq from 'typemoq'; -import { - IPythonExecutionFactory, - IPythonExecutionService, - Output, - SpawnOptions, -} from '../../../client/common/process/types'; +import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions } from '../../../client/common/process/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; -import { Deferred, createDeferred } from '../../../client/common/utils/async'; -import { MockChildProcess } from '../../mocks/mockChildProcess'; +import { createDeferred } from '../../../client/common/utils/async'; suite('Python Test Server', () => { const fakeUuid = 'fake-uuid'; @@ -27,12 +18,10 @@ suite('Python Test Server', () => { let stubExecutionService: IPythonExecutionService; let server: PythonTestServer; let sandbox: sinon.SinonSandbox; + let execArgs: string[]; + let spawnOptions: SpawnOptions; let v4Stub: sinon.SinonStub; let debugLauncher: ITestDebugLauncher; - let mockProc: MockChildProcess; - let execService: typeMoq.IMock; - let deferred: Deferred; - let execFactory = typeMoq.Mock.ofType(); setup(() => { sandbox = sinon.createSandbox(); @@ -40,42 +29,27 @@ suite('Python Test Server', () => { v4Stub.returns(fakeUuid); stubExecutionService = ({ - execObservable: () => Promise.resolve({ stdout: '', stderr: '' }), + exec: (args: string[], spawnOptionsProvided: SpawnOptions) => { + execArgs = args; + spawnOptions = spawnOptionsProvided; + return Promise.resolve({ stdout: '', stderr: '' }); + }, } as unknown) as IPythonExecutionService; stubExecutionFactory = ({ createActivatedEnvironment: () => Promise.resolve(stubExecutionService), } as unknown) as IPythonExecutionFactory; - - // set up exec service with child process - mockProc = new MockChildProcess('', ['']); - execService = typeMoq.Mock.ofType(); - const output = new Observable>(() => { - /* no op */ - }); - execService - .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => ({ - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - })); - execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); }); teardown(() => { sandbox.restore(); + execArgs = []; server.dispose(); }); test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { const options = { - command: { - script: 'myscript', - args: ['-foo', 'foo'], - }, + command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, @@ -85,31 +59,17 @@ suite('Python Test Server', () => { outputChannel: undefined, token: undefined, throwOnStdErr: true, - extraVariables: { - PYTHONPATH: '/foo/bar', - RUN_TEST_IDS_PORT: '56789', - }, + extraVariables: { PYTHONPATH: '/foo/bar', RUN_TEST_IDS_PORT: '56789' }, } as SpawnOptions; - const deferred2 = createDeferred(); - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => { - deferred2.resolve(); - return Promise.resolve(execService.object); - }); - server = new PythonTestServer(execFactory.object, debugLauncher); + server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); - server.sendCommand(options, '56789'); - // add in await and trigger - await deferred2.promise; - mockProc.trigger('close'); - + await server.sendCommand(options, '56789'); const port = server.getPort(); - const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']; - execService.verify((x) => x.execObservable(expectedArgs, expectedSpawnOptions), typeMoq.Times.once()); + + assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); + assert.deepStrictEqual(spawnOptions, expectedSpawnOptions); }); test('sendCommand should write to an output channel if it is provided as an option', async () => { @@ -120,31 +80,17 @@ suite('Python Test Server', () => { }, } as OutputChannel; const options = { - command: { - script: 'myscript', - args: ['-foo', 'foo'], - }, + command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, outChannel, }; - deferred = createDeferred(); - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => { - deferred.resolve(); - return Promise.resolve(execService.object); - }); - server = new PythonTestServer(execFactory.object, debugLauncher); + server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); - server.sendCommand(options); - // add in await and trigger - await deferred.promise; - mockProc.trigger('close'); + await server.sendCommand(options); const port = server.getPort(); const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); @@ -153,12 +99,13 @@ suite('Python Test Server', () => { }); test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { - let eventData: { status: string; errors: string[] } | undefined; + let eventData: { status: string; errors: string[] }; stubExecutionService = ({ - execObservable: () => { + exec: () => { throw new Error('Failed to execute'); }, } as unknown) as IPythonExecutionService; + const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -175,43 +122,30 @@ suite('Python Test Server', () => { await server.sendCommand(options); - assert.notEqual(eventData, undefined); - assert.deepStrictEqual(eventData?.status, 'error'); - assert.deepStrictEqual(eventData?.errors, ['Failed to execute']); + assert.deepStrictEqual(eventData!.status, 'error'); + assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); }); test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { let eventData: string | undefined; const client = new net.Socket(); + const deferred = createDeferred(); + const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, }; - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { + + stubExecutionService = ({ + exec: async () => { client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; + return Promise.resolve({ stdout: '', stderr: '' }); }, } as unknown) as IPythonExecutionService; - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - - deferred = createDeferred(); - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); + server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -227,17 +161,16 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - server.sendCommand(options); - // add in await and trigger + await server.sendCommand(options); await deferred.promise; - mockProc.trigger('close'); - assert.deepStrictEqual(eventData, ''); }); test('If the server doesnt recognize the UUID it should ignore it', async () => { let eventData: string | undefined; const client = new net.Socket(); + const deferred = createDeferred(); + const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -245,28 +178,14 @@ suite('Python Test Server', () => { uuid: fakeUuid, }; - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { + stubExecutionService = ({ + exec: async () => { client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; + return Promise.resolve({ stdout: '', stderr: '' }); }, } as unknown) as IPythonExecutionService; - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); + server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -282,7 +201,7 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - server.sendCommand(options); + await server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, ''); }); @@ -293,34 +212,23 @@ suite('Python Test Server', () => { test('Error if payload does not have a content length header', async () => { let eventData: string | undefined; const client = new net.Socket(); + const deferred = createDeferred(); + const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, }; - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { + + stubExecutionService = ({ + exec: async () => { client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; + return Promise.resolve({ stdout: '', stderr: '' }); }, } as unknown) as IPythonExecutionService; - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); + server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -336,7 +244,7 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - server.sendCommand(options); + await server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, ''); }); @@ -359,6 +267,7 @@ Request-uuid: UUID_HERE // Your test logic here let eventData: string | undefined; const client = new net.Socket(); + const deferred = createDeferred(); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, @@ -366,28 +275,15 @@ Request-uuid: UUID_HERE cwd: '/foo/bar', uuid: fakeUuid, }; - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { + + stubExecutionService = ({ + exec: async () => { client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; + return Promise.resolve({ stdout: '', stderr: '' }); }, } as unknown) as IPythonExecutionService; - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); + server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); const uuid = server.createUUID(); payload = payload.replace('UUID_HERE', uuid); @@ -405,7 +301,7 @@ Request-uuid: UUID_HERE console.log('Socket connection error:', error); }); - server.sendCommand(options); + await server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, expectedResult); }); @@ -414,29 +310,8 @@ Request-uuid: UUID_HERE test('Calls run resolver if the result header is in the payload', async () => { let eventData: string | undefined; const client = new net.Socket(); + const deferred = createDeferred(); - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -444,6 +319,14 @@ Request-uuid: UUID_HERE uuid: fakeUuid, }; + stubExecutionService = ({ + exec: async () => { + client.connect(server.getPort()); + return Promise.resolve({ stdout: '', stderr: '' }); + }, + } as unknown) as IPythonExecutionService; + + server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); const uuid = server.createUUID(); server.onRunDataReceived(({ data }) => { @@ -466,8 +349,9 @@ Request-uuid: ${uuid} console.log('Socket connection error:', error); }); - server.sendCommand(options); + await server.sendCommand(options); await deferred.promise; + console.log('event data', eventData); const expectedResult = '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; assert.deepStrictEqual(eventData, expectedResult); diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts index 41cd1bbd7ef2..5a2e48130746 100644 --- a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -164,7 +164,8 @@ suite('Workspace test adapter', () => { const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); const testProvider = 'unittest'; - await workspaceTestAdapter.discoverTests(testController); + const abc = await workspaceTestAdapter.discoverTests(testController); + console.log(abc); sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider); From cabdf39f66fa85c23b64bef429e815454094922a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 4 Aug 2023 11:52:30 -0700 Subject: [PATCH 0126/1136] Use `optionalDependencies` instead of `peerDependencies` for `@vscode/python-extension` npm package (#21763) Closes https://github.com/microsoft/vscode-python/issues/21720 --- pythonExtensionApi/package-lock.json | 2 +- pythonExtensionApi/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index 4c5e98adbc16..f882b489d793 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -16,7 +16,7 @@ "node": ">=16.17.1", "vscode": "^1.78.0" }, - "peerDependencies": { + "optionalDependencies": { "@types/vscode": "^1.78.0" } }, diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index 271542f161ec..6359a7fe0fae 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -25,7 +25,7 @@ "bugs": { "url": "https://github.com/Microsoft/vscode-python/issues" }, - "peerDependencies": { + "optionalDependencies": { "@types/vscode": "^1.78.0" }, "devDependencies": { From 8e0e59be81eae58c8120af5453ba178e3aa40aec Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 4 Aug 2023 13:37:44 -0700 Subject: [PATCH 0127/1136] Remove optionalDependencies from API npm package and document to install vscode types separately (#21764) Closes It still leads to conflicts due to double installation of vscode types when testing through the cases, removing vscode types as dependencies altogether and documenting to install it separately instead. --- pythonExtensionApi/README.md | 5 ++++- pythonExtensionApi/package-lock.json | 3 --- pythonExtensionApi/package.json | 3 --- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pythonExtensionApi/README.md b/pythonExtensionApi/README.md index 1587635aae40..4acd26008b24 100644 --- a/pythonExtensionApi/README.md +++ b/pythonExtensionApi/README.md @@ -17,11 +17,14 @@ First we need to define a `package.json` for the extension that wants to use the // Depend on the Python extension facade npm module to get easier API access to the // core extension. "dependencies": { - "@vscode/python-extension": "..." + "@vscode/python-extension": "...", + "@types/vscode": "..." }, } ``` +Update `"@types/vscode"` to [a recent version](https://code.visualstudio.com/updates/) of VS Code, say `"^1.81.0"` for VS Code version `"1.81"`, in case there are any conflicts. + The actual source code to get the active environment to run some script could look like this: ```typescript diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index f882b489d793..0b2abfd1895b 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -15,9 +15,6 @@ "engines": { "node": ">=16.17.1", "vscode": "^1.78.0" - }, - "optionalDependencies": { - "@types/vscode": "^1.78.0" } }, "node_modules/@types/vscode": { diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index 6359a7fe0fae..ee61029b07d2 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -25,9 +25,6 @@ "bugs": { "url": "https://github.com/Microsoft/vscode-python/issues" }, - "optionalDependencies": { - "@types/vscode": "^1.78.0" - }, "devDependencies": { "typescript": "5.0.4", "@types/vscode": "^1.78.0" From c490339df7f4de2651f18205399023e85f64ec43 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 4 Aug 2023 14:35:11 -0700 Subject: [PATCH 0128/1136] Update version of npm package (#21765) --- pythonExtensionApi/package-lock.json | 4 ++-- pythonExtensionApi/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index 0b2abfd1895b..9b4847457b20 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vscode/python-extension", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vscode/python-extension", - "version": "1.0.3", + "version": "1.0.4", "license": "MIT", "devDependencies": { "@types/vscode": "^1.78.0", diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index ee61029b07d2..86ac58f42f20 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -1,7 +1,7 @@ { "name": "@vscode/python-extension", "description": "An API facade for the Python extension in VS Code", - "version": "1.0.3", + "version": "1.0.4", "author": { "name": "Microsoft Corporation" }, From 3fed49f0b79feb500e2f262d55d5d214282dde22 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Fri, 4 Aug 2023 14:38:50 -0700 Subject: [PATCH 0129/1136] Add Ruff to CI (#21739) Add Ruff to (lint > action.yml) CI #21738 --- .github/actions/lint/action.yml | 7 +++++ pythonFiles/installed_check.py | 8 ++--- pythonFiles/normalizeSelection.py | 2 +- pythonFiles/pyproject.toml | 31 +++++++++++++++++++ pythonFiles/shell_exec.py | 3 +- .../testing_tools/adapter/pytest/__init__.py | 4 +-- .../expected_execution_test_output.py | 4 ++- pythonFiles/tests/pytestadapter/helpers.py | 2 +- .../tests/pytestadapter/test_execution.py | 5 +-- pythonFiles/tests/test_create_microvenv.py | 3 +- pythonFiles/tests/test_create_venv.py | 5 +-- .../.data/discovery_error/file_one.py | 2 +- .../tests/unittestadapter/test_utils.py | 4 ++- pythonFiles/unittestadapter/execution.py | 7 +++-- pythonFiles/unittestadapter/utils.py | 7 +++-- .../tests/logParser.py | 9 +++--- 16 files changed, 73 insertions(+), 30 deletions(-) diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml index 9478550c107b..1efa6aab79a5 100644 --- a/.github/actions/lint/action.yml +++ b/.github/actions/lint/action.yml @@ -47,3 +47,10 @@ runs: python -m black . --check working-directory: pythonFiles shell: bash + + - name: Run Ruff + run: | + python -m pip install -U ruff + python -m ruff check . + working-directory: pythonFiles + shell: bash diff --git a/pythonFiles/installed_check.py b/pythonFiles/installed_check.py index f0e1c268d270..df5d3cb9d082 100644 --- a/pythonFiles/installed_check.py +++ b/pythonFiles/installed_check.py @@ -36,7 +36,7 @@ def parse_requirements(line: str) -> Optional[Requirement]: return req elif req.marker.evaluate(): return req - except: + except Exception: return None @@ -51,7 +51,7 @@ def process_requirements(req_file: pathlib.Path) -> List[Dict[str, Union[str, in try: # Check if package is installed metadata(req.name) - except: + except Exception: diagnostics.append( { "line": n, @@ -79,7 +79,7 @@ def process_pyproject(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]] try: raw_text = req_file.read_text(encoding="utf-8") pyproject = tomli.loads(raw_text) - except: + except Exception: return diagnostics lines = raw_text.splitlines() @@ -91,7 +91,7 @@ def process_pyproject(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]] try: # Check if package is installed metadata(req.name) - except: + except Exception: diagnostics.append( { "line": n, diff --git a/pythonFiles/normalizeSelection.py b/pythonFiles/normalizeSelection.py index 35bc42d6e6fe..0363702717ab 100644 --- a/pythonFiles/normalizeSelection.py +++ b/pythonFiles/normalizeSelection.py @@ -118,7 +118,7 @@ def normalize_lines(selection): # Insert a newline between each top-level statement, and append a newline to the selection. source = "\n".join(statements) + "\n" - except: + except Exception: # If there's a problem when parsing statements, # append a blank line to end the block and send it as-is. source = selection + "\n\n" diff --git a/pythonFiles/pyproject.toml b/pythonFiles/pyproject.toml index 56237999e603..d865e27418e0 100644 --- a/pythonFiles/pyproject.toml +++ b/pythonFiles/pyproject.toml @@ -34,3 +34,34 @@ ignore = [ 'tests/testing_tools/adapter/pytest/test_cli.py', 'tests/testing_tools/adapter/pytest/test_discovery.py', ] + +[tool.ruff] +line-length = 140 +ignore = ["E402"] +exclude = [ + # Ignore testing_tools files same as Pyright way + 'get-pip.py', + 'install_debugpy.py', + 'tensorboard_launcher.py', + 'testlauncher.py', + 'visualstudio_py_testlauncher.py', + 'testing_tools/unittest_discovery.py', + 'testing_tools/adapter/util.py', + 'testing_tools/adapter/pytest/_discovery.py', + 'testing_tools/adapter/pytest/_pytest_item.py', + 'tests/debug_adapter/test_install_debugpy.py', + 'tests/testing_tools/adapter/.data', + 'tests/testing_tools/adapter/test___main__.py', + 'tests/testing_tools/adapter/test_discovery.py', + 'tests/testing_tools/adapter/test_functional.py', + 'tests/testing_tools/adapter/test_report.py', + 'tests/testing_tools/adapter/test_util.py', + 'tests/testing_tools/adapter/pytest/test_cli.py', + 'tests/testing_tools/adapter/pytest/test_discovery.py', + 'pythonFiles/testing_tools/*', + 'pythonFiles/testing_tools/adapter/pytest/__init__.py', + 'pythonFiles/tests/pytestadapter/expected_execution_test_output.py', + 'pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py', + 'pythonFiles/tests/unittestadapter/test_utils.py', + +] diff --git a/pythonFiles/shell_exec.py b/pythonFiles/shell_exec.py index c521586ca31b..4987399a53ea 100644 --- a/pythonFiles/shell_exec.py +++ b/pythonFiles/shell_exec.py @@ -1,9 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import os -import sys import subprocess +import sys # This is a simple solution to waiting for completion of commands sent to terminal. # 1. Intercept commands send to a terminal diff --git a/pythonFiles/testing_tools/adapter/pytest/__init__.py b/pythonFiles/testing_tools/adapter/pytest/__init__.py index e894f7bcdb8e..89b7c066a459 100644 --- a/pythonFiles/testing_tools/adapter/pytest/__init__.py +++ b/pythonFiles/testing_tools/adapter/pytest/__init__.py @@ -3,5 +3,5 @@ from __future__ import absolute_import -from ._cli import add_subparser as add_cli_subparser -from ._discovery import discover +from ._cli import add_subparser as add_cli_subparser # noqa: F401 +from ._discovery import discover # noqa: F401 diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index fe1d40a55b43..dd8f458d792e 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -5,7 +5,9 @@ TEST_ADD_FUNCTION = "unittest_folder/test_add.py::TestAddFunction::" SUCCESS = "success" FAILURE = "failure" -TEST_SUBTRACT_FUNCTION_NEGATIVE_NUMBERS_ERROR = "self = \n\n def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers\n self,\n ):\n result = subtract(-2, -3)\n> self.assertEqual(result, 100000)\nE AssertionError: 1 != 100000\n\nunittest_folder/test_subtract.py:25: AssertionError" + +TEST_SUBTRACT_FUNCTION_NEGATIVE_NUMBERS_ERROR = "self = \n\n def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers\n self,\n ):\n result = subtract(-2, -3)\n> self.assertEqual(result, 100000)\nE AssertionError: 1 != 100000\n\nunittest_folder/test_subtract.py:25: AssertionError" # noqa: E501 + # This is the expected output for the unittest_folder execute tests # └── unittest_folder diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index c3e01d52170a..47b4f75d6d60 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -10,7 +10,7 @@ import sys import threading import uuid -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" from typing_extensions import TypedDict diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index ffc84955bf54..e3b00386882d 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -4,6 +4,7 @@ import shutil import pytest + from tests.pytestadapter import expected_execution_test_output from .helpers import TEST_DATA_PATH, runner @@ -161,7 +162,7 @@ def test_pytest_execution(test_ids, expected_const): Keyword arguments: test_ids -- an array of test_ids to run. expected_const -- a dictionary of the expected output from running pytest discovery on the files. - """ + """ # noqa: E501 args = test_ids actual = runner(args) assert actual @@ -179,6 +180,6 @@ def test_pytest_execution(test_ids, expected_const): or actual_result_dict[key]["outcome"] == "error" ): actual_result_dict[key]["message"] = "ERROR MESSAGE" - if actual_result_dict[key]["traceback"] != None: + if actual_result_dict[key]["traceback"] is not None: actual_result_dict[key]["traceback"] = "TRACEBACK" assert actual_result_dict == expected_const diff --git a/pythonFiles/tests/test_create_microvenv.py b/pythonFiles/tests/test_create_microvenv.py index f123052c491c..e5d4e68802e9 100644 --- a/pythonFiles/tests/test_create_microvenv.py +++ b/pythonFiles/tests/test_create_microvenv.py @@ -6,7 +6,6 @@ import sys import create_microvenv -import pytest def test_create_microvenv(): @@ -26,4 +25,4 @@ def run_process(args, error_message): create_microvenv.run_process = run_process create_microvenv.main() - assert run_process_called == True + assert run_process_called is True diff --git a/pythonFiles/tests/test_create_venv.py b/pythonFiles/tests/test_create_venv.py index bebe304c13c3..ae3f18be6f3c 100644 --- a/pythonFiles/tests/test_create_venv.py +++ b/pythonFiles/tests/test_create_venv.py @@ -5,9 +5,10 @@ import os import sys -import create_venv import pytest +import create_venv + @pytest.mark.skipif( sys.platform == "win32", reason="Windows does not have micro venv fallback." @@ -35,7 +36,7 @@ def run_process(args, error_message): create_venv.main(["--name", ".test_venv"]) # run_process is called when the venv does not exist - assert run_process_called == True + assert run_process_called is True @pytest.mark.skipif( diff --git a/pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py b/pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py index 42f84f046760..031b6f6c9d68 100644 --- a/pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py +++ b/pythonFiles/tests/unittestadapter/.data/discovery_error/file_one.py @@ -3,7 +3,7 @@ import unittest -import something_else # type: ignore +import something_else # type: ignore # noqa: F401 class DiscoveryErrorOne(unittest.TestCase): diff --git a/pythonFiles/tests/unittestadapter/test_utils.py b/pythonFiles/tests/unittestadapter/test_utils.py index a3bc1dd7693c..e262f877d52c 100644 --- a/pythonFiles/tests/unittestadapter/test_utils.py +++ b/pythonFiles/tests/unittestadapter/test_utils.py @@ -6,6 +6,7 @@ import unittest import pytest + from unittestadapter.utils import ( TestNode, TestNodeTypeEnum, @@ -284,7 +285,8 @@ def test_build_decorated_tree() -> None: def test_build_empty_tree() -> None: - """The build_test_tree function should return None if there are no discovered test suites, and an empty list of errors if there are none in the discovered data.""" + """The build_test_tree function should return None if there are no discovered test suites, + and an empty list of errors if there are none in the discovered data.""" start_dir = os.fsdecode(TEST_DATA_PATH) pattern = "does_not_exist*" diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index dfb6928a2074..f239f81c2d87 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -17,8 +17,9 @@ sys.path.append(os.fspath(script_dir)) sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from testing_tools import process_json_util, socket_manager from typing_extensions import NotRequired, TypeAlias, TypedDict + +from testing_tools import process_json_util, socket_manager from unittestadapter.utils import parse_unittest_args DEFAULT_PORT = "45454" @@ -194,12 +195,12 @@ def run_tests( # Discover tests at path with the file name as a pattern (if any). loader = unittest.TestLoader() - args = { + args = { # noqa: F841 "start_dir": start_dir, "pattern": pattern, "top_level_dir": top_level_dir, } - suite = loader.discover(start_dir, pattern, top_level_dir) + suite = loader.discover(start_dir, pattern, top_level_dir) # noqa: F841 # Run tests. runner = unittest.TextTestRunner(resultclass=UnittestTestResult) diff --git a/pythonFiles/unittestadapter/utils.py b/pythonFiles/unittestadapter/utils.py index 78000e2a945f..64f08217f38f 100644 --- a/pythonFiles/unittestadapter/utils.py +++ b/pythonFiles/unittestadapter/utils.py @@ -60,11 +60,11 @@ def get_source_line(obj) -> str: """Get the line number of a test case start line.""" try: sourcelines, lineno = inspect.getsourcelines(obj) - except: + except Exception: try: # tornado-specific, see https://github.com/microsoft/vscode-python/issues/17285. sourcelines, lineno = inspect.getsourcelines(obj.orig_method) - except: + except Exception: return "*" # Return the line number of the first line of the test case definition. @@ -226,7 +226,8 @@ def parse_unittest_args(args: List[str]) -> Tuple[str, str, Union[str, None]]: The returned tuple contains the following items - start_directory: The directory where to start discovery, defaults to . - pattern: The pattern to match test files, defaults to test*.py - - top_level_directory: The top-level directory of the project, defaults to None, and unittest will use start_directory behind the scenes. + - top_level_directory: The top-level directory of the project, defaults to None, + and unittest will use start_directory behind the scenes. """ arg_parser = argparse.ArgumentParser() diff --git a/pythonFiles/vscode_datascience_helpers/tests/logParser.py b/pythonFiles/vscode_datascience_helpers/tests/logParser.py index 767f837c5136..e021853fee7a 100644 --- a/pythonFiles/vscode_datascience_helpers/tests/logParser.py +++ b/pythonFiles/vscode_datascience_helpers/tests/logParser.py @@ -1,11 +1,10 @@ -from io import TextIOWrapper -import sys import argparse import os +from io import TextIOWrapper os.system("color") -from pathlib import Path import re +from pathlib import Path parser = argparse.ArgumentParser(description="Parse a test log into its parts") parser.add_argument("testlog", type=str, nargs=1, help="Log to parse") @@ -63,14 +62,14 @@ def splitByPid(testlog): pid = int(match.group(1)) # See if we've created a log for this pid or not - if not pid in pids: + if pid not in pids: pids.add(pid) logFile = "{}_{}.log".format(baseFile, pid) print("Writing to new log: " + logFile) logs[pid] = Path(logFile).open(mode="w") # Add this line to the log - if pid != None: + if pid is not None: logs[pid].write(line) # Close all of the open logs for key in logs: From 9ac14b893291a883338dd93ab84b59bc82f77280 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 4 Aug 2023 15:52:29 -0700 Subject: [PATCH 0130/1136] Update README.md for npm package (#21766) Fix indent for https://www.npmjs.com/package/@vscode/python-extension?activeTab=readme --- pythonExtensionApi/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonExtensionApi/README.md b/pythonExtensionApi/README.md index 4acd26008b24..5208d90cdfa5 100644 --- a/pythonExtensionApi/README.md +++ b/pythonExtensionApi/README.md @@ -18,7 +18,7 @@ First we need to define a `package.json` for the extension that wants to use the // core extension. "dependencies": { "@vscode/python-extension": "...", - "@types/vscode": "..." + "@types/vscode": "..." }, } ``` From 0a2c28501d5bead69f8a05fcf40b0e4c0e0fd9b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Aug 2023 16:51:59 -0700 Subject: [PATCH 0131/1136] Bump brettcannon/check-for-changed-files from 1.1.1 to 1.2.0 (#21772) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [brettcannon/check-for-changed-files](https://github.com/brettcannon/check-for-changed-files) from 1.1.1 to 1.2.0.
Release notes

Sourced from brettcannon/check-for-changed-files's releases.

v1.2.0

What's Changed

New Contributors

Full Changelog: https://github.com/brettcannon/check-for-changed-files/compare/v1...v1.2.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=brettcannon/check-for-changed-files&package-manager=github_actions&previous-version=1.1.1&new-version=1.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-file-check.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index 2d227ea00399..ba019c790e99 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'package-lock.json matches package.json' - uses: brettcannon/check-for-changed-files@v1.1.1 + uses: brettcannon/check-for-changed-files@v1.2.0 with: prereq-pattern: 'package.json' file-pattern: 'package-lock.json' @@ -25,7 +25,7 @@ jobs: failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - name: 'package.json matches package-lock.json' - uses: brettcannon/check-for-changed-files@v1.1.1 + uses: brettcannon/check-for-changed-files@v1.2.0 with: prereq-pattern: 'package-lock.json' file-pattern: 'package.json' @@ -33,7 +33,7 @@ jobs: failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - name: 'Tests' - uses: brettcannon/check-for-changed-files@v1.1.1 + uses: brettcannon/check-for-changed-files@v1.2.0 with: prereq-pattern: src/**/*.ts file-pattern: | From fbbf987c3affb475c2360164f4ecc1eb86e83f7b Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 9 Aug 2023 18:20:26 -0700 Subject: [PATCH 0132/1136] Use updated API to fetch scoped env collection (#21788) For https://github.com/microsoft/vscode/issues/171173 https://github.com/microsoft/vscode-python/issues/20822 To be merged tomorrow when latest insiders is released. Blocked on https://github.com/microsoft/vscode/pull/189979. --- package-lock.json | 16 ++++----- package.json | 4 +-- .../terminalEnvVarCollectionService.ts | 34 ++++++------------- ...rminalEnvVarCollectionService.unit.test.ts | 20 +++++------ ...scode.proposed.envCollectionWorkspace.d.ts | 33 +++++++++--------- 5 files changed, 46 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01f03cb31661..56b94d1dd44c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "@types/stack-trace": "0.0.29", "@types/tmp": "^0.0.33", "@types/uuid": "^8.3.4", - "@types/vscode": "^1.75.0", + "@types/vscode": "^1.81.0", "@types/which": "^2.0.1", "@types/winreg": "^1.2.30", "@types/xml2js": "^0.4.2", @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.79.0-20230526" + "vscode": "^1.81.0-20230809" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1491,9 +1491,9 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.75.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.75.1.tgz", - "integrity": "sha512-emg7wdsTFzdi+elvoyoA+Q8keEautdQHyY5LNmHVM4PTpY8JgOTVADrGVyXGepJ6dVW2OS5/xnLUWh+nZxvdiA==", + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.81.0.tgz", + "integrity": "sha512-YIaCwpT+O2E7WOMq0eCgBEABE++SX3Yl/O02GoMIF2DO3qAtvw7m6BXFYsxnc6XyzwZgh6/s/UG78LSSombl2w==", "dev": true }, "node_modules/@types/which": { @@ -16508,9 +16508,9 @@ "dev": true }, "@types/vscode": { - "version": "1.75.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.75.1.tgz", - "integrity": "sha512-emg7wdsTFzdi+elvoyoA+Q8keEautdQHyY5LNmHVM4PTpY8JgOTVADrGVyXGepJ6dVW2OS5/xnLUWh+nZxvdiA==", + "version": "1.81.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.81.0.tgz", + "integrity": "sha512-YIaCwpT+O2E7WOMq0eCgBEABE++SX3Yl/O02GoMIF2DO3qAtvw7m6BXFYsxnc6XyzwZgh6/s/UG78LSSombl2w==", "dev": true }, "@types/which": { diff --git a/package.json b/package.json index b6a4b65b42a9..2d78d407a0c9 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.79.0-20230526" + "vscode": "^1.81.0-20230809" }, "enableTelemetry": false, "keywords": [ @@ -2151,7 +2151,7 @@ "@types/stack-trace": "0.0.29", "@types/tmp": "^0.0.33", "@types/uuid": "^8.3.4", - "@types/vscode": "^1.75.0", + "@types/vscode": "^1.81.0", "@types/which": "^2.0.1", "@types/winreg": "^1.2.30", "@types/xml2js": "^0.4.2", diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index f4c29f03707d..2e5c26be0222 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -3,14 +3,7 @@ import * as path from 'path'; import { inject, injectable } from 'inversify'; -import { - ProgressOptions, - ProgressLocation, - MarkdownString, - WorkspaceFolder, - EnvironmentVariableCollection, - EnvironmentVariableScope, -} from 'vscode'; +import { ProgressOptions, ProgressLocation, MarkdownString, WorkspaceFolder } from 'vscode'; import { pathExists } from 'fs-extra'; import { IExtensionActivationService } from '../../activation/types'; import { IApplicationShell, IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; @@ -67,7 +60,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ public async activate(resource: Resource): Promise { if (!inTerminalEnvVarExperiment(this.experimentService)) { - this.context.environmentVariableCollection.clear(); + const workspaceFolder = this.getWorkspaceFolder(resource); + this.context.getEnvironmentVariableCollection({ workspaceFolder }).clear(); await this.handleMicroVenv(resource); if (!this.registeredOnce) { this.interpreterService.onDidChangeInterpreter( @@ -111,8 +105,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { const workspaceFolder = this.getWorkspaceFolder(resource); const settings = this.configurationService.getSettings(resource); - const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder); - // Clear any previously set env vars from collection. + const envVarCollection = this.context.getEnvironmentVariableCollection({ workspaceFolder }); + // Clear any previously set env vars from collection envVarCollection.clear(); if (!settings.terminal.activateEnvironment) { traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); @@ -160,7 +154,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ return; } traceVerbose(`Setting environment variable ${key} in collection to ${value}`); - envVarCollection.replace(key, value, { applyAtShellIntegration: true }); + envVarCollection.replace(key, value, { + applyAtShellIntegration: true, + applyAtProcessCreation: true, + }); } } }); @@ -170,22 +167,13 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ envVarCollection.description = description; } - private getEnvironmentVariableCollection(workspaceFolder?: WorkspaceFolder) { - const envVarCollection = this.context.environmentVariableCollection as EnvironmentVariableCollection & { - getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; - }; - return workspaceFolder - ? envVarCollection.getScopedEnvironmentVariableCollection({ workspaceFolder }) - : envVarCollection; - } - private async handleMicroVenv(resource: Resource) { const workspaceFolder = this.getWorkspaceFolder(resource); const interpreter = await this.interpreterService.getActiveInterpreter(resource); if (interpreter?.envType === EnvironmentType.Venv) { const activatePath = path.join(path.dirname(interpreter.path), 'activate'); if (!(await pathExists(activatePath))) { - const envVarCollection = this.getEnvironmentVariableCollection(workspaceFolder); + const envVarCollection = this.context.getEnvironmentVariableCollection({ workspaceFolder }); const pathVarName = getSearchPathEnvVarNames()[0]; envVarCollection.replace( 'PATH', @@ -195,7 +183,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ return; } } - this.context.environmentVariableCollection.clear(); + this.context.getEnvironmentVariableCollection({ workspaceFolder }).clear(); } private getWorkspaceFolder(resource: Resource): WorkspaceFolder | undefined { diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 458e2b28c181..73c9e82274de 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -45,7 +45,6 @@ suite('Terminal Environment Variable Collection Service', () => { let collection: EnvironmentVariableCollection & { getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; }; - let scopedCollection: EnvironmentVariableCollection; let applicationEnvironment: IApplicationEnvironment; let environmentActivationService: IEnvironmentActivationService; let workspaceService: IWorkspaceService; @@ -73,9 +72,7 @@ suite('Terminal Environment Variable Collection Service', () => { getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; } >(); - scopedCollection = mock(); - when(collection.getScopedEnvironmentVariableCollection(anything())).thenReturn(instance(scopedCollection)); - when(context.environmentVariableCollection).thenReturn(instance(collection)); + when(context.getEnvironmentVariableCollection(anything())).thenReturn(instance(collection)); experimentService = mock(); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); applicationEnvironment = mock(); @@ -95,7 +92,6 @@ suite('Terminal Environment Variable Collection Service', () => { pythonPath: displayPath, } as unknown) as IPythonSettings); when(collection.clear()).thenResolve(); - when(scopedCollection.clear()).thenResolve(); terminalEnvVarCollectionService = new TerminalEnvVarCollectionService( instance(platform), instance(interpreterService), @@ -253,15 +249,17 @@ suite('Terminal Environment Variable Collection Service', () => { environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, customShell), ).thenResolve(envVars); - when(scopedCollection.replace(anything(), anything(), anything())).thenCall((_e, _v, options) => { - assert.deepEqual(options, { applyAtShellIntegration: true }); - return Promise.resolve(); - }); + when(collection.replace(anything(), anything(), anything())).thenCall( + (_e, _v, options: EnvironmentVariableMutatorOptions) => { + assert.deepEqual(options, { applyAtShellIntegration: true, applyAtProcessCreation: true }); + return Promise.resolve(); + }, + ); await terminalEnvVarCollectionService._applyCollection(resource, customShell); - verify(scopedCollection.clear()).once(); - verify(scopedCollection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); }); test('If no activated variables are returned for custom shell, fallback to using default shell', async () => { diff --git a/types/vscode.proposed.envCollectionWorkspace.d.ts b/types/vscode.proposed.envCollectionWorkspace.d.ts index d778e53e5086..406e5b98cd47 100644 --- a/types/vscode.proposed.envCollectionWorkspace.d.ts +++ b/types/vscode.proposed.envCollectionWorkspace.d.ts @@ -7,15 +7,22 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/182069 - // export interface ExtensionContext { - // /** - // * Gets the extension's environment variable collection for this workspace, enabling changes - // * to be applied to terminal environment variables. - // * - // * @param scope The scope to which the environment variable collection applies to. - // */ - // readonly environmentVariableCollection: EnvironmentVariableCollection & { getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection }; - // } + export interface ExtensionContext { + /** + * Gets the extension's environment variable collection for this workspace, enabling changes + * to be applied to terminal environment variables. + * + * @deprecated Use {@link getEnvironmentVariableCollection} instead. + */ + readonly environmentVariableCollection: EnvironmentVariableCollection; + /** + * Gets the extension's environment variable collection for this workspace, enabling changes + * to be applied to terminal environment variables. + * + * @param scope The scope to which the environment variable collection applies to. + */ + getEnvironmentVariableCollection(scope?: EnvironmentVariableScope): EnvironmentVariableCollection; + } export type EnvironmentVariableScope = { /** @@ -23,12 +30,4 @@ declare module 'vscode' { */ workspaceFolder?: WorkspaceFolder; }; - - export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { - /** - * A description for the environment variable collection, this will be used to describe the - * changes in the UI. - */ - description: string | MarkdownString | undefined; - } } From 835eab5c131bc2993dd7e0e1b449a11f023c3a0a Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 10 Aug 2023 14:33:06 -0700 Subject: [PATCH 0133/1136] Add setting to control severity of missing package diagnostic. (#21794) Closes https://github.com/microsoft/vscode-python/issues/21792 --- package.json | 15 ++++++ package.nls.json | 3 +- pythonFiles/installed_check.py | 13 +++-- pythonFiles/tests/test_installed_check.py | 53 ++++++++++++++++++- .../creation/common/installCheckUtils.ts | 22 +++++++- .../common/installCheckUtils.unit.test.ts | 51 +++++++++++++++++- 6 files changed, 148 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 2d78d407a0c9..3f98b3d08b6c 100644 --- a/package.json +++ b/package.json @@ -1147,6 +1147,21 @@ "scope": "machine", "type": "string" }, + "python.missingPackage.severity":{ + "default": "Hint", + "description": "%python.missingPackage.severity.description%", + "enum": [ + "Error", + "Hint", + "Information", + "Warning" + ], + "scope": "resource", + "type": "string", + "tags": [ + "experimental" + ] + }, "python.pipenvPath": { "default": "pipenv", "description": "%python.pipenvPath.description%", diff --git a/package.nls.json b/package.nls.json index 79609f02e83a..b8e82b150e76 100644 --- a/package.nls.json +++ b/package.nls.json @@ -200,6 +200,7 @@ "python.linting.pylintPath.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.logging.level.description": "The logging level the extension logs at, defaults to 'error'", "python.logging.level.deprecation": "This setting is deprecated. Please use command `Developer: Set Log Level...` to set logging level.", + "python.missingPackage.severity.description": "Set severity of missing packages in requirements.txt or pyproject.toml", "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", "python.poetryPath.description": "Path to the poetry executable.", "python.sortImports.args.description": "Arguments passed in. Each argument is a separate item in the array.", @@ -265,4 +266,4 @@ "walkthrough.step.python.createNewNotebook.altText": "Creating a new Jupyter notebook", "walkthrough.step.python.openInteractiveWindow.altText": "Opening Python interactive window", "walkthrough.step.python.dataScienceLearnMore.altText": "Image representing our documentation page and mailing list resources." -} \ No newline at end of file +} diff --git a/pythonFiles/installed_check.py b/pythonFiles/installed_check.py index df5d3cb9d082..4a43a8bc8b30 100644 --- a/pythonFiles/installed_check.py +++ b/pythonFiles/installed_check.py @@ -15,7 +15,11 @@ from importlib_metadata import metadata from packaging.requirements import Requirement -DEFAULT_SEVERITY = 3 +DEFAULT_SEVERITY = "3" # 'Hint' +try: + SEVERITY = int(os.getenv("VSCODE_MISSING_PGK_SEVERITY", DEFAULT_SEVERITY)) +except ValueError: + SEVERITY = int(DEFAULT_SEVERITY) def parse_args(argv: Optional[Sequence[str]] = None): @@ -37,7 +41,8 @@ def parse_requirements(line: str) -> Optional[Requirement]: elif req.marker.evaluate(): return req except Exception: - return None + pass + return None def process_requirements(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]: @@ -60,7 +65,7 @@ def process_requirements(req_file: pathlib.Path) -> List[Dict[str, Union[str, in "endCharacter": len(req.name), "package": req.name, "code": "not-installed", - "severity": DEFAULT_SEVERITY, + "severity": SEVERITY, } ) return diagnostics @@ -100,7 +105,7 @@ def process_pyproject(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]] "endCharacter": end, "package": req.name, "code": "not-installed", - "severity": DEFAULT_SEVERITY, + "severity": SEVERITY, } ) return diagnostics diff --git a/pythonFiles/tests/test_installed_check.py b/pythonFiles/tests/test_installed_check.py index f76070d197be..dae019359e08 100644 --- a/pythonFiles/tests/test_installed_check.py +++ b/pythonFiles/tests/test_installed_check.py @@ -9,7 +9,7 @@ import sys import pytest -from typing import Dict, List, Union +from typing import Dict, List, Optional, Union SCRIPT_PATH = pathlib.Path(__file__).parent.parent / "installed_check.py" TEST_DATA = pathlib.Path(__file__).parent / "test_data" @@ -29,7 +29,12 @@ def generate_file(base_file: pathlib.Path): os.unlink(str(fullpath)) -def run_on_file(file_path: pathlib.Path) -> List[Dict[str, Union[str, int]]]: +def run_on_file( + file_path: pathlib.Path, severity: Optional[str] = None +) -> List[Dict[str, Union[str, int]]]: + env = os.environ.copy() + if severity: + env["VSCODE_MISSING_PGK_SEVERITY"] = severity result = subprocess.run( [ sys.executable, @@ -39,6 +44,7 @@ def run_on_file(file_path: pathlib.Path) -> List[Dict[str, Union[str, int]]]: stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, + env=env, ) assert result.returncode == 0 assert result.stderr == b"" @@ -88,3 +94,46 @@ def test_installed_check(test_name: str): with generate_file(base_file) as file_path: result = run_on_file(file_path) assert result == EXPECTED_DATA[test_name] + + +EXPECTED_DATA2 = { + "missing-deps": [ + { + "line": 6, + "character": 0, + "endLine": 6, + "endCharacter": 10, + "package": "flake8-csv", + "code": "not-installed", + "severity": 0, + }, + { + "line": 10, + "character": 0, + "endLine": 10, + "endCharacter": 11, + "package": "levenshtein", + "code": "not-installed", + "severity": 0, + }, + ], + "pyproject-missing-deps": [ + { + "line": 8, + "character": 34, + "endLine": 8, + "endCharacter": 44, + "package": "flake8-csv", + "code": "not-installed", + "severity": 0, + } + ], +} + + +@pytest.mark.parametrize("test_name", EXPECTED_DATA2.keys()) +def test_with_severity(test_name: str): + base_file = TEST_DATA / f"{test_name}.data" + with generate_file(base_file) as file_path: + result = run_on_file(file_path, severity="0") + assert result == EXPECTED_DATA2[test_name] diff --git a/src/client/pythonEnvironments/creation/common/installCheckUtils.ts b/src/client/pythonEnvironments/creation/common/installCheckUtils.ts index 8c9817f83b7a..5d51b6186b11 100644 --- a/src/client/pythonEnvironments/creation/common/installCheckUtils.ts +++ b/src/client/pythonEnvironments/creation/common/installCheckUtils.ts @@ -6,6 +6,7 @@ import { installedCheckScript } from '../../../common/process/internal/scripts'; import { plainExec } from '../../../common/process/rawProcessApis'; import { IInterpreterPathService } from '../../../common/types'; import { traceInfo, traceVerbose, traceError } from '../../../logging'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; interface PackageDiagnostic { package: string; @@ -39,6 +40,21 @@ function parseDiagnostics(data: string): Diagnostic[] { return diagnostics; } +function getMissingPackageSeverity(doc: TextDocument): number { + const config = getConfiguration('python', doc.uri); + const severity: string = config.get('missingPackage.severity', 'Hint'); + if (severity === 'Error') { + return DiagnosticSeverity.Error; + } + if (severity === 'Warning') { + return DiagnosticSeverity.Warning; + } + if (severity === 'Information') { + return DiagnosticSeverity.Information; + } + return DiagnosticSeverity.Hint; +} + export async function getInstalledPackagesDiagnostics( interpreterPathService: IInterpreterPathService, doc: TextDocument, @@ -47,7 +63,11 @@ export async function getInstalledPackagesDiagnostics( const scriptPath = installedCheckScript(); try { traceInfo('Running installed packages checker: ', interpreter, scriptPath, doc.uri.fsPath); - const result = await plainExec(interpreter, [scriptPath, doc.uri.fsPath]); + const result = await plainExec(interpreter, [scriptPath, doc.uri.fsPath], { + env: { + VSCODE_MISSING_PGK_SEVERITY: `${getMissingPackageSeverity(doc)}`, + }, + }); traceVerbose('Installed packages check result:\n', result.stdout); if (result.stderr) { traceError('Installed packages check error:\n', result.stderr); diff --git a/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts b/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts index de8e263fc3fe..4763b54a730a 100644 --- a/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts +++ b/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts @@ -5,10 +5,12 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; import { assert, use as chaiUse } from 'chai'; -import { Diagnostic, TextDocument, Range, Uri } from 'vscode'; +import { Diagnostic, TextDocument, Range, Uri, WorkspaceConfiguration, ConfigurationScope } from 'vscode'; import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; import { getInstalledPackagesDiagnostics } from '../../../../client/pythonEnvironments/creation/common/installCheckUtils'; import { IInterpreterPathService } from '../../../../client/common/types'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import { SpawnOptions } from '../../../../client/common/process/types'; chaiUse(chaiAsPromised); @@ -37,10 +39,20 @@ const MISSING_PACKAGES: Diagnostic[] = [ suite('Install check diagnostics tests', () => { let plainExecStub: sinon.SinonStub; let interpreterPathService: typemoq.IMock; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock; setup(() => { + configMock = typemoq.Mock.ofType(); plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); interpreterPathService = typemoq.Mock.ofType(); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + getConfigurationStub.callsFake((section?: string, _scope?: ConfigurationScope | null) => { + if (section === 'python') { + return configMock.object; + } + return undefined; + }); }); teardown(() => { @@ -48,18 +60,55 @@ suite('Install check diagnostics tests', () => { }); test('Test parse diagnostics', async () => { + configMock + .setup((c) => c.get('missingPackage.severity', 'Hint')) + .returns(() => 'Error') + .verifiable(typemoq.Times.atLeastOnce()); plainExecStub.resolves({ stdout: MISSING_PACKAGES_STR, stderr: '' }); const someFile = getSomeRequirementFile(); const result = await getInstalledPackagesDiagnostics(interpreterPathService.object, someFile.object); assert.deepStrictEqual(result, MISSING_PACKAGES); + configMock.verifyAll(); }); test('Test parse empty diagnostics', async () => { + configMock + .setup((c) => c.get('missingPackage.severity', 'Hint')) + .returns(() => 'Error') + .verifiable(typemoq.Times.atLeastOnce()); plainExecStub.resolves({ stdout: '', stderr: '' }); const someFile = getSomeRequirementFile(); const result = await getInstalledPackagesDiagnostics(interpreterPathService.object, someFile.object); assert.deepStrictEqual(result, []); + configMock.verifyAll(); + }); + + [ + ['Error', '0'], + ['Warning', '1'], + ['Information', '2'], + ['Hint', '3'], + ].forEach((severityType: string[]) => { + const setting = severityType[0]; + const expected = severityType[1]; + test(`Test missing package severity: ${setting}`, async () => { + configMock + .setup((c) => c.get('missingPackage.severity', 'Hint')) + .returns(() => setting) + .verifiable(typemoq.Times.atLeastOnce()); + let severity: string | undefined; + plainExecStub.callsFake((_cmd: string, _args: string[], options: SpawnOptions) => { + severity = options.env?.VSCODE_MISSING_PGK_SEVERITY; + return { stdout: '', stderr: '' }; + }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterPathService.object, someFile.object); + + assert.deepStrictEqual(result, []); + assert.deepStrictEqual(severity, expected); + configMock.verifyAll(); + }); }); }); From ab8d3b22652cd08889839927442d66228d537b4c Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 10 Aug 2023 15:07:08 -0700 Subject: [PATCH 0134/1136] Update VS Code engine (#21799) For #11039 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56b94d1dd44c..84357451dc3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.81.0-20230809" + "vscode": "^1.82.0-20230809" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index 3f98b3d08b6c..fe247f8b25c1 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.81.0-20230809" + "vscode": "^1.82.0-20230809" }, "enableTelemetry": false, "keywords": [ From 71d6dab02ac6bf129e775afe6c5787a080f45e93 Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Fri, 11 Aug 2023 11:46:42 -0700 Subject: [PATCH 0135/1136] Add one more property to load event (#21800) This PR adds app name to the editor_load telemetry event --- src/client/startupTelemetry.ts | 3 +++ src/client/telemetry/index.ts | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/client/startupTelemetry.ts b/src/client/startupTelemetry.ts index f4d3fc254b67..43cf09c23c37 100644 --- a/src/client/startupTelemetry.ts +++ b/src/client/startupTelemetry.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as vscode from 'vscode'; import { IWorkspaceService } from './common/application/types'; import { isTestExecution } from './common/constants'; import { ITerminalHelper } from './common/terminal/types'; @@ -81,6 +82,7 @@ async function getActivationTelemetryProps(serviceContainer: IServiceContainer): // TODO: If any one of these parts fails we send no info. We should // be able to partially populate as much as possible instead // (through granular try-catch statements). + const appName = vscode.env.appName; const workspaceService = serviceContainer.get(IWorkspaceService); const workspaceFolderCount = workspaceService.workspaceFolders?.length || 0; const terminalHelper = serviceContainer.get(ITerminalHelper); @@ -129,5 +131,6 @@ async function getActivationTelemetryProps(serviceContainer: IServiceContainer): hasPythonThree, usingUserDefinedInterpreter, usingGlobalInterpreter, + appName, }; } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index f0b57a4043d9..3fcc177edea7 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -729,6 +729,7 @@ export interface IEventNamePropertyMapping { */ /* __GDPR__ "editor.load" : { + "appName" : {"classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud"}, "codeloadingtime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, "condaversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" }, "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "owner": "luabud" }, @@ -747,6 +748,10 @@ export interface IEventNamePropertyMapping { } */ [EventName.EDITOR_LOAD]: { + /** + * The name of the application where the Python extension is running + */ + appName?: string | undefined; /** * The conda version if selected */ @@ -1549,7 +1554,7 @@ export interface IEventNamePropertyMapping { * This event also has a measure, "resultLength", which records the number of completions provided. */ /* __GDPR__ - "jedi_language_server.request" : { + "jedi_language_server.request" : { "method": {"classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig"} } */ From 385bb370ce21e3b15a92fbb80067f89690cbde74 Mon Sep 17 00:00:00 2001 From: Erik De Bonte Date: Mon, 14 Aug 2023 13:53:56 -0700 Subject: [PATCH 0136/1136] Add `language_server.jinja_usage` to `pylance.ts` (#21809) --- src/client/telemetry/pylance.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index c59d279b98c2..afa6b5298084 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -6,6 +6,12 @@ "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ +/* __GDPR__ + "language_server.jinja_usage" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "openfileextensions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ /* __GDPR__ "language_server.ready" : { "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, From bd749aaae4519b10f493e71f179aa6535c6e987a Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 14 Aug 2023 16:24:16 -0700 Subject: [PATCH 0137/1136] Fix `service.test.ts` to stop disposing of all services (#21811) file `service.test.ts` was calling to dispose of all items related to the service container for clean up. This led to services in later tests failing since they were close already. Fixes here allow for new tests in the test adapter to be written. fix helps https://github.com/microsoft/vscode-python/pull/21803 --- src/test/common/configuration/service.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/test/common/configuration/service.test.ts b/src/test/common/configuration/service.test.ts index ff47500db731..c57617b2a610 100644 --- a/src/test/common/configuration/service.test.ts +++ b/src/test/common/configuration/service.test.ts @@ -2,8 +2,7 @@ // Licensed under the MIT License. import { expect } from 'chai'; import { workspace } from 'vscode'; -import { IConfigurationService, IDisposableRegistry } from '../../../client/common/types'; -import { disposeAll } from '../../../client/common/utils/resourceLifecycle'; +import { IConfigurationService, IDisposableRegistry, IExtensionContext } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { getExtensionSettings } from '../../extensionSettings'; import { initialize } from '../../initialize'; @@ -23,15 +22,17 @@ suite('Configuration Service', () => { test('Ensure async registry works', async () => { const asyncRegistry = serviceContainer.get(IDisposableRegistry); - let disposed = false; + let subs = serviceContainer.get(IExtensionContext).subscriptions; + const oldLength = subs.length; const disposable = { dispose(): Promise { - disposed = true; return Promise.resolve(); }, }; asyncRegistry.push(disposable); - await disposeAll(asyncRegistry); - expect(disposed).to.be.equal(true, "Didn't dispose during async registry cleanup"); + subs = serviceContainer.get(IExtensionContext).subscriptions; + const newLength = subs.length; + expect(newLength).to.be.equal(oldLength + 1, 'Subscription not added'); + // serviceContainer subscriptions are not disposed of as this breaks other tests that use the service container. }); }); From b447bf1cf0439e2eb7c1164220aadc7399a18606 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 14 Aug 2023 17:22:53 -0700 Subject: [PATCH 0138/1136] Feature branch testing overflow bug fix (#21812) This merges in two PRs that were reverted because of a bug introduced that caused subprocess overflow. reverted PRs: https://github.com/microsoft/vscode-python/pull/21667, https://github.com/microsoft/vscode-python/pull/21682 This now implements these two PRs allowing for absolute testIds and an execObservable for the subprocess. This PR also adds a bug fix and functional tests to ensure this doesn't happen again. Since this PR is large, all items in it have already been reviewed as they were merged into the feature branch. --- .../pytestadapter/.data/root/tests/pytest.ini | 0 .../pytestadapter/.data/root/tests/test_a.py | 6 + .../pytestadapter/.data/root/tests/test_b.py | 6 + .../expected_discovery_test_output.py | 516 +++++++++++++----- .../expected_execution_test_output.py | 365 ++++++++++--- pythonFiles/tests/pytestadapter/helpers.py | 16 +- .../tests/pytestadapter/test_discovery.py | 52 +- .../tests/pytestadapter/test_execution.py | 45 +- pythonFiles/vscode_pytest/__init__.py | 58 +- .../testing/testController/common/server.ts | 36 +- .../testing/testController/common/types.ts | 7 +- .../pytest/pytestDiscoveryAdapter.ts | 48 +- .../pytest/pytestExecutionAdapter.ts | 64 ++- .../unittest/testDiscoveryAdapter.ts | 20 +- .../unittest/testExecutionAdapter.ts | 22 +- src/test/linters/lint.functional.test.ts | 6 - src/test/mocks/helper.ts | 11 + src/test/mocks/mockChildProcess.ts | 239 ++++++++ .../testing/common/testingAdapter.test.ts | 428 +++++++++++++++ .../pytestDiscoveryAdapter.unit.test.ts | 76 ++- .../pytestExecutionAdapter.unit.test.ts | 118 +++- .../testController/server.unit.test.ts | 248 ++++++--- .../workspaceTestAdapter.unit.test.ts | 3 +- .../test_parameterized_subtest.py | 16 + .../smallWorkspace/test_simple.py | 12 + 25 files changed, 1993 insertions(+), 425 deletions(-) create mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini create mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py create mode 100644 pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py create mode 100644 src/test/mocks/helper.ts create mode 100644 src/test/mocks/mockChildProcess.ts create mode 100644 src/test/testing/common/testingAdapter.test.ts create mode 100644 src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py create mode 100644 src/testTestingRootWkspc/smallWorkspace/test_simple.py diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini b/pythonFiles/tests/pytestadapter/.data/root/tests/pytest.ini new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py b/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py new file mode 100644 index 000000000000..3ec3dd9626cb --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/root/tests/test_a.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def test_a_function(): # test_marker--test_a_function + assert True diff --git a/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py b/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py new file mode 100644 index 000000000000..0d3148641f85 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/root/tests/test_b.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def test_b_function(): # test_marker--test_b_function + assert True diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 91c1453dfc77..2b2c07ab8ea7 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -1,6 +1,7 @@ import os -from .helpers import TEST_DATA_PATH, find_test_line_number + +from .helpers import TEST_DATA_PATH, find_test_line_number, get_absolute_test_id # This file contains the expected output dictionaries for tests discovery and is used in test_discovery.py. @@ -18,7 +19,7 @@ # This is the expected output for the simple_pytest.py file. # └── simple_pytest.py # └── test_function -simple_test_file_path = os.fspath(TEST_DATA_PATH / "simple_pytest.py") +simple_test_file_path = TEST_DATA_PATH / "simple_pytest.py" simple_discovery_pytest_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -26,20 +27,24 @@ "children": [ { "name": "simple_pytest.py", - "path": simple_test_file_path, + "path": os.fspath(simple_test_file_path), "type_": "file", - "id_": simple_test_file_path, + "id_": os.fspath(simple_test_file_path), "children": [ { "name": "test_function", - "path": simple_test_file_path, + "path": os.fspath(simple_test_file_path), "lineno": find_test_line_number( "test_function", simple_test_file_path, ), "type_": "test", - "id_": "simple_pytest.py::test_function", - "runID": "simple_pytest.py::test_function", + "id_": get_absolute_test_id( + "simple_pytest.py::test_function", simple_test_file_path + ), + "runID": get_absolute_test_id( + "simple_pytest.py::test_function", simple_test_file_path + ), } ], } @@ -52,7 +57,7 @@ # ├── TestExample # │ └── test_true_unittest # └── test_true_pytest -unit_pytest_same_file_path = os.fspath(TEST_DATA_PATH / "unittest_pytest_same_file.py") +unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" unit_pytest_same_file_discovery_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -60,39 +65,51 @@ "children": [ { "name": "unittest_pytest_same_file.py", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "type_": "file", - "id_": unit_pytest_same_file_path, + "id_": os.fspath(unit_pytest_same_file_path), "children": [ { "name": "TestExample", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "type_": "class", "children": [ { "name": "test_true_unittest", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "lineno": find_test_line_number( "test_true_unittest", - unit_pytest_same_file_path, + os.fspath(unit_pytest_same_file_path), ), "type_": "test", - "id_": "unittest_pytest_same_file.py::TestExample::test_true_unittest", - "runID": "unittest_pytest_same_file.py::TestExample::test_true_unittest", + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), + "runID": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), } ], "id_": "unittest_pytest_same_file.py::TestExample", }, { "name": "test_true_pytest", - "path": unit_pytest_same_file_path, + "path": os.fspath(unit_pytest_same_file_path), "lineno": find_test_line_number( "test_true_pytest", unit_pytest_same_file_path, ), "type_": "test", - "id_": "unittest_pytest_same_file.py::test_true_pytest", - "runID": "unittest_pytest_same_file.py::test_true_pytest", + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), + "runID": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), }, ], } @@ -124,9 +141,9 @@ # └── test_subtract_positive_numbers # │ └── TestDuplicateFunction # │ └── test_dup_s -unittest_folder_path = os.fspath(TEST_DATA_PATH / "unittest_folder") -test_add_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_add.py") -test_subtract_path = os.fspath(TEST_DATA_PATH / "unittest_folder" / "test_subtract.py") +unittest_folder_path = TEST_DATA_PATH / "unittest_folder" +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" unittest_folder_discovery_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -134,61 +151,79 @@ "children": [ { "name": "unittest_folder", - "path": unittest_folder_path, + "path": os.fspath(unittest_folder_path), "type_": "folder", - "id_": unittest_folder_path, + "id_": os.fspath(unittest_folder_path), "children": [ { "name": "test_add.py", - "path": test_add_path, + "path": os.fspath(test_add_path), "type_": "file", - "id_": test_add_path, + "id_": os.fspath(test_add_path), "children": [ { "name": "TestAddFunction", - "path": test_add_path, + "path": os.fspath(test_add_path), "type_": "class", "children": [ { "name": "test_add_negative_numbers", - "path": test_add_path, + "path": os.fspath(test_add_path), "lineno": find_test_line_number( "test_add_negative_numbers", - test_add_path, + os.fspath(test_add_path), ), "type_": "test", - "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", - "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), }, { "name": "test_add_positive_numbers", - "path": test_add_path, + "path": os.fspath(test_add_path), "lineno": find_test_line_number( "test_add_positive_numbers", - test_add_path, + os.fspath(test_add_path), ), "type_": "test", - "id_": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", - "runID": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), }, ], "id_": "unittest_folder/test_add.py::TestAddFunction", }, { "name": "TestDuplicateFunction", - "path": test_add_path, + "path": os.fspath(test_add_path), "type_": "class", "children": [ { "name": "test_dup_a", - "path": test_add_path, + "path": os.fspath(test_add_path), "lineno": find_test_line_number( "test_dup_a", - test_add_path, + os.fspath(test_add_path), ), "type_": "test", - "id_": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", - "runID": "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), }, ], "id_": "unittest_folder/test_add.py::TestDuplicateFunction", @@ -197,55 +232,73 @@ }, { "name": "test_subtract.py", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "type_": "file", - "id_": test_subtract_path, + "id_": os.fspath(test_subtract_path), "children": [ { "name": "TestSubtractFunction", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "type_": "class", "children": [ { "name": "test_subtract_negative_numbers", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "lineno": find_test_line_number( "test_subtract_negative_numbers", - test_subtract_path, + os.fspath(test_subtract_path), ), "type_": "test", - "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", - "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), }, { "name": "test_subtract_positive_numbers", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "lineno": find_test_line_number( "test_subtract_positive_numbers", - test_subtract_path, + os.fspath(test_subtract_path), ), "type_": "test", - "id_": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", - "runID": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), }, ], "id_": "unittest_folder/test_subtract.py::TestSubtractFunction", }, { "name": "TestDuplicateFunction", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "type_": "class", "children": [ { "name": "test_dup_s", - "path": test_subtract_path, + "path": os.fspath(test_subtract_path), "lineno": find_test_line_number( "test_dup_s", - test_subtract_path, + os.fspath(test_subtract_path), ), "type_": "test", - "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", - "runID": "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), }, ], "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction", @@ -268,20 +321,23 @@ # └── test_bottom_folder.py # └── test_bottom_function_t # └── test_bottom_function_f -dual_level_nested_folder_path = os.fspath(TEST_DATA_PATH / "dual_level_nested_folder") -test_top_folder_path = os.fspath( +dual_level_nested_folder_path = TEST_DATA_PATH / "dual_level_nested_folder" +test_top_folder_path = ( TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" ) -test_nested_folder_one_path = os.fspath( + +test_nested_folder_one_path = ( TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" ) -test_bottom_folder_path = os.fspath( + +test_bottom_folder_path = ( TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" ) + dual_level_nested_folder_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -289,73 +345,97 @@ "children": [ { "name": "dual_level_nested_folder", - "path": dual_level_nested_folder_path, + "path": os.fspath(dual_level_nested_folder_path), "type_": "folder", - "id_": dual_level_nested_folder_path, + "id_": os.fspath(dual_level_nested_folder_path), "children": [ { "name": "test_top_folder.py", - "path": test_top_folder_path, + "path": os.fspath(test_top_folder_path), "type_": "file", - "id_": test_top_folder_path, + "id_": os.fspath(test_top_folder_path), "children": [ { "name": "test_top_function_t", - "path": test_top_folder_path, + "path": os.fspath(test_top_folder_path), "lineno": find_test_line_number( "test_top_function_t", test_top_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", - "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "id_": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + test_top_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + test_top_folder_path, + ), }, { "name": "test_top_function_f", - "path": test_top_folder_path, + "path": os.fspath(test_top_folder_path), "lineno": find_test_line_number( "test_top_function_f", test_top_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", - "runID": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "id_": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + test_top_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + test_top_folder_path, + ), }, ], }, { "name": "nested_folder_one", - "path": test_nested_folder_one_path, + "path": os.fspath(test_nested_folder_one_path), "type_": "folder", - "id_": test_nested_folder_one_path, + "id_": os.fspath(test_nested_folder_one_path), "children": [ { "name": "test_bottom_folder.py", - "path": test_bottom_folder_path, + "path": os.fspath(test_bottom_folder_path), "type_": "file", - "id_": test_bottom_folder_path, + "id_": os.fspath(test_bottom_folder_path), "children": [ { "name": "test_bottom_function_t", - "path": test_bottom_folder_path, + "path": os.fspath(test_bottom_folder_path), "lineno": find_test_line_number( "test_bottom_function_t", test_bottom_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", - "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "id_": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + test_bottom_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + test_bottom_folder_path, + ), }, { "name": "test_bottom_function_f", - "path": test_bottom_folder_path, + "path": os.fspath(test_bottom_folder_path), "lineno": find_test_line_number( "test_bottom_function_f", test_bottom_folder_path, ), "type_": "test", - "id_": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", - "runID": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "id_": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + test_bottom_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + test_bottom_folder_path, + ), }, ], } @@ -374,12 +454,10 @@ # └── test_nest.py # └── test_function -folder_a_path = os.fspath(TEST_DATA_PATH / "folder_a") -folder_b_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b") -folder_a_nested_path = os.fspath(TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a") -test_nest_path = os.fspath( - TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" -) +folder_a_path = TEST_DATA_PATH / "folder_a" +folder_b_path = TEST_DATA_PATH / "folder_a" / "folder_b" +folder_a_nested_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" +test_nest_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" double_nested_folder_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -387,38 +465,44 @@ "children": [ { "name": "folder_a", - "path": folder_a_path, + "path": os.fspath(folder_a_path), "type_": "folder", - "id_": folder_a_path, + "id_": os.fspath(folder_a_path), "children": [ { "name": "folder_b", - "path": folder_b_path, + "path": os.fspath(folder_b_path), "type_": "folder", - "id_": folder_b_path, + "id_": os.fspath(folder_b_path), "children": [ { "name": "folder_a", - "path": folder_a_nested_path, + "path": os.fspath(folder_a_nested_path), "type_": "folder", - "id_": folder_a_nested_path, + "id_": os.fspath(folder_a_nested_path), "children": [ { "name": "test_nest.py", - "path": test_nest_path, + "path": os.fspath(test_nest_path), "type_": "file", - "id_": test_nest_path, + "id_": os.fspath(test_nest_path), "children": [ { "name": "test_function", - "path": test_nest_path, + "path": os.fspath(test_nest_path), "lineno": find_test_line_number( "test_function", test_nest_path, ), "type_": "test", - "id_": "folder_a/folder_b/folder_a/test_nest.py::test_function", - "runID": "folder_a/folder_b/folder_a/test_nest.py::test_function", + "id_": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", + test_nest_path, + ), + "runID": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", + test_nest_path, + ), } ], } @@ -438,7 +522,7 @@ # └── [3+5-8] # └── [2+4-6] # └── [6+9-16] -parameterize_tests_path = os.fspath(TEST_DATA_PATH / "parametrize_tests.py") +parameterize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" parametrize_tests_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -446,77 +530,107 @@ "children": [ { "name": "parametrize_tests.py", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "type_": "file", - "id_": parameterize_tests_path, + "id_": os.fspath(parameterize_tests_path), "children": [ { "name": "test_adding", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "type_": "function", "id_": "parametrize_tests.py::test_adding", "children": [ { "name": "[3+5-8]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_adding[3+5-8]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_adding[3+5-8]", - "runID": "parametrize_tests.py::test_adding[3+5-8]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", + parameterize_tests_path, + ), }, { "name": "[2+4-6]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_adding[2+4-6]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_adding[2+4-6]", - "runID": "parametrize_tests.py::test_adding[2+4-6]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", + parameterize_tests_path, + ), }, { "name": "[6+9-16]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_adding[6+9-16]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_adding[6+9-16]", - "runID": "parametrize_tests.py::test_adding[6+9-16]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", + parameterize_tests_path, + ), }, ], }, { "name": "test_under_ten", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "type_": "function", "children": [ { "name": "[1]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_under_ten[1]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_under_ten[1]", - "runID": "parametrize_tests.py::test_under_ten[1]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[1]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[1]", + parameterize_tests_path, + ), }, { "name": "[2]", - "path": parameterize_tests_path, + "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( "test_under_ten[2]", parameterize_tests_path, ), "type_": "test", - "id_": "parametrize_tests.py::test_under_ten[2]", - "runID": "parametrize_tests.py::test_under_ten[2]", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[2]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_under_ten[2]", + parameterize_tests_path, + ), }, ], "id_": "parametrize_tests.py::test_under_ten", @@ -529,7 +643,7 @@ # This is the expected output for the text_docstring.txt tests. # └── text_docstring.txt -text_docstring_path = os.fspath(TEST_DATA_PATH / "text_docstring.txt") +text_docstring_path = TEST_DATA_PATH / "text_docstring.txt" doctest_pytest_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -537,20 +651,24 @@ "children": [ { "name": "text_docstring.txt", - "path": text_docstring_path, + "path": os.fspath(text_docstring_path), "type_": "file", - "id_": text_docstring_path, + "id_": os.fspath(text_docstring_path), "children": [ { "name": "text_docstring.txt", - "path": text_docstring_path, + "path": os.fspath(text_docstring_path), "lineno": find_test_line_number( "text_docstring.txt", - text_docstring_path, + os.fspath(text_docstring_path), ), "type_": "test", - "id_": "text_docstring.txt::text_docstring.txt", - "runID": "text_docstring.txt::text_docstring.txt", + "id_": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", text_docstring_path + ), + "runID": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", text_docstring_path + ), } ], } @@ -570,8 +688,8 @@ # └── [1] # └── [2] # └── [3] -param1_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param1.py") -param2_path = os.fspath(TEST_DATA_PATH / "param_same_name" / "test_param2.py") +param1_path = TEST_DATA_PATH / "param_same_name" / "test_param1.py" +param2_path = TEST_DATA_PATH / "param_same_name" / "test_param2.py" param_same_name_expected_output = { "name": ".data", "path": TEST_DATA_PATH_STR, @@ -585,38 +703,56 @@ "children": [ { "name": "test_param1.py", - "path": param1_path, + "path": os.fspath(param1_path), "type_": "file", - "id_": param1_path, + "id_": os.fspath(param1_path), "children": [ { "name": "test_odd_even", - "path": param1_path, + "path": os.fspath(param1_path), "type_": "function", "children": [ { "name": "[a]", - "path": param1_path, + "path": os.fspath(param1_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param1.py::test_odd_even[a]", - "runID": "param_same_name/test_param1.py::test_odd_even[a]", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[a]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[a]", + param1_path, + ), }, { "name": "[b]", - "path": param1_path, + "path": os.fspath(param1_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param1.py::test_odd_even[b]", - "runID": "param_same_name/test_param1.py::test_odd_even[b]", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[b]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[b]", + param1_path, + ), }, { "name": "[c]", - "path": param1_path, + "path": os.fspath(param1_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param1.py::test_odd_even[c]", - "runID": "param_same_name/test_param1.py::test_odd_even[c]", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[c]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[c]", + param1_path, + ), }, ], "id_": "param_same_name/test_param1.py::test_odd_even", @@ -625,38 +761,56 @@ }, { "name": "test_param2.py", - "path": param2_path, + "path": os.fspath(param2_path), "type_": "file", - "id_": param2_path, + "id_": os.fspath(param2_path), "children": [ { "name": "test_odd_even", - "path": param2_path, + "path": os.fspath(param2_path), "type_": "function", "children": [ { "name": "[1]", - "path": param2_path, + "path": os.fspath(param2_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param2.py::test_odd_even[1]", - "runID": "param_same_name/test_param2.py::test_odd_even[1]", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[1]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[1]", + param2_path, + ), }, { "name": "[2]", - "path": param2_path, + "path": os.fspath(param2_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param2.py::test_odd_even[2]", - "runID": "param_same_name/test_param2.py::test_odd_even[2]", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[2]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[2]", + param2_path, + ), }, { "name": "[3]", - "path": param2_path, + "path": os.fspath(param2_path), "lineno": "6", "type_": "test", - "id_": "param_same_name/test_param2.py::test_odd_even[3]", - "runID": "param_same_name/test_param2.py::test_odd_even[3]", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[3]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[3]", + param2_path, + ), }, ], "id_": "param_same_name/test_param2.py::test_odd_even", @@ -668,3 +822,67 @@ ], "id_": TEST_DATA_PATH_STR, } + +tests_path = TEST_DATA_PATH / "root" / "tests" +tests_a_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" +tests_b_path = TEST_DATA_PATH / "root" / "tests" / "test_b.py" +# This is the expected output for the root folder tests. +# └── tests +# └── test_a.py +# └── test_a_function +# └── test_b.py +# └── test_b_function +root_with_config_expected_output = { + "name": "tests", + "path": os.fspath(tests_path), + "type_": "folder", + "children": [ + { + "name": "test_a.py", + "path": os.fspath(tests_a_path), + "type_": "file", + "id_": os.fspath(tests_a_path), + "children": [ + { + "name": "test_a_function", + "path": os.fspath(os.path.join(tests_path, "test_a.py")), + "lineno": find_test_line_number( + "test_a_function", + os.path.join(tests_path, "test_a.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "tests/test_a.py::test_a_function", tests_a_path + ), + "runID": get_absolute_test_id( + "tests/test_a.py::test_a_function", tests_a_path + ), + } + ], + }, + { + "name": "test_b.py", + "path": os.fspath(tests_b_path), + "type_": "file", + "id_": os.fspath(tests_b_path), + "children": [ + { + "name": "test_b_function", + "path": os.fspath(os.path.join(tests_path, "test_b.py")), + "lineno": find_test_line_number( + "test_b_function", + os.path.join(tests_path, "test_b.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "tests/test_b.py::test_b_function", tests_b_path + ), + "runID": get_absolute_test_id( + "tests/test_b.py::test_b_function", tests_b_path + ), + } + ], + }, + ], + "id_": os.fspath(tests_path), +} diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index dd8f458d792e..76d21b3e2518 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -1,14 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from .helpers import TEST_DATA_PATH, get_absolute_test_id TEST_SUBTRACT_FUNCTION = "unittest_folder/test_subtract.py::TestSubtractFunction::" TEST_ADD_FUNCTION = "unittest_folder/test_add.py::TestAddFunction::" SUCCESS = "success" FAILURE = "failure" -TEST_SUBTRACT_FUNCTION_NEGATIVE_NUMBERS_ERROR = "self = \n\n def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers\n self,\n ):\n result = subtract(-2, -3)\n> self.assertEqual(result, 100000)\nE AssertionError: 1 != 100000\n\nunittest_folder/test_subtract.py:25: AssertionError" # noqa: E501 - - # This is the expected output for the unittest_folder execute tests # └── unittest_folder # ├── test_add.py @@ -19,30 +17,52 @@ # └── TestSubtractFunction # ├── test_subtract_negative_numbers: failure # └── test_subtract_positive_numbers: success +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" uf_execution_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers": { - "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + test_subtract_path, + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + test_subtract_path, + ), "outcome": FAILURE, "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers": { - "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ), "outcome": SUCCESS, "message": None, "traceback": None, @@ -57,16 +77,26 @@ # │ └── TestAddFunction # │ ├── test_add_negative_numbers: success # │ └── test_add_positive_numbers: success +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" + uf_single_file_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, @@ -74,19 +104,24 @@ }, } + # This is the expected output for the unittest_folder execute only signle method # └── unittest_folder # ├── test_add.py # │ └── TestAddFunction # │ └── test_add_positive_numbers: success uf_single_method_execution_expected_output = { - f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { - "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, - } + }, } # This is the expected output for the unittest_folder tests run where two tests @@ -98,18 +133,28 @@ # └── test_subtract.py # └── TestSubtractFunction # └── test_subtract_positive_numbers: success +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" + uf_non_adjacent_tests_execution_expected_output = { - TEST_SUBTRACT_FUNCTION - + "test_subtract_positive_numbers": { - "test": TEST_SUBTRACT_FUNCTION + "test_subtract_positive_numbers", + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", test_subtract_path + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ), "outcome": SUCCESS, "message": None, "traceback": None, "subtest": None, }, - TEST_ADD_FUNCTION - + "test_add_positive_numbers": { - "test": TEST_ADD_FUNCTION + "test_add_positive_numbers", + get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), "outcome": SUCCESS, "message": None, "traceback": None, @@ -117,12 +162,15 @@ }, } + # This is the expected output for the simple_pytest.py file. # └── simple_pytest.py # └── test_function: success +simple_pytest_path = TEST_DATA_PATH / "unittest_folder" / "simple_pytest.py" + simple_execution_pytest_expected_output = { - "simple_pytest.py::test_function": { - "test": "simple_pytest.py::test_function", + get_absolute_test_id("test_function", simple_pytest_path): { + "test": get_absolute_test_id("test_function", simple_pytest_path), "outcome": "success", "message": None, "traceback": None, @@ -130,21 +178,34 @@ } } + # This is the expected output for the unittest_pytest_same_file.py file. # ├── unittest_pytest_same_file.py # ├── TestExample # │ └── test_true_unittest: success # └── test_true_pytest: success +unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" unit_pytest_same_file_execution_expected_output = { - "unittest_pytest_same_file.py::TestExample::test_true_unittest": { - "test": "unittest_pytest_same_file.py::TestExample::test_true_unittest", + get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ): { + "test": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "unittest_pytest_same_file.py::test_true_pytest": { - "test": "unittest_pytest_same_file.py::test_true_pytest", + get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", unit_pytest_same_file_path + ): { + "test": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), "outcome": "success", "message": None, "traceback": None, @@ -156,9 +217,15 @@ # └── error_raise_exception.py # ├── TestSomething # │ └── test_a: failure +error_raised_exception_path = TEST_DATA_PATH / "error_raise_exception.py" error_raised_exception_execution_expected_output = { - "error_raise_exception.py::TestSomething::test_a": { - "test": "error_raise_exception.py::TestSomething::test_a", + get_absolute_test_id( + "error_raise_exception.py::TestSomething::test_a", error_raised_exception_path + ): { + "test": get_absolute_test_id( + "error_raise_exception.py::TestSomething::test_a", + error_raised_exception_path, + ), "outcome": "error", "message": "ERROR MESSAGE", "traceback": "TRACEBACK", @@ -174,44 +241,60 @@ # ├── TestClass # │ └── test_class_function_a: skipped # │ └── test_class_function_b: skipped + +skip_tests_path = TEST_DATA_PATH / "skip_tests.py" skip_tests_execution_expected_output = { - "skip_tests.py::test_something": { - "test": "skip_tests.py::test_something", + get_absolute_test_id("skip_tests.py::test_something", skip_tests_path): { + "test": get_absolute_test_id("skip_tests.py::test_something", skip_tests_path), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::test_another_thing": { - "test": "skip_tests.py::test_another_thing", + get_absolute_test_id("skip_tests.py::test_another_thing", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::test_another_thing", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::test_decorator_thing": { - "test": "skip_tests.py::test_decorator_thing", + get_absolute_test_id("skip_tests.py::test_decorator_thing", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::test_decorator_thing", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::test_decorator_thing_2": { - "test": "skip_tests.py::test_decorator_thing_2", + get_absolute_test_id("skip_tests.py::test_decorator_thing_2", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::test_decorator_thing_2", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::TestClass::test_class_function_a": { - "test": "skip_tests.py::TestClass::test_class_function_a", + get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_a", skip_tests_path + ): { + "test": get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_a", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, "subtest": None, }, - "skip_tests.py::TestClass::test_class_function_b": { - "test": "skip_tests.py::TestClass::test_class_function_b", + get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_b", skip_tests_path + ): { + "test": get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_b", skip_tests_path + ), "outcome": "skipped", "message": None, "traceback": None, @@ -229,30 +312,59 @@ # └── test_bottom_folder.py # └── test_bottom_function_t: success # └── test_bottom_function_f: failure +dual_level_nested_folder_top_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" +) +dual_level_nested_folder_bottom_path = ( + TEST_DATA_PATH + / "dual_level_nested_folder" + / "nested_folder_one" + / "test_bottom_folder.py" +) dual_level_nested_folder_execution_expected_output = { - "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + get_absolute_test_id( + "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path + ): { + "test": get_absolute_test_id( + "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + get_absolute_test_id( + "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path + ): { + "test": get_absolute_test_id( + "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + dual_level_nested_folder_bottom_path, + ): { + "test": get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + dual_level_nested_folder_bottom_path, + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + dual_level_nested_folder_bottom_path, + ): { + "test": get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + dual_level_nested_folder_bottom_path, + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, @@ -266,38 +378,59 @@ # └── folder_a # └── test_nest.py # └── test_function: success + +nested_folder_path = ( + TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +) double_nested_folder_expected_execution_output = { - "folder_a/folder_b/folder_a/test_nest.py::test_function": { - "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", + get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path + ): { + "test": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, } } - # This is the expected output for the nested_folder tests. # └── parametrize_tests.py # └── test_adding[3+5-8]: success # └── test_adding[2+4-6]: success # └── test_adding[6+9-16]: failure +parametrize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" + parametrize_tests_expected_execution_output = { - "parametrize_tests.py::test_adding[3+5-8]": { - "test": "parametrize_tests.py::test_adding[3+5-8]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "parametrize_tests.py::test_adding[2+4-6]": { - "test": "parametrize_tests.py::test_adding[2+4-6]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[2+4-6]", parametrize_tests_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "parametrize_tests.py::test_adding[6+9-16]": { - "test": "parametrize_tests.py::test_adding[6+9-16]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[6+9-16]", parametrize_tests_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, @@ -309,8 +442,12 @@ # └── parametrize_tests.py # └── test_adding[3+5-8]: success single_parametrize_tests_expected_execution_output = { - "parametrize_tests.py::test_adding[3+5-8]": { - "test": "parametrize_tests.py::test_adding[3+5-8]", + get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::test_adding[3+5-8]", parametrize_tests_path + ), "outcome": "success", "message": None, "traceback": None, @@ -321,9 +458,12 @@ # This is the expected output for the single parameterized tests. # └── text_docstring.txt # └── text_docstring: success +doc_test_path = TEST_DATA_PATH / "text_docstring.txt" doctest_pytest_expected_execution_output = { - "text_docstring.txt::text_docstring.txt": { - "test": "text_docstring.txt::text_docstring.txt", + get_absolute_test_id("text_docstring.txt::text_docstring.txt", doc_test_path): { + "test": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", doc_test_path + ), "outcome": "success", "message": None, "traceback": None, @@ -332,68 +472,127 @@ } # Will run all tests in the cwd that fit the test file naming pattern. +folder_a_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +dual_level_nested_folder_top_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" +) +dual_level_nested_folder_bottom_path = ( + TEST_DATA_PATH + / "dual_level_nested_folder" + / "nested_folder_one" + / "test_bottom_folder.py" +) +unittest_folder_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +unittest_folder_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" + no_test_ids_pytest_execution_expected_output = { - "folder_a/folder_b/folder_a/test_nest.py::test_function": { - "test": "folder_a/folder_b/folder_a/test_nest.py::test_function", + get_absolute_test_id("test_function", folder_a_path): { + "test": get_absolute_test_id("test_function", folder_a_path), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + get_absolute_test_id("test_top_function_t", dual_level_nested_folder_top_path): { + "test": get_absolute_test_id( + "test_top_function_t", dual_level_nested_folder_top_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { - "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + get_absolute_test_id("test_top_function_f", dual_level_nested_folder_top_path): { + "test": get_absolute_test_id( + "test_top_function_f", dual_level_nested_folder_top_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + get_absolute_test_id( + "test_bottom_function_t", dual_level_nested_folder_bottom_path + ): { + "test": get_absolute_test_id( + "test_bottom_function_t", dual_level_nested_folder_bottom_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { - "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + get_absolute_test_id( + "test_bottom_function_f", dual_level_nested_folder_bottom_path + ): { + "test": get_absolute_test_id( + "test_bottom_function_f", dual_level_nested_folder_bottom_path + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers": { - "test": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + get_absolute_test_id( + "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path + ): { + "test": get_absolute_test_id( + "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers": { - "test": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + get_absolute_test_id( + "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path + ): { + "test": get_absolute_test_id( + "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers": { - "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + get_absolute_test_id( + "TestSubtractFunction::test_subtract_negative_numbers", + unittest_folder_subtract_path, + ): { + "test": get_absolute_test_id( + "TestSubtractFunction::test_subtract_negative_numbers", + unittest_folder_subtract_path, + ), "outcome": "failure", "message": "ERROR MESSAGE", "traceback": None, "subtest": None, }, - "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers": { - "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + get_absolute_test_id( + "TestSubtractFunction::test_subtract_positive_numbers", + unittest_folder_subtract_path, + ): { + "test": get_absolute_test_id( + "TestSubtractFunction::test_subtract_positive_numbers", + unittest_folder_subtract_path, + ), "outcome": "success", "message": None, "traceback": None, "subtest": None, }, } + +# This is the expected output for the root folder with the config file referenced. +# └── test_a.py +# └── test_a_function: success +test_add_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" +config_file_pytest_expected_execution_output = { + get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path): { + "test": get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index 47b4f75d6d60..7195cfe43ea5 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -16,6 +16,13 @@ from typing_extensions import TypedDict +def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: + split_id = test_id.split("::")[1:] + absolute_test_id = "::".join([str(testPath), *split_id]) + print("absolute path", absolute_test_id) + return absolute_test_id + + def create_server( host: str = "127.0.0.1", port: int = 0, @@ -104,6 +111,13 @@ def process_rpc_json(data: str) -> List[Dict[str, Any]]: def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: + """Run the pytest discovery and return the JSON data from the server.""" + return runner_with_cwd(args, TEST_DATA_PATH) + + +def runner_with_cwd( + args: List[str], path: pathlib.Path +) -> Optional[List[Dict[str, Any]]]: """Run the pytest discovery and return the JSON data from the server.""" process_args: List[str] = [ sys.executable, @@ -134,7 +148,7 @@ def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: t2 = threading.Thread( target=_run_test_code, - args=(process_args, env, TEST_DATA_PATH, completed), + args=(process_args, env, path, completed), ) t2.start() diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 5288c7ad769e..8d785be27c8b 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -7,7 +7,7 @@ import pytest from . import expected_discovery_test_output -from .helpers import TEST_DATA_PATH, runner +from .helpers import TEST_DATA_PATH, runner, runner_with_cwd def test_import_error(tmp_path): @@ -153,3 +153,53 @@ def test_pytest_collect(file, expected_const): assert actual["status"] == "success" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) assert actual["tests"] == expected_const + + +def test_pytest_root_dir(): + """ + Test to test pytest discovery with the command line arg --rootdir specified to be a subfolder + of the workspace root. Discovery should succeed and testids should be relative to workspace root. + """ + rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" + actual = runner_with_cwd( + [ + "--collect-only", + rd, + ], + TEST_DATA_PATH / "root", + ) + if actual: + actual = actual[0] + assert actual + assert all(item in actual for item in ("status", "cwd", "tests")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert ( + actual["tests"] + == expected_discovery_test_output.root_with_config_expected_output + ) + + +def test_pytest_config_file(): + """ + Test to test pytest discovery with the command line arg -c with a specified config file which + changes the workspace root. Discovery should succeed and testids should be relative to workspace root. + """ + actual = runner_with_cwd( + [ + "--collect-only", + "-c", + "tests/pytest.ini", + ], + TEST_DATA_PATH / "root", + ) + if actual: + actual = actual[0] + assert actual + assert all(item in actual for item in ("status", "cwd", "tests")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert ( + actual["tests"] + == expected_discovery_test_output.root_with_config_expected_output + ) diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index e3b00386882d..07354b01709b 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -7,7 +7,50 @@ from tests.pytestadapter import expected_execution_test_output -from .helpers import TEST_DATA_PATH, runner +from .helpers import TEST_DATA_PATH, runner, runner_with_cwd + + +def test_config_file(): + """Test pytest execution when a config file is specified.""" + args = [ + "-c", + "tests/pytest.ini", + str(TEST_DATA_PATH / "root" / "tests" / "test_a.py::test_a_function"), + ] + new_cwd = TEST_DATA_PATH / "root" + actual = runner_with_cwd(args, new_cwd) + expected_const = ( + expected_execution_test_output.config_file_pytest_expected_execution_output + ) + assert actual + assert len(actual) == len(expected_const) + actual_result_dict = dict() + for a in actual: + assert all(item in a for item in ("status", "cwd", "result")) + assert a["status"] == "success" + assert a["cwd"] == os.fspath(new_cwd) + actual_result_dict.update(a["result"]) + assert actual_result_dict == expected_const + + +def test_rootdir_specified(): + """Test pytest execution when a --rootdir is specified.""" + rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" + args = [rd, "tests/test_a.py::test_a_function"] + new_cwd = TEST_DATA_PATH / "root" + actual = runner_with_cwd(args, new_cwd) + expected_const = ( + expected_execution_test_output.config_file_pytest_expected_execution_output + ) + assert actual + assert len(actual) == len(expected_const) + actual_result_dict = dict() + for a in actual: + assert all(item in a for item in ("status", "cwd", "result")) + assert a["status"] == "success" + assert a["cwd"] == os.fspath(new_cwd) + actual_result_dict.update(a["result"]) + assert actual_result_dict == expected_const def test_syntax_error_execution(tmp_path): diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 1ac287a8410a..49d429662e3a 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -69,8 +69,7 @@ def pytest_exception_interact(node, call, report): """ # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. # call.excinfo.exconly() returns the exception as a string. - # See if it is during discovery or execution. - # if discovery, then add the error to error logs. + # If it is during discovery, then add the error to error logs. if type(report) == pytest.CollectReport: if call.excinfo and call.excinfo.typename != "AssertionError": if report.outcome == "skipped" and "SkipTest" in str(call): @@ -83,11 +82,11 @@ def pytest_exception_interact(node, call, report): report.longreprtext + "\n Check Python Test Logs for more details." ) else: - # if execution, send this data that the given node failed. + # If during execution, send this data that the given node failed. report_value = "error" if call.excinfo.typename == "AssertionError": report_value = "failure" - node_id = str(node.nodeid) + node_id = get_absolute_test_id(node.nodeid, get_node_path(node)) if node_id not in collected_tests_so_far: collected_tests_so_far.append(node_id) item_result = create_test_outcome( @@ -106,6 +105,22 @@ def pytest_exception_interact(node, call, report): ) +def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: + """A function that returns the absolute test id. This is necessary because testIds are relative to the rootdir. + This does not work for our case since testIds when referenced during run time are relative to the instantiation + location. Absolute paths for testIds are necessary for the test tree ensures configurations that change the rootdir + of pytest are handled correctly. + + Keyword arguments: + test_id -- the pytest id of the test which is relative to the rootdir. + testPath -- the path to the file the test is located in, as a pathlib.Path object. + """ + split_id = test_id.split("::")[1:] + absolute_test_id = "::".join([str(testPath), *split_id]) + print("absolute path", absolute_test_id) + return absolute_test_id + + def pytest_keyboard_interrupt(excinfo): """A pytest hook that is called when a keyboard interrupt is raised. @@ -130,7 +145,7 @@ class TestOutcome(Dict): def create_test_outcome( - test: str, + testid: str, outcome: str, message: Union[str, None], traceback: Union[str, None], @@ -138,7 +153,7 @@ def create_test_outcome( ) -> TestOutcome: """A function that creates a TestOutcome object.""" return TestOutcome( - test=test, + test=testid, outcome=outcome, message=message, traceback=traceback, # TODO: traceback @@ -154,6 +169,7 @@ class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): IS_DISCOVERY = False +map_id_to_path = dict() def pytest_load_initial_conftests(early_config, parser, args): @@ -184,17 +200,21 @@ def pytest_report_teststatus(report, config): elif report.failed: report_value = "failure" message = report.longreprtext - node_id = str(report.nodeid) - if node_id not in collected_tests_so_far: - collected_tests_so_far.append(node_id) + node_path = map_id_to_path[report.nodeid] + if not node_path: + node_path = cwd + # Calculate the absolute test id and use this as the ID moving forward. + absolute_node_id = get_absolute_test_id(report.nodeid, node_path) + if absolute_node_id not in collected_tests_so_far: + collected_tests_so_far.append(absolute_node_id) item_result = create_test_outcome( - node_id, + absolute_node_id, report_value, message, traceback, ) collected_test = testRunResultDict() - collected_test[node_id] = item_result + collected_test[absolute_node_id] = item_result execution_post( os.fsdecode(cwd), "success", @@ -211,21 +231,22 @@ def pytest_report_teststatus(report, config): def pytest_runtest_protocol(item, nextitem): + map_id_to_path[item.nodeid] = get_node_path(item) skipped = check_skipped_wrapper(item) if skipped: - node_id = str(item.nodeid) + absolute_node_id = get_absolute_test_id(item.nodeid, get_node_path(item)) report_value = "skipped" cwd = pathlib.Path.cwd() - if node_id not in collected_tests_so_far: - collected_tests_so_far.append(node_id) + if absolute_node_id not in collected_tests_so_far: + collected_tests_so_far.append(absolute_node_id) item_result = create_test_outcome( - node_id, + absolute_node_id, report_value, None, None, ) collected_test = testRunResultDict() - collected_test[node_id] = item_result + collected_test[absolute_node_id] = item_result execution_post( os.fsdecode(cwd), "success", @@ -471,13 +492,14 @@ def create_test_node( test_case_loc: str = ( str(test_case.location[1] + 1) if (test_case.location[1] is not None) else "" ) + absolute_test_id = get_absolute_test_id(test_case.nodeid, get_node_path(test_case)) return { "name": test_case.name, "path": get_node_path(test_case), "lineno": test_case_loc, "type_": "test", - "id_": test_case.nodeid, - "runID": test_case.nodeid, + "id_": absolute_test_id, + "runID": absolute_test_id, } diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index f854371ffc35..661290bf4988 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -3,10 +3,11 @@ import * as net from 'net'; import * as crypto from 'crypto'; -import { Disposable, Event, EventEmitter } from 'vscode'; +import { Disposable, Event, EventEmitter, TestRun } from 'vscode'; import * as path from 'path'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -15,6 +16,7 @@ import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER } from './utils'; +import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { private _onDataReceived: EventEmitter = new EventEmitter(); @@ -140,7 +142,12 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDataReceived.event; } - async sendCommand(options: TestCommandOptions, runTestIdPort?: string, callback?: () => void): Promise { + async sendCommand( + options: TestCommandOptions, + runTestIdPort?: string, + runInstance?: TestRun, + callback?: () => void, + ): Promise { const { uuid } = options; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; @@ -154,7 +161,7 @@ export class PythonTestServer implements ITestServer, Disposable { }; if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; - const isRun = !options.testIds; + const isRun = runTestIdPort !== undefined; // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, @@ -195,7 +202,28 @@ export class PythonTestServer implements ITestServer, Disposable { // This means it is running discovery traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); } - await execService.exec(args, spawnOptions); + const deferred = createDeferred>(); + + const result = execService.execObservable(args, spawnOptions); + + runInstance?.token.onCancellationRequested(() => { + result?.proc?.kill(); + }); + + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. + // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + result?.proc?.stdout?.on('data', (data) => { + spawnOptions?.outputChannel?.append(data); + }); + result?.proc?.stderr?.on('data', (data) => { + spawnOptions?.outputChannel?.append(data); + }); + result?.proc?.on('exit', () => { + traceLog('Exec server closed.', uuid); + deferred.resolve({ stdout: '', stderr: '' }); + callback?.(); + }); + await deferred.promise; } } catch (ex) { this.uuids = this.uuids.filter((u) => u !== uuid); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index d4e54951bfd7..16c0bd0e3cee 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -174,7 +174,12 @@ export interface ITestServer { readonly onDataReceived: Event; readonly onRunDataReceived: Event; readonly onDiscoveryDataReceived: Event; - sendCommand(options: TestCommandOptions, runTestIdsPort?: string, callback?: () => void): Promise; + sendCommand( + options: TestCommandOptions, + runTestIdsPort?: string, + runInstance?: TestRun, + callback?: () => void, + ): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index b83224d4161b..44ab3746dde4 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -4,13 +4,14 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceError, traceVerbose } from '../../../logging'; +import { traceVerbose } from '../../../logging'; import { DataReceivedEvent, DiscoveredTestPayload, @@ -32,27 +33,30 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { async discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { const settings = this.configSettings.getSettings(uri); + const uuid = this.testServer.createUUID(uri.fsPath); const { pytestArgs } = settings.testing; traceVerbose(pytestArgs); - const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { - // cancelation token ? + const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); }); + const disposeDataReceiver = function (testServer: ITestServer) { + testServer.deleteUUID(uuid); + dataReceivedDisposable.dispose(); + }; try { - await this.runPytestDiscovery(uri, executionFactory); + await this.runPytestDiscovery(uri, uuid, executionFactory); } finally { - disposable.dispose(); + disposeDataReceiver(this.testServer); } // this is only a placeholder to handle function overloading until rewrite is finished const discoveryPayload: DiscoveredTestPayload = { cwd: uri.fsPath, status: 'success' }; return discoveryPayload; } - async runPytestDiscovery(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { + async runPytestDiscovery(uri: Uri, uuid: string, executionFactory?: IPythonExecutionFactory): Promise { const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); - const uuid = this.testServer.createUUID(uri.fsPath); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; @@ -78,17 +82,23 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); // delete UUID following entire discovery finishing. - execService - ?.exec(['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs), spawnOptions) - .then(() => { - this.testServer.deleteUUID(uuid); - return deferred.resolve(); - }) - .catch((err) => { - traceError(`Error while trying to run pytest discovery, \n${err}\r\n\r\n`); - this.testServer.deleteUUID(uuid); - return deferred.reject(err); - }); - return deferred.promise; + const deferredExec = createDeferred>(); + const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + const result = execService?.execObservable(execArgs, spawnOptions); + + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. + // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + result?.proc?.stdout?.on('data', (data) => { + spawnOptions.outputChannel?.append(data); + }); + result?.proc?.stderr?.on('data', (data) => { + spawnOptions.outputChannel?.append(data); + }); + result?.proc?.on('exit', () => { + deferredExec.resolve({ stdout: '', stderr: '' }); + deferred.resolve(); + }); + + await deferredExec.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index a75a6089627c..4a9a57b16fed 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -15,6 +15,7 @@ import { } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -22,13 +23,7 @@ import { removePositionalFoldersAndFiles } from './arguments'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { PYTEST_PROVIDER } from '../../common/constants'; import { EXTENSION_ROOT_DIR } from '../../../common/constants'; -import { startTestIdServer } from '../common/utils'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -// (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; -/** - * Wrapper Class for pytest test execution.. - */ +import * as utils from '../common/utils'; export class PytestTestExecutionAdapter implements ITestExecutionAdapter { constructor( @@ -48,18 +43,29 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); traceVerbose(uri, testIds, debugBool); - const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + const dataReceivedDisposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); - try { - await this.runTestsNew(uri, testIds, uuid, debugBool, executionFactory, debugLauncher); - } finally { - this.testServer.deleteUUID(uuid); - disposable.dispose(); - // confirm with testing that this gets called (it must clean this up) - } + const disposeDataReceiver = function (testServer: ITestServer) { + testServer.deleteUUID(uuid); + dataReceivedDisposable.dispose(); + }; + runInstance?.token.onCancellationRequested(() => { + disposeDataReceiver(this.testServer); + }); + await this.runTestsNew( + uri, + testIds, + uuid, + runInstance, + debugBool, + executionFactory, + debugLauncher, + disposeDataReceiver, + ); + // placeholder until after the rewrite is adopted // TODO: remove after adoption. const executionPayload: ExecutionTestPayload = { @@ -74,9 +80,11 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], uuid: string, + runInstance?: TestRun, debugBool?: boolean, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, + disposeDataReceiver?: (testServer: ITestServer) => void, ): Promise { const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; @@ -124,7 +132,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } traceLog(`Running PYTEST execution for the following test ids: ${testIds}`); - const pytestRunTestIdsPort = await startTestIdServer(testIds); + const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); @@ -143,6 +151,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions, () => { deferred.resolve(); + this.testServer.deleteUUID(uuid); }); } else { // combine path to run script with run args @@ -150,7 +159,28 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const runArgs = [scriptPath, ...testArgs]; traceInfo(`Running pytests with arguments: ${runArgs.join(' ')}\r\n`); - await execService?.exec(runArgs, spawnOptions); + const deferredExec = createDeferred>(); + const result = execService?.execObservable(runArgs, spawnOptions); + + runInstance?.token.onCancellationRequested(() => { + result?.proc?.kill(); + }); + + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. + // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + result?.proc?.stdout?.on('data', (data) => { + this.outputChannel?.append(data); + }); + result?.proc?.stderr?.on('data', (data) => { + this.outputChannel?.append(data); + }); + + result?.proc?.on('exit', () => { + deferredExec.resolve({ stdout: '', stderr: '' }); + deferred.resolve(); + disposeDataReceiver?.(this.testServer); + }); + await deferredExec.promise; } } catch (ex) { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 6deca55117ea..1cbad7ef65ef 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -43,15 +43,17 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { outChannel: this.outputChannel, }; - const disposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { + const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); }); - try { - await this.callSendCommand(options); - } finally { - this.testServer.deleteUUID(uuid); - disposable.dispose(); - } + const disposeDataReceiver = function (testServer: ITestServer) { + testServer.deleteUUID(uuid); + dataReceivedDisposable.dispose(); + }; + + await this.callSendCommand(options, () => { + disposeDataReceiver(this.testServer); + }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. const discoveryPayload: DiscoveredTestPayload = { @@ -61,8 +63,8 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - private async callSendCommand(options: TestCommandOptions): Promise { - await this.testServer.sendCommand(options); + private async callSendCommand(options: TestCommandOptions, callback: () => void): Promise { + await this.testServer.sendCommand(options, undefined, undefined, callback); const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; return discoveryPayload; } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 4cab941c2608..9af9e593c246 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -37,18 +37,19 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance?: TestRun, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); - const disposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); } }); - try { - await this.runTestsNew(uri, testIds, uuid, debugBool); - } finally { - this.testServer.deleteUUID(uuid); - disposable.dispose(); - // confirm with testing that this gets called (it must clean this up) - } + const disposeDataReceiver = function (testServer: ITestServer) { + testServer.deleteUUID(uuid); + disposedDataReceived.dispose(); + }; + runInstance?.token.onCancellationRequested(() => { + disposeDataReceiver(this.testServer); + }); + await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, disposeDataReceiver); const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; return executionPayload; } @@ -57,7 +58,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], uuid: string, + runInstance?: TestRun, debugBool?: boolean, + disposeDataReceiver?: (testServer: ITestServer) => void, ): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -80,8 +83,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const runTestIdsPort = await startTestIdServer(testIds); - await this.testServer.sendCommand(options, runTestIdsPort.toString(), () => { + await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, () => { deferred.resolve(); + disposeDataReceiver?.(this.testServer); }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. diff --git a/src/test/linters/lint.functional.test.ts b/src/test/linters/lint.functional.test.ts index a3dc70b7c21e..9887cbc5605a 100644 --- a/src/test/linters/lint.functional.test.ts +++ b/src/test/linters/lint.functional.test.ts @@ -4,7 +4,6 @@ 'use strict'; import * as assert from 'assert'; -import * as childProcess from 'child_process'; import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; @@ -780,11 +779,6 @@ suite('Linting Functional Tests', () => { teardown(() => { sinon.restore(); }); - - const pythonPath = childProcess.execSync(`"${PYTHON_PATH}" -c "import sys;print(sys.executable)"`); - - console.log(`Testing linter with python ${pythonPath}`); - // These are integration tests that mock out everything except // the filesystem and process execution. diff --git a/src/test/mocks/helper.ts b/src/test/mocks/helper.ts new file mode 100644 index 000000000000..24d7a8290b18 --- /dev/null +++ b/src/test/mocks/helper.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Readable } from 'stream'; + +export class FakeReadableStream extends Readable { + _read(_size: unknown): void | null { + // custom reading logic here + this.push(null); // end the stream + } +} diff --git a/src/test/mocks/mockChildProcess.ts b/src/test/mocks/mockChildProcess.ts new file mode 100644 index 000000000000..a46d66d79ca0 --- /dev/null +++ b/src/test/mocks/mockChildProcess.ts @@ -0,0 +1,239 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Serializable, SendHandle, MessageOptions } from 'child_process'; +import { EventEmitter } from 'node:events'; +import { Writable, Readable, Pipe } from 'stream'; +import { FakeReadableStream } from './helper'; + +export class MockChildProcess extends EventEmitter { + constructor(spawnfile: string, spawnargs: string[]) { + super(); + this.spawnfile = spawnfile; + this.spawnargs = spawnargs; + this.stdin = new Writable(); + this.stdout = new FakeReadableStream(); + this.stderr = new FakeReadableStream(); + this.channel = null; + this.stdio = [this.stdin, this.stdout, this.stdout, this.stderr, null]; + this.killed = false; + this.connected = false; + this.exitCode = null; + this.signalCode = null; + this.eventMap = new Map(); + } + + stdin: Writable | null; + + stdout: Readable | null; + + stderr: Readable | null; + + eventMap: Map; + + readonly channel?: Pipe | null | undefined; + + readonly stdio: [ + Writable | null, + // stdin + Readable | null, + // stdout + Readable | null, + // stderr + Readable | Writable | null | undefined, + // extra + Readable | Writable | null | undefined, // extra + ]; + + readonly killed: boolean; + + readonly pid?: number | undefined; + + readonly connected: boolean; + + readonly exitCode: number | null; + + readonly signalCode: NodeJS.Signals | null; + + readonly spawnargs: string[]; + + readonly spawnfile: string; + + signal?: NodeJS.Signals | number; + + send(message: Serializable, callback?: (error: Error | null) => void): boolean; + + send(message: Serializable, sendHandle?: SendHandle, callback?: (error: Error | null) => void): boolean; + + send( + message: Serializable, + sendHandle?: SendHandle, + options?: MessageOptions, + callback?: (error: Error | null) => void, + ): boolean; + + send( + message: Serializable, + _sendHandleOrCallback?: SendHandle | ((error: Error | null) => void), + _optionsOrCallback?: MessageOptions | ((error: Error | null) => void), + _callback?: (error: Error | null) => void, + ): boolean { + // Implementation of the send method + // For example, you might want to emit a 'message' event + this.stdout?.push(message.toString()); + return true; + } + + // eslint-disable-next-line class-methods-use-this + disconnect(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + unref(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + ref(): void { + /* noop */ + } + + addListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'disconnect', listener: () => void): this; + + addListener(event: 'error', listener: (err: Error) => void): this; + + addListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + addListener(event: 'spawn', listener: () => void): this; + + addListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + emit(event: 'close', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'disconnect'): boolean; + + emit(event: 'error', err: Error): boolean; + + emit(event: 'exit', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'message', message: Serializable, sendHandle: SendHandle): boolean; + + emit(event: 'spawn', listener: () => void): boolean; + + emit(event: string | symbol, ...args: unknown[]): boolean { + if (this.eventMap.has(event.toString())) { + this.eventMap.get(event.toString()).forEach((listener: (arg0: unknown) => void) => { + const argsArray = Array.isArray(args) ? args : [args]; + listener(argsArray); + }); + } + return true; + } + + on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'disconnect', listener: () => void): this; + + on(event: 'error', listener: (err: Error) => void): this; + + on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + on(event: 'spawn', listener: () => void): this; + + on(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'disconnect', listener: () => void): this; + + once(event: 'error', listener: (err: Error) => void): this; + + once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + once(event: 'spawn', listener: () => void): this; + + once(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'disconnect', listener: () => void): this; + + prependListener(event: 'error', listener: (err: Error) => void): this; + + prependListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependListener(event: 'spawn', listener: () => void): this; + + prependListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependOnceListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'disconnect', listener: () => void): this; + + prependOnceListener(event: 'error', listener: (err: Error) => void): this; + + prependOnceListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependOnceListener(event: 'spawn', listener: () => void): this; + + prependOnceListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + trigger(event: string): Array { + if (this.eventMap.has(event)) { + return this.eventMap.get(event); + } + return []; + } + + kill(_signal?: NodeJS.Signals | number): boolean { + this.stdout?.destroy(); + return true; + } +} diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts new file mode 100644 index 000000000000..5c92c7cf3941 --- /dev/null +++ b/src/test/testing/common/testingAdapter.test.ts @@ -0,0 +1,428 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { TestRun, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as path from 'path'; +import * as assert from 'assert'; +import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; +import { ITestResultResolver, ITestServer } from '../../../client/testing/testController/common/types'; +import { PythonTestServer } from '../../../client/testing/testController/common/server'; +import { IPythonExecutionFactory } from '../../../client/common/process/types'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize'; +import { traceError, traceLog } from '../../../client/logging'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; +import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; + +suite('End to End Tests: test adapters', () => { + let resultResolver: typeMoq.IMock; + let pythonTestServer: ITestServer; + let pythonExecFactory: IPythonExecutionFactory; + let debugLauncher: ITestDebugLauncher; + let configService: IConfigurationService; + let testOutputChannel: ITestOutputChannel; + let serviceContainer: IServiceContainer; + let workspaceUri: Uri; + const rootPathSmallWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'smallWorkspace', + ); + const rootPathLargeWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'largeWorkspace', + ); + suiteSetup(async () => { + serviceContainer = (await initialize()).serviceContainer; + }); + + setup(async () => { + // create objects that were injected + configService = serviceContainer.get(IConfigurationService); + pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); + debugLauncher = serviceContainer.get(ITestDebugLauncher); + testOutputChannel = serviceContainer.get(ITestOutputChannel); + + // create mock resultResolver object + resultResolver = typeMoq.Mock.ofType(); + + // create objects that were not injected + pythonTestServer = new PythonTestServer(pythonExecFactory, debugLauncher); + await pythonTestServer.serverReady(); + }); + test('unittest discovery adapter small workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + status: unknown; + error: string | any[]; + tests: unknown; + }; + resultResolver + .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceLog(`resolveDiscovery ${data}`); + actualData = data; + return Promise.resolve(); + }); + + // set workspace to test workspace folder and set up settings + workspaceUri = Uri.parse(rootPathSmallWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + // run unittest discovery + const discoveryAdapter = new UnittestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + + await discoveryAdapter.discoverTests(workspaceUri).finally(() => { + // verification after discovery is complete + resultResolver.verify( + (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.once(), + ); + + // 1. Check the status is "success" + assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + // 2. Confirm no errors + assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + }); + }); + + test('unittest discovery adapter large workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + status: unknown; + error: string | any[]; + tests: unknown; + }; + resultResolver + .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceLog(`resolveDiscovery ${data}`); + actualData = data; + return Promise.resolve(); + }); + + // set settings to work for the given workspace + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + // run discovery + const discoveryAdapter = new UnittestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + + await discoveryAdapter.discoverTests(workspaceUri).finally(() => { + // verification after discovery is complete + resultResolver.verify( + (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.once(), + ); + + // 1. Check the status is "success" + assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + // 2. Confirm no errors + assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + }); + }); + test('pytest discovery adapter small workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + status: unknown; + error: string | any[]; + tests: unknown; + }; + resultResolver + .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceLog(`resolveDiscovery ${data}`); + actualData = data; + return Promise.resolve(); + }); + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + resultResolver.verify( + (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.once(), + ); + + // 1. Check the status is "success" + assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + // 2. Confirm no errors + assert.strictEqual(actualData.error.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + }); + }); + test('pytest discovery adapter large workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + status: unknown; + error: string | any[]; + tests: unknown; + }; + resultResolver + .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceLog(`resolveDiscovery ${data}`); + actualData = data; + return Promise.resolve(); + }); + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + resultResolver.verify( + (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.once(), + ); + + // 1. Check the status is "success" + assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + // 2. Confirm no errors + assert.strictEqual(actualData.error.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + }); + }); + test('unittest execution adapter small workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + status: unknown; + error: string | any[]; + result: unknown; + }; + resultResolver + .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceLog(`resolveExecution ${data}`); + actualData = data; + return Promise.resolve(); + }); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + // run execution + const executionAdapter = new UnittestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests(workspaceUri, ['test_simple.SimpleClass.test_simple_unit'], false, testRun.object) + .finally(() => { + // verification after execution is complete + resultResolver.verify( + (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.once(), + ); + + // 1. Check the status is "success" + assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + // 2. Confirm tests are found + assert.ok(actualData.result, 'Expected results to be present'); + }); + }); + test('unittest execution adapter large workspace', async () => { + // result resolver and saved data for assertions + resultResolver + .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceError(`resolveExecution ${data}`); + console.log(`resolveExecution ${data}`); + traceLog(`resolveExecution ${data}`); + // do the following asserts for each time resolveExecution is called, should be called once per test. + // 1. Check the status, can be subtest success or failure + assert( + data.status === 'subtest-success' || data.status === 'subtest-failure', + "Expected status to be 'subtest-success' or 'subtest-failure'", + ); + // 2. Confirm tests are found + assert.ok(data.result, 'Expected results to be present'); + return Promise.resolve(); + }); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + // run unittest execution + const executionAdapter = new UnittestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests(workspaceUri, ['test_parameterized_subtest.NumbersTest.test_even'], false, testRun.object) + .finally(() => { + // verification after discovery is complete + resultResolver.verify( + (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.exactly(200), + ); + }); + }); + test('pytest execution adapter small workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + status: unknown; + error: string | any[]; + result: unknown; + }; + resultResolver + .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceLog(`resolveExecution ${data}`); + actualData = data; + return Promise.resolve(); + }); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests( + workspaceUri, + [`${rootPathSmallWorkspace}/test_simple.py::test_a`], + false, + testRun.object, + pythonExecFactory, + ) + .finally(() => { + // verification after discovery is complete + resultResolver.verify( + (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.once(), + ); + + // 1. Check the status is "success" + assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + // 2. Confirm no errors + assert.strictEqual(actualData.error, null, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.result, 'Expected results to be present'); + }); + }); + test('pytest execution adapter large workspace', async () => { + resultResolver + .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + traceLog(`resolveExecution ${data}`); + // do the following asserts for each time resolveExecution is called, should be called once per test. + // 1. Check the status is "success" + assert.strictEqual(data.status, 'success', "Expected status to be 'success'"); + // 2. Confirm no errors + assert.strictEqual(data.error, null, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(data.result, 'Expected results to be present'); + return Promise.resolve(); + }); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + + // generate list of test_ids + const testIds: string[] = []; + for (let i = 0; i < 200; i = i + 1) { + const testId = `${rootPathLargeWorkspace}/test_parameterized_subtest.py::test_odd_even[${i}]`; + testIds.push(testId); + } + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).finally(() => { + // resolve execution should be called 200 times since there are 200 tests run. + resultResolver.verify( + (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.exactly(200), + ); + }); + }); +}); diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 18212b2d1032..8ba7dd9a6f00 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; import { ITestServer } from '../../../../client/testing/testController/common/types'; @@ -12,9 +13,11 @@ import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions, + Output, } from '../../../../client/common/process/types'; -import { createDeferred, Deferred } from '../../../../client/common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { Deferred, createDeferred } from '../../../../client/common/utils/async'; suite('pytest test discovery adapter', () => { let testServer: typeMoq.IMock; @@ -29,6 +32,7 @@ suite('pytest test discovery adapter', () => { let expectedPath: string; let uri: Uri; let expectedExtraVariables: Record; + let mockProc: MockChildProcess; setup(() => { const mockExtensionRootDir = typeMoq.Mock.ofType(); @@ -66,32 +70,46 @@ suite('pytest test discovery adapter', () => { }), } as unknown) as IConfigurationService; - // set up exec factory - execFactory = typeMoq.Mock.ofType(); - execFactory - .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) - .returns(() => Promise.resolve(execService.object)); - - // set up exec service + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); execService = typeMoq.Mock.ofType(); - deferred = createDeferred(); + const output = new Observable>(() => { + /* no op */ + }); execService - .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => { - deferred.resolve(); - return Promise.resolve({ stdout: '{}' }); - }); + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); outputChannel = typeMoq.Mock.ofType(); }); test('Discovery should call exec with correct basic args', async () => { + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); - await adapter.discoverTests(uri, execFactory.object); - const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); + // verification + const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); @@ -108,16 +126,34 @@ suite('pytest test discovery adapter', () => { const expectedPathNew = path.join('other', 'path'); const configServiceNew: IConfigurationService = ({ getSettings: () => ({ - testing: { pytestArgs: ['.', 'abc', 'xyz'], cwd: expectedPathNew }, + testing: { + pytestArgs: ['.', 'abc', 'xyz'], + cwd: expectedPathNew, + }, }), } as unknown) as IConfigurationService; + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + adapter = new PytestTestDiscoveryAdapter(testServer.object, configServiceNew, outputChannel.object); - await adapter.discoverTests(uri, execFactory.object); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); + + // verification const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.', 'abc', 'xyz']; execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.deepEqual(options.extraVariables, expectedExtraVariables); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 44116fd753b0..43b763f56e6c 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -6,11 +6,13 @@ import { TestRun, Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as sinon from 'sinon'; import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { ITestServer } from '../../../../client/testing/testController/common/types'; import { IPythonExecutionFactory, IPythonExecutionService, + Output, SpawnOptions, } from '../../../../client/common/process/types'; import { createDeferred, Deferred } from '../../../../client/common/utils/async'; @@ -18,6 +20,7 @@ import { PytestTestExecutionAdapter } from '../../../../client/testing/testContr import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; import * as util from '../../../../client/testing/testController/common/utils'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; suite('pytest test execution adapter', () => { let testServer: typeMoq.IMock; @@ -29,8 +32,8 @@ suite('pytest test execution adapter', () => { let debugLauncher: typeMoq.IMock; (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; let myTestPath: string; - let startTestIdServerStub: sinon.SinonStub; - + let mockProc: MockChildProcess; + let utilsStub: sinon.SinonStub; setup(() => { testServer = typeMoq.Mock.ofType(); testServer.setup((t) => t.getPort()).returns(() => 12345); @@ -47,8 +50,24 @@ suite('pytest test execution adapter', () => { }), isTestExecution: () => false, } as unknown) as IConfigurationService; - execFactory = typeMoq.Mock.ofType(); + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); execService = typeMoq.Mock.ofType(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); + execFactory = typeMoq.Mock.ofType(); + utilsStub = sinon.stub(util, 'startTestIdServer'); debugLauncher = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) @@ -66,7 +85,6 @@ suite('pytest test execution adapter', () => { deferred.resolve(); return Promise.resolve(); }); - startTestIdServerStub = sinon.stub(util, 'startTestIdServer').returns(Promise.resolve(54321)); execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); @@ -77,10 +95,25 @@ suite('pytest test execution adapter', () => { sinon.restore(); }); test('startTestIdServer called with correct testIds', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -88,19 +121,38 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - const testIds = ['test1id', 'test2id']; - await adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); + adapter.runTests(uri, testIds, false, testRun.object, execFactory.object); - sinon.assert.calledWithExactly(startTestIdServerStub, testIds); + // add in await and trigger + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); + + // assert + sinon.assert.calledWithExactly(utilsStub, testIds); }); test('pytest execution called with correct args', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -108,9 +160,12 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - await adapter.runTests(uri, [], false, testRun.object, execFactory.object); + adapter.runTests(uri, [], false, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); @@ -123,7 +178,7 @@ suite('pytest test execution adapter', () => { // execService.verify((x) => x.exec(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); @@ -139,6 +194,21 @@ suite('pytest test execution adapter', () => { ); }); test('pytest execution respects settings.testing.cwd when present', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const newCwd = path.join('new', 'path'); configService = ({ getSettings: () => ({ @@ -149,7 +219,7 @@ suite('pytest test execution adapter', () => { const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -157,9 +227,12 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); - await adapter.runTests(uri, [], false, testRun.object, execFactory.object); + adapter.runTests(uri, [], false, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); @@ -172,7 +245,7 @@ suite('pytest test execution adapter', () => { execService.verify( (x) => - x.exec( + x.execObservable( expectedArgs, typeMoq.It.is((options) => { assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); @@ -188,10 +261,17 @@ suite('pytest test execution adapter', () => { ); }); test('Debug launched correctly for pytest', async () => { + const deferred3 = createDeferred(); + utilsStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve(54321); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer - .setup((t) => t.onDiscoveryDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ dispose: () => { /* no-body */ @@ -199,9 +279,9 @@ suite('pytest test execution adapter', () => { })); testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); const outputChannel = typeMoq.Mock.ofType(); - const testRun = typeMoq.Mock.ofType(); adapter = new PytestTestExecutionAdapter(testServer.object, configService, outputChannel.object); await adapter.runTests(uri, [], true, testRun.object, execFactory.object, debugLauncher.object); + await deferred3.promise; debugLauncher.verify( (x) => x.launchDebugger( diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 1131c26c6444..53c2b72e40f7 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -1,15 +1,24 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ import * as assert from 'assert'; import * as net from 'net'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; import { OutputChannel, Uri } from 'vscode'; -import { IPythonExecutionFactory, IPythonExecutionService, SpawnOptions } from '../../../client/common/process/types'; +import { Observable } from 'rxjs'; +import * as typeMoq from 'typemoq'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + Output, + SpawnOptions, +} from '../../../client/common/process/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; -import { createDeferred } from '../../../client/common/utils/async'; +import { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; suite('Python Test Server', () => { const fakeUuid = 'fake-uuid'; @@ -18,10 +27,12 @@ suite('Python Test Server', () => { let stubExecutionService: IPythonExecutionService; let server: PythonTestServer; let sandbox: sinon.SinonSandbox; - let execArgs: string[]; - let spawnOptions: SpawnOptions; let v4Stub: sinon.SinonStub; let debugLauncher: ITestDebugLauncher; + let mockProc: MockChildProcess; + let execService: typeMoq.IMock; + let deferred: Deferred; + let execFactory = typeMoq.Mock.ofType(); setup(() => { sandbox = sinon.createSandbox(); @@ -29,27 +40,42 @@ suite('Python Test Server', () => { v4Stub.returns(fakeUuid); stubExecutionService = ({ - exec: (args: string[], spawnOptionsProvided: SpawnOptions) => { - execArgs = args; - spawnOptions = spawnOptionsProvided; - return Promise.resolve({ stdout: '', stderr: '' }); - }, + execObservable: () => Promise.resolve({ stdout: '', stderr: '' }), } as unknown) as IPythonExecutionService; stubExecutionFactory = ({ createActivatedEnvironment: () => Promise.resolve(stubExecutionService), } as unknown) as IPythonExecutionFactory; + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + execService = typeMoq.Mock.ofType(); + const output = new Observable>(() => { + /* no op */ + }); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); }); teardown(() => { sandbox.restore(); - execArgs = []; server.dispose(); }); test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, + command: { + script: 'myscript', + args: ['-foo', 'foo'], + }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, @@ -59,17 +85,31 @@ suite('Python Test Server', () => { outputChannel: undefined, token: undefined, throwOnStdErr: true, - extraVariables: { PYTHONPATH: '/foo/bar', RUN_TEST_IDS_PORT: '56789' }, + extraVariables: { + PYTHONPATH: '/foo/bar', + RUN_TEST_IDS_PORT: '56789', + }, } as SpawnOptions; + const deferred2 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(execFactory.object, debugLauncher); await server.serverReady(); - await server.sendCommand(options, '56789'); - const port = server.getPort(); + server.sendCommand(options, '56789'); + // add in await and trigger + await deferred2.promise; + mockProc.trigger('close'); - assert.deepStrictEqual(execArgs, ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']); - assert.deepStrictEqual(spawnOptions, expectedSpawnOptions); + const port = server.getPort(); + const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']; + execService.verify((x) => x.execObservable(expectedArgs, expectedSpawnOptions), typeMoq.Times.once()); }); test('sendCommand should write to an output channel if it is provided as an option', async () => { @@ -80,17 +120,31 @@ suite('Python Test Server', () => { }, } as OutputChannel; const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, + command: { + script: 'myscript', + args: ['-foo', 'foo'], + }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, outChannel, }; + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(execFactory.object, debugLauncher); await server.serverReady(); - await server.sendCommand(options); + server.sendCommand(options); + // add in await and trigger + await deferred.promise; + mockProc.trigger('close'); const port = server.getPort(); const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); @@ -99,13 +153,12 @@ suite('Python Test Server', () => { }); test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { - let eventData: { status: string; errors: string[] }; + let eventData: { status: string; errors: string[] } | undefined; stubExecutionService = ({ - exec: () => { + execObservable: () => { throw new Error('Failed to execute'); }, } as unknown) as IPythonExecutionService; - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -122,30 +175,43 @@ suite('Python Test Server', () => { await server.sendCommand(options); - assert.deepStrictEqual(eventData!.status, 'error'); - assert.deepStrictEqual(eventData!.errors, ['Failed to execute']); + assert.notEqual(eventData, undefined); + assert.deepStrictEqual(eventData?.status, 'error'); + assert.deepStrictEqual(eventData?.errors, ['Failed to execute']); }); test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + + deferred = createDeferred(); + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -161,16 +227,17 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); + // add in await and trigger await deferred.promise; + mockProc.trigger('close'); + assert.deepStrictEqual(eventData, ''); }); test('If the server doesnt recognize the UUID it should ignore it', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -178,14 +245,28 @@ suite('Python Test Server', () => { uuid: fakeUuid, }; - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -201,7 +282,7 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, ''); }); @@ -212,23 +293,34 @@ suite('Python Test Server', () => { test('Error if payload does not have a content length header', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); - const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = data; @@ -244,7 +336,7 @@ suite('Python Test Server', () => { console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, ''); }); @@ -267,7 +359,6 @@ Request-uuid: UUID_HERE // Your test logic here let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, @@ -275,15 +366,28 @@ Request-uuid: UUID_HERE cwd: '/foo/bar', uuid: fakeUuid, }; - - stubExecutionService = ({ - exec: async () => { + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; }, } as unknown) as IPythonExecutionService; + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); await server.serverReady(); const uuid = server.createUUID(); payload = payload.replace('UUID_HERE', uuid); @@ -301,7 +405,7 @@ Request-uuid: UUID_HERE console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; assert.deepStrictEqual(eventData, expectedResult); }); @@ -310,8 +414,29 @@ Request-uuid: UUID_HERE test('Calls run resolver if the result header is in the payload', async () => { let eventData: string | undefined; const client = new net.Socket(); - const deferred = createDeferred(); + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { + client.connect(server.getPort()); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }, + } as unknown) as IPythonExecutionService; + + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), @@ -319,14 +444,6 @@ Request-uuid: UUID_HERE uuid: fakeUuid, }; - stubExecutionService = ({ - exec: async () => { - client.connect(server.getPort()); - return Promise.resolve({ stdout: '', stderr: '' }); - }, - } as unknown) as IPythonExecutionService; - - server = new PythonTestServer(stubExecutionFactory, debugLauncher); await server.serverReady(); const uuid = server.createUUID(); server.onRunDataReceived(({ data }) => { @@ -349,9 +466,8 @@ Request-uuid: ${uuid} console.log('Socket connection error:', error); }); - await server.sendCommand(options); + server.sendCommand(options); await deferred.promise; - console.log('event data', eventData); const expectedResult = '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; assert.deepStrictEqual(eventData, expectedResult); diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts index 5a2e48130746..41cd1bbd7ef2 100644 --- a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -164,8 +164,7 @@ suite('Workspace test adapter', () => { const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); const testProvider = 'unittest'; - const abc = await workspaceTestAdapter.discoverTests(testController); - console.log(abc); + await workspaceTestAdapter.discoverTests(testController); sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider); diff --git a/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py new file mode 100644 index 000000000000..3e84df0a2d9f --- /dev/null +++ b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest +import unittest + + +@pytest.mark.parametrize("num", range(0, 200)) +def test_odd_even(num): + return num % 2 == 0 + + +class NumbersTest(unittest.TestCase): + def test_even(self): + for i in range(0, 200): + with self.subTest(i=i): + self.assertEqual(i % 2, 0) diff --git a/src/testTestingRootWkspc/smallWorkspace/test_simple.py b/src/testTestingRootWkspc/smallWorkspace/test_simple.py new file mode 100644 index 000000000000..6b4f7bd2f8a6 --- /dev/null +++ b/src/testTestingRootWkspc/smallWorkspace/test_simple.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + + +def test_a(): + assert 1 == 1 + + +class SimpleClass(unittest.TestCase): + def test_simple_unit(self): + assert True From 9c740b9557d62e21b062a35ff4a945835fe45c64 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 14 Aug 2023 17:50:34 -0700 Subject: [PATCH 0139/1136] Show notification reaffirming Python extension still handles activation when in `pythonTerminalEnvVarActivation` experiment (#21802) Closes https://github.com/microsoft/vscode-python/issues/21793 Only show notification when terminal prompt does not already indicate that env is activated. --- src/client/activation/extensionSurvey.ts | 4 +- src/client/common/experiments/helpers.ts | 6 + src/client/common/utils/localize.ts | 5 +- .../terminalEnvVarCollectionPrompt.ts | 80 +++++++ .../terminalEnvVarCollectionService.ts | 65 +++++- src/client/interpreter/activation/types.ts | 8 + src/client/interpreter/serviceRegistry.ts | 12 +- src/client/pythonEnvironments/info/index.ts | 3 +- src/client/telemetry/index.ts | 6 +- .../activation/extensionSurvey.unit.test.ts | 2 +- .../checks/macPythonInterpreter.unit.test.ts | 2 +- .../common/installer/installer.unit.test.ts | 12 +- ...erminalEnvVarCollectionPrompt.unit.test.ts | 203 ++++++++++++++++++ ...rminalEnvVarCollectionService.unit.test.ts | 157 ++++++++++++++ .../virtualEnvs/virtualEnvPrompt.unit.test.ts | 2 +- 15 files changed, 545 insertions(+), 22 deletions(-) create mode 100644 src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts create mode 100644 src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts diff --git a/src/client/activation/extensionSurvey.ts b/src/client/activation/extensionSurvey.ts index 6d1d784237ba..11b581a27252 100644 --- a/src/client/activation/extensionSurvey.ts +++ b/src/client/activation/extensionSurvey.ts @@ -83,10 +83,10 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService @traceDecoratorError('Failed to display prompt for extension survey') public async showSurvey() { const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain]; - const telemetrySelections: ['Yes', 'Maybe later', 'Do not show again'] = [ + const telemetrySelections: ['Yes', 'Maybe later', "Don't show again"] = [ 'Yes', 'Maybe later', - 'Do not show again', + "Don't show again", ]; const selection = await this.appShell.showInformationMessage(ExtensionSurveyBanner.bannerMessage, ...prompts); sendTelemetryEvent(EventName.EXTENSION_SURVEY_PROMPT, undefined, { diff --git a/src/client/common/experiments/helpers.ts b/src/client/common/experiments/helpers.ts index 04da948fd15d..bae96b222eb6 100644 --- a/src/client/common/experiments/helpers.ts +++ b/src/client/common/experiments/helpers.ts @@ -3,10 +3,16 @@ 'use strict'; +import { env, workspace } from 'vscode'; import { IExperimentService } from '../types'; import { TerminalEnvVarActivation } from './groups'; +import { isTestExecution } from '../constants'; export function inTerminalEnvVarExperiment(experimentService: IExperimentService): boolean { + if (!isTestExecution() && workspace.workspaceFile && env.remoteName) { + // TODO: Remove this if statement once https://github.com/microsoft/vscode/issues/180486 is fixed. + return false; + } if (!experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)) { return false; } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index b3af0c476957..3ec1829201f8 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -63,7 +63,7 @@ export namespace Common { export const openOutputPanel = l10n.t('Show output'); export const noIWillDoItLater = l10n.t('No, I will do it later'); export const notNow = l10n.t('Not now'); - export const doNotShowAgain = l10n.t('Do not show again'); + export const doNotShowAgain = l10n.t("Don't show again"); export const reload = l10n.t('Reload'); export const moreInfo = l10n.t('More Info'); export const learnMore = l10n.t('Learn more'); @@ -198,6 +198,9 @@ export namespace Interpreters { ); export const activatingTerminals = l10n.t('Reactivating terminals...'); export const activateTerminalDescription = l10n.t('Activated environment for'); + export const terminalEnvVarCollectionPrompt = l10n.t( + 'The Python extension automatically activates all terminals using the selected environment. You can hover over the terminal tab to see more information about the activation. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', + ); export const activatedCondaEnvLaunch = l10n.t( 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', ); diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts new file mode 100644 index 000000000000..7833f34ce2fb --- /dev/null +++ b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../common/application/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IPersistentStateFactory, +} from '../../common/types'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { ITerminalEnvVarCollectionService } from './types'; +import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; + +export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; + +@injectable() +export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + @inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService, + @inject(ITerminalEnvVarCollectionService) + private readonly terminalEnvVarCollectionService: ITerminalEnvVarCollectionService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + ) {} + + public async activate(): Promise { + if (!inTerminalEnvVarExperiment(this.experimentService)) { + return; + } + this.disposableRegistry.push( + this.terminalManager.onDidOpenTerminal(async (terminal) => { + const cwd = + 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd + ? terminal.creationOptions.cwd + : this.activeResourceService.getActiveResource(); + const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; + const settings = this.configurationService.getSettings(resource); + if (!settings.terminal.activateEnvironment) { + return; + } + if (this.terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)) { + // No need to show notification if terminal prompt already indicates when env is activated. + return; + } + await this.notifyUsers(); + }), + ); + } + + private async notifyUsers(): Promise { + const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + terminalEnvCollectionPromptKey, + true, + ); + if (!notificationPromptEnabled.value) { + return; + } + const prompts = [Common.doNotShowAgain]; + const selection = await this.appShell.showInformationMessage( + Interpreters.terminalEnvVarCollectionPrompt, + ...prompts, + ); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await notificationPromptEnabled.updateValue(false); + } + } +} diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 2e5c26be0222..a45306132bf4 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -23,13 +23,15 @@ import { Interpreters } from '../../common/utils/localize'; import { traceDecoratorVerbose, traceVerbose } from '../../logging'; import { IInterpreterService } from '../contracts'; import { defaultShells } from './service'; -import { IEnvironmentActivationService } from './types'; +import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; import { EnvironmentType } from '../../pythonEnvironments/info'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; +import { TerminalShellType } from '../../common/terminal/types'; +import { OSType } from '../../common/utils/platform'; @injectable() -export class TerminalEnvVarCollectionService implements IExtensionActivationService { +export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false, @@ -127,6 +129,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ await this._applyCollection(resource, defaultShell?.shell); return; } + await this.trackTerminalPrompt(shell, resource, env); this.processEnvVars = undefined; return; } @@ -146,6 +149,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (prevValue !== value) { if (value !== undefined) { if (key === 'PS1') { + // We cannot have the full PS1 without executing in terminal, which we do not. Hence prepend it. traceVerbose(`Prepending environment variable ${key} in collection with ${value}`); envVarCollection.prepend(key, value, { applyAtShellIntegration: true, @@ -165,6 +169,61 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); envVarCollection.description = description; + + await this.trackTerminalPrompt(shell, resource, env); + } + + private isPromptSet = new Map(); + + // eslint-disable-next-line class-methods-use-this + public isTerminalPromptSetCorrectly(resource?: Resource): boolean { + const workspaceFolder = this.getWorkspaceFolder(resource); + return !!this.isPromptSet.get(workspaceFolder?.index); + } + + /** + * Call this once we know terminal prompt is set correctly for terminal owned by this resource. + */ + private terminalPromptIsCorrect(resource: Resource) { + const key = this.getWorkspaceFolder(resource)?.index; + this.isPromptSet.set(key, true); + } + + private terminalPromptIsUnknown(resource: Resource) { + const key = this.getWorkspaceFolder(resource)?.index; + this.isPromptSet.delete(key); + } + + /** + * Tracks whether prompt for terminal was correctly set. + */ + private async trackTerminalPrompt(shell: string, resource: Resource, env: EnvironmentVariables | undefined) { + this.terminalPromptIsUnknown(resource); + if (!env) { + this.terminalPromptIsCorrect(resource); + return; + } + // Prompts for these shells cannot be set reliably using variables + const exceptionShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.fish, + TerminalShellType.zsh, // TODO: Remove this once https://github.com/microsoft/vscode/issues/188875 is fixed + ]; + const customShellType = identifyShellFromShellPath(shell); + if (exceptionShells.includes(customShellType)) { + return; + } + if (this.platform.osType !== OSType.Windows) { + // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const shouldPS1BeSet = interpreter?.type !== undefined; + if (shouldPS1BeSet && !env.PS1) { + // PS1 should be set but no PS1 was set. + return; + } + } + this.terminalPromptIsCorrect(resource); } private async handleMicroVenv(resource: Resource) { @@ -178,7 +237,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ envVarCollection.replace( 'PATH', `${path.dirname(interpreter.path)}${path.delimiter}${process.env[pathVarName]}`, - { applyAtShellIntegration: true }, + { applyAtShellIntegration: true, applyAtProcessCreation: true }, ); return; } diff --git a/src/client/interpreter/activation/types.ts b/src/client/interpreter/activation/types.ts index e00ef9b62b3f..2b364cbeb862 100644 --- a/src/client/interpreter/activation/types.ts +++ b/src/client/interpreter/activation/types.ts @@ -21,3 +21,11 @@ export interface IEnvironmentActivationService { interpreter?: PythonEnvironment, ): Promise; } + +export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); +export interface ITerminalEnvVarCollectionService { + /** + * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. + */ + isTerminalPromptSetCorrectly(resource?: Resource): boolean; +} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 04af15415b04..018e7abfdc46 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -6,8 +6,9 @@ import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { EnvironmentActivationService } from './activation/service'; +import { TerminalEnvVarCollectionPrompt } from './activation/terminalEnvVarCollectionPrompt'; import { TerminalEnvVarCollectionService } from './activation/terminalEnvVarCollectionService'; -import { IEnvironmentActivationService } from './activation/types'; +import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './activation/types'; import { InterpreterAutoSelectionService } from './autoSelection/index'; import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy'; import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types'; @@ -109,8 +110,13 @@ export function registerTypes(serviceManager: IServiceManager): void { IEnvironmentActivationService, EnvironmentActivationService, ); - serviceManager.addSingleton( - IExtensionActivationService, + serviceManager.addSingleton( + ITerminalEnvVarCollectionService, TerminalEnvVarCollectionService, ); + serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalEnvVarCollectionPrompt, + ); } diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index 60628f61314e..ee2ff9d7cc22 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -4,6 +4,7 @@ 'use strict'; import { Architecture } from '../../common/utils/platform'; +import { PythonEnvType } from '../base/info'; import { PythonVersion } from './pythonVersion'; /** @@ -85,7 +86,7 @@ export type PythonEnvironment = InterpreterInformation & { envName?: string; envPath?: string; cachedEntry?: boolean; - type?: string; + type?: PythonEnvType; }; /** diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 3fcc177edea7..a068f21d2327 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -950,7 +950,7 @@ export interface IEventNamePropertyMapping { tool?: LinterId; /** * `select` When 'Select linter' option is selected - * `disablePrompt` When 'Do not show again' option is selected + * `disablePrompt` When "Don't show again" option is selected * `install` When 'Install' option is selected * * @type {('select' | 'disablePrompt' | 'install')} @@ -1374,7 +1374,7 @@ export interface IEventNamePropertyMapping { /** * `Yes` When 'Yes' option is selected * `No` When 'No' option is selected - * `Ignore` When 'Do not show again' option is clicked + * `Ignore` When "Don't show again" option is clicked * * @type {('Yes' | 'No' | 'Ignore' | undefined)} */ @@ -1571,7 +1571,7 @@ export interface IEventNamePropertyMapping { /** * Carries the selection of user when they are asked to take the extension survey */ - selection: 'Yes' | 'Maybe later' | 'Do not show again' | undefined; + selection: 'Yes' | 'Maybe later' | "Don't show again" | undefined; }; /** * Telemetry event sent when starting REPL diff --git a/src/test/activation/extensionSurvey.unit.test.ts b/src/test/activation/extensionSurvey.unit.test.ts index 6449eae24f31..ba96b917aff3 100644 --- a/src/test/activation/extensionSurvey.unit.test.ts +++ b/src/test/activation/extensionSurvey.unit.test.ts @@ -355,7 +355,7 @@ suite('Extension survey prompt - showSurvey()', () => { platformService.verifyAll(); }); - test("Disable prompt if 'Do not show again' option is clicked", async () => { + test('Disable prompt if "Don\'t show again" option is clicked', async () => { const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain]; platformService.setup((p) => p.osType).verifiable(TypeMoq.Times.never()); appShell diff --git a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts index f75375520ec8..ba2436d0ffeb 100644 --- a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts @@ -220,7 +220,7 @@ suite('Application Diagnostics - Checks Mac Python Interpreter', () => { expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); expect(messagePrompt!.commandPrompts).to.be.deep.equal([ { prompt: 'Select Python Interpreter', command: cmd }, - { prompt: 'Do not show again', command: cmdIgnore }, + { prompt: "Don't show again", command: cmdIgnore }, ]); }); test('Should not display a message if No Interpreters diagnostic has been ignored', async () => { diff --git a/src/test/common/installer/installer.unit.test.ts b/src/test/common/installer/installer.unit.test.ts index bdd7ab32a028..38b9d9174472 100644 --- a/src/test/common/installer/installer.unit.test.ts +++ b/src/test/common/installer/installer.unit.test.ts @@ -307,10 +307,10 @@ suite('Module Installer only', () => { TypeMoq.It.isAnyString(), TypeMoq.It.isValue('Install'), TypeMoq.It.isValue('Select Linter'), - TypeMoq.It.isValue('Do not show again'), + TypeMoq.It.isValue("Don't show again"), ), ) - .returns(async () => 'Do not show again') + .returns(async () => "Don't show again") .verifiable(TypeMoq.Times.once()); const persistVal = TypeMoq.Mock.ofType>(); let mockPersistVal = false; @@ -367,7 +367,7 @@ suite('Module Installer only', () => { TypeMoq.It.isAnyString(), TypeMoq.It.isValue('Install'), TypeMoq.It.isValue('Select Linter'), - TypeMoq.It.isValue('Do not show again'), + TypeMoq.It.isValue("Don't show again"), ), ) .returns(async () => undefined) @@ -864,7 +864,7 @@ suite('Module Installer only', () => { test('Ensure 3 options for pylint', async () => { const product = Product.pylint; - const options = ['Select Linter', 'Do not show again']; + const options = ['Select Linter', "Don't show again"]; const productName = ProductNames.get(product)!; await installer.promptToInstallImplementation(product, resource); @@ -875,7 +875,7 @@ suite('Module Installer only', () => { }); test('Ensure select linter command is invoked', async () => { const product = Product.pylint; - const options = ['Select Linter', 'Do not show again']; + const options = ['Select Linter', "Don't show again"]; const productName = ProductNames.get(product)!; when( appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), @@ -892,7 +892,7 @@ suite('Module Installer only', () => { }); test('If install button is selected, install linter and return response', async () => { const product = Product.pylint; - const options = ['Select Linter', 'Do not show again']; + const options = ['Select Linter', "Don't show again"]; const productName = ProductNames.get(product)!; when( appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts new file mode 100644 index 000000000000..8edaa00dcf73 --- /dev/null +++ b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; +import { EventEmitter, Terminal, Uri } from 'vscode'; +import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../../client/common/application/types'; +import { + IConfigurationService, + IExperimentService, + IPersistentState, + IPersistentStateFactory, + IPythonSettings, +} from '../../../client/common/types'; +import { TerminalEnvVarCollectionPrompt } from '../../../client/interpreter/activation/terminalEnvVarCollectionPrompt'; +import { ITerminalEnvVarCollectionService } from '../../../client/interpreter/activation/types'; +import { Common, Interpreters } from '../../../client/common/utils/localize'; +import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; +import { sleep } from '../../core'; + +suite('Terminal Environment Variable Collection Prompt', () => { + let shell: IApplicationShell; + let terminalManager: ITerminalManager; + let experimentService: IExperimentService; + let activeResourceService: IActiveResourceService; + let terminalEnvVarCollectionService: ITerminalEnvVarCollectionService; + let persistentStateFactory: IPersistentStateFactory; + let terminalEnvVarCollectionPrompt: TerminalEnvVarCollectionPrompt; + let terminalEventEmitter: EventEmitter; + let notificationEnabled: IPersistentState; + let configurationService: IConfigurationService; + const prompts = [Common.doNotShowAgain]; + const message = Interpreters.terminalEnvVarCollectionPrompt; + + setup(async () => { + shell = mock(); + terminalManager = mock(); + experimentService = mock(); + activeResourceService = mock(); + persistentStateFactory = mock(); + terminalEnvVarCollectionService = mock(); + configurationService = mock(); + when(configurationService.getSettings(anything())).thenReturn(({ + terminal: { + activateEnvironment: true, + }, + } as unknown) as IPythonSettings); + notificationEnabled = mock>(); + terminalEventEmitter = new EventEmitter(); + when(persistentStateFactory.createGlobalPersistentState(anything(), true)).thenReturn( + instance(notificationEnabled), + ); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); + when(terminalManager.onDidOpenTerminal).thenReturn(terminalEventEmitter.event); + terminalEnvVarCollectionPrompt = new TerminalEnvVarCollectionPrompt( + instance(shell), + instance(persistentStateFactory), + instance(terminalManager), + [], + instance(activeResourceService), + instance(terminalEnvVarCollectionService), + instance(configurationService), + instance(experimentService), + ); + }); + + test('Show notification when a new terminal is opened for which there is no prompt set', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).once(); + }); + + test('Do not show notification if automatic terminal activation is turned off', async () => { + reset(configurationService); + when(configurationService.getSettings(anything())).thenReturn(({ + terminal: { + activateEnvironment: false, + }, + } as unknown) as IPythonSettings); + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).never(); + }); + + test('When not in experiment, do not show notification for the same', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + reset(experimentService); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).never(); + }); + + test('Do not show notification if notification is disabled', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(false); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).never(); + }); + + test('Do not show notification when a new terminal is opened for which there is prompt set', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(true); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(message, ...prompts)).never(); + }); + + test("Disable notification if `Don't show again` is clicked", async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(notificationEnabled.updateValue(false)).thenResolve(); + when(shell.showInformationMessage(message, ...prompts)).thenReturn(Promise.resolve(Common.doNotShowAgain)); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).once(); + }); + + test('Do not disable notification if prompt is closed', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(notificationEnabled.updateValue(false)).thenResolve(); + when(shell.showInformationMessage(message, ...prompts)).thenReturn(Promise.resolve(undefined)); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).never(); + }); +}); diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 73c9e82274de..86823f16a8c8 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -35,6 +35,8 @@ import { TerminalEnvVarCollectionService } from '../../../client/interpreter/act import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; suite('Terminal Environment Variable Collection Service', () => { let platform: IPlatformService; @@ -262,6 +264,161 @@ suite('Terminal Environment Variable Collection Service', () => { verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); }); + test('Correct track that prompt was set for non-Windows bash where PS1 is set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const ps1Shell = 'bash'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was not set for non-Windows zsh where PS1 is set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('Correct track that prompt was not set for non-Windows where PS1 is not set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Conda, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('Correct track that prompt was set correctly for global interpreters', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: undefined, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(undefined); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was set for Windows when not using powershell', async () => { + when(platform.osType).thenReturn(OSType.Windows); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', ...process.env }; + const windowsShell = 'cmd'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, windowsShell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, windowsShell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was not set for Windows when using powershell', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', ...process.env }; + const windowsShell = 'powershell'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, windowsShell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, windowsShell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + test('If no activated variables are returned for custom shell, fallback to using default shell', async () => { when( environmentActivationService.getActivatedEnvironmentVariables( diff --git a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts index 9671e393dc43..2ad67831c455 100644 --- a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts +++ b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts @@ -223,7 +223,7 @@ suite('Virtual Environment Prompt', () => { notificationPromptEnabled.verifyAll(); }); - test("If user selects 'Do not show again', prompt is disabled", async () => { + test('If user selects "Don\'t show again", prompt is disabled', async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; From 0248fa8b32e8b8e15d682550243e36443a5d8193 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 14 Aug 2023 22:20:52 -0700 Subject: [PATCH 0140/1136] fixing failing tests on CI (#21814) fixing https://github.com/microsoft/vscode-python/issues/21813 --- src/test/testing/common/testingAdapter.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 5c92c7cf3941..1334085e4cea 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -276,7 +276,6 @@ suite('End to End Tests: test adapters', () => { .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns((data) => { traceError(`resolveExecution ${data}`); - console.log(`resolveExecution ${data}`); traceLog(`resolveExecution ${data}`); // do the following asserts for each time resolveExecution is called, should be called once per test. // 1. Check the status, can be subtest success or failure @@ -315,7 +314,7 @@ suite('End to End Tests: test adapters', () => { // verification after discovery is complete resultResolver.verify( (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.exactly(200), + typeMoq.Times.atLeastOnce(), ); }); }); From 5140a8d3e5181785b11448620a66969afbeb089a Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 14 Aug 2023 22:30:09 -0700 Subject: [PATCH 0141/1136] Apply API recommendations for Create Env API (#21804) Closes https://github.com/microsoft/vscode-python/issues/21090 --- .../creation/createEnvironment.ts | 17 ++-- .../creation/proposed.createEnvApis.ts | 99 +++++++++++++------ .../provider/condaCreationProvider.ts | 23 +++-- .../creation/provider/venvCreationProvider.ts | 21 ++-- .../condaCreationProvider.unit.test.ts | 2 - .../venvCreationProvider.unit.test.ts | 2 - 6 files changed, 104 insertions(+), 60 deletions(-) diff --git a/src/client/pythonEnvironments/creation/createEnvironment.ts b/src/client/pythonEnvironments/creation/createEnvironment.ts index 4593ff1abf92..f026a1a82ccd 100644 --- a/src/client/pythonEnvironments/creation/createEnvironment.ts +++ b/src/client/pythonEnvironments/creation/createEnvironment.ts @@ -33,14 +33,12 @@ function fireStartedEvent(options?: CreateEnvironmentOptions): void { } function fireExitedEvent(result?: CreateEnvironmentResult, options?: CreateEnvironmentOptions, error?: Error): void { - onCreateEnvironmentExitedEvent.fire({ - options, - workspaceFolder: result?.workspaceFolder, - path: result?.path, - action: result?.action, - error: error || result?.error, - }); startedEventCount -= 1; + if (result) { + onCreateEnvironmentExitedEvent.fire({ options, ...result }); + } else if (error) { + onCreateEnvironmentExitedEvent.fire({ options, error }); + } } export function getCreationEvents(): { @@ -195,5 +193,8 @@ export async function handleCreateEnvironmentCommand( } } - return result; + if (result) { + return Object.freeze(result); + } + return undefined; } diff --git a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts index 0120b2a6e8d7..ea520fdd27e2 100644 --- a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts +++ b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts @@ -40,40 +40,83 @@ export interface EnvironmentWillCreateEvent { /** * Options used to create a Python environment. */ - options: CreateEnvironmentOptions | undefined; + readonly options: CreateEnvironmentOptions | undefined; } +export type CreateEnvironmentResult = + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action?: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error?: Error; + } + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path?: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error?: Error; + } + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path?: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action?: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error: Error; + }; + /** * Params passed on `onDidCreateEnvironment` event handler. */ -export interface EnvironmentDidCreateEvent extends CreateEnvironmentResult { +export type EnvironmentDidCreateEvent = CreateEnvironmentResult & { /** * Options used to create the Python environment. */ - options: CreateEnvironmentOptions | undefined; -} - -export interface CreateEnvironmentResult { - /** - * Workspace folder associated with the environment. - */ - workspaceFolder: WorkspaceFolder | undefined; - - /** - * Path to the executable python in the environment - */ - path: string | undefined; - - /** - * User action that resulted in exit from the create environment flow. - */ - action: CreateEnvironmentUserActions | undefined; - - /** - * Error if any occurred during environment creation. - */ - error: Error | undefined; -} + readonly options: CreateEnvironmentOptions | undefined; +}; /** * Extensions that want to contribute their own environment creation can do that by registering an object @@ -120,14 +163,14 @@ export interface ProposedCreateEnvironmentAPI { * provider (including internal providers). This will also receive any options passed in * or defaults used to create environment. */ - onWillCreateEnvironment: Event; + readonly onWillCreateEnvironment: Event; /** * This API can be used to detect when the environment provider exits for any registered * provider (including internal providers). This will also receive created environment path, * any errors, or user actions taken from the provider. */ - onDidCreateEnvironment: Event; + readonly onDidCreateEnvironment: Event; /** * This API will show a QuickPick to select an environment provider from available list of diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 39cd40afd41a..74a42808e609 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -4,7 +4,7 @@ import { CancellationToken, ProgressLocation, WorkspaceFolder } from 'vscode'; import * as path from 'path'; import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; -import { traceError, traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog } from '../../../logging'; import { CreateEnvironmentProgress } from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { execObservable } from '../../../common/process/rawProcessApis'; @@ -77,7 +77,7 @@ async function createCondaEnv( args: string[], progress: CreateEnvironmentProgress, token?: CancellationToken, -): Promise { +): Promise { progress.report({ message: CreateEnv.Conda.creating, }); @@ -174,6 +174,7 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise => { - let hasError = false; - progress.report({ message: CreateEnv.statusStarting, }); @@ -237,17 +237,20 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise Python for more info.'); + } else { + throw new Error('A workspace is needed to create conda environment'); } } catch (ex) { traceError(ex); - hasError = true; + showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); throw ex; - } finally { - if (hasError) { - showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); - } } - return { path: envPath, workspaceFolder: workspace, action: undefined, error: undefined }; }, ); } diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index b00682c3cb5d..6c310fd68331 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -8,7 +8,7 @@ import { createVenvScript } from '../../../common/process/internal/scripts'; import { execObservable } from '../../../common/process/rawProcessApis'; import { createDeferred } from '../../../common/utils/async'; import { Common, CreateEnv } from '../../../common/utils/localize'; -import { traceError, traceLog, traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { CreateEnvironmentProgress } from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { IInterpreterQuickPick } from '../../../interpreter/configuration/types'; @@ -144,6 +144,7 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { traceError('Workspace was not selected or found for creating virtual environment.'); return MultiStepAction.Cancel; } + traceInfo(`Selected workspace ${workspace.uri.fsPath} for creating virtual environment.`); return MultiStepAction.Continue; }, undefined, @@ -183,6 +184,7 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { traceError('Virtual env creation requires an interpreter.'); return MultiStepAction.Cancel; } + traceInfo(`Selected interpreter ${interpreter} for creating virtual environment.`); return MultiStepAction.Continue; }, undefined, @@ -237,8 +239,6 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { progress: CreateEnvironmentProgress, token: CancellationToken, ): Promise => { - let hasError = false; - progress.report({ message: CreateEnv.statusStarting, }); @@ -247,18 +247,19 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { try { if (interpreter && workspace) { envPath = await createVenv(workspace, interpreter, args, progress, token); + if (envPath) { + return { path: envPath, workspaceFolder: workspace }; + } + throw new Error('Failed to create virtual environment. See Output > Python for more info.'); } + throw new Error( + 'Failed to create virtual environment. Either interpreter or workspace is undefined.', + ); } catch (ex) { traceError(ex); - hasError = true; + showErrorMessageWithLogs(CreateEnv.Venv.errorCreatingEnvironment); throw ex; - } finally { - if (hasError) { - showErrorMessageWithLogs(CreateEnv.Venv.errorCreatingEnvironment); - } } - - return { path: envPath, workspaceFolder: workspace, action: undefined, error: undefined }; }, ); } diff --git a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts index cb4df95c8c1f..3fcadc42ba09 100644 --- a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -134,8 +134,6 @@ suite('Conda Creation provider tests', () => { assert.deepStrictEqual(await promise, { path: 'new_environment', workspaceFolder: workspace1, - action: undefined, - error: undefined, }); assert.isTrue(showErrorMessageWithLogsStub.notCalled); }); diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index 1c22264f2ada..5bd325c51a0f 100644 --- a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -158,8 +158,6 @@ suite('venv Creation provider tests', () => { assert.deepStrictEqual(actual, { path: 'new_environment', workspaceFolder: workspace1, - action: undefined, - error: undefined, }); interpreterQuickPick.verifyAll(); progressMock.verifyAll(); From 96ba7353c15f530e1848735d2be2b09982c70d9f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 16 Aug 2023 09:35:25 -0700 Subject: [PATCH 0142/1136] fix data to string from buffer for output channel (#21821) fix https://github.com/microsoft/vscode-python/issues/21820 --- src/client/testing/testController/common/server.ts | 4 ++-- .../testing/testController/pytest/pytestDiscoveryAdapter.ts | 4 ++-- .../testing/testController/pytest/pytestExecutionAdapter.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 661290bf4988..8797a861fb4a 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -213,10 +213,10 @@ export class PythonTestServer implements ITestServer, Disposable { // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. result?.proc?.stdout?.on('data', (data) => { - spawnOptions?.outputChannel?.append(data); + spawnOptions?.outputChannel?.append(data.toString()); }); result?.proc?.stderr?.on('data', (data) => { - spawnOptions?.outputChannel?.append(data); + spawnOptions?.outputChannel?.append(data.toString()); }); result?.proc?.on('exit', () => { traceLog('Exec server closed.', uuid); diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 44ab3746dde4..450e2ef1edf2 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -89,10 +89,10 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. result?.proc?.stdout?.on('data', (data) => { - spawnOptions.outputChannel?.append(data); + spawnOptions.outputChannel?.append(data.toString()); }); result?.proc?.stderr?.on('data', (data) => { - spawnOptions.outputChannel?.append(data); + spawnOptions.outputChannel?.append(data.toString()); }); result?.proc?.on('exit', () => { deferredExec.resolve({ stdout: '', stderr: '' }); diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 4a9a57b16fed..96d53db22c1c 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -169,10 +169,10 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. result?.proc?.stdout?.on('data', (data) => { - this.outputChannel?.append(data); + this.outputChannel?.append(data.toString()); }); result?.proc?.stderr?.on('data', (data) => { - this.outputChannel?.append(data); + this.outputChannel?.append(data.toString()); }); result?.proc?.on('exit', () => { From c97945571e24c14bae9d885b359deec1385dff0e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 17 Aug 2023 15:47:03 -0700 Subject: [PATCH 0143/1136] Set workspaceFolder in debug config before substituting command variables (#21835) For https://github.com/microsoft/vscode-python/issues/18482 --- .../debugger/extension/configuration/resolvers/launch.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts index f48b2c19aaff..e4828844a7ad 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -55,6 +55,10 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver Date: Fri, 18 Aug 2023 12:03:59 -0700 Subject: [PATCH 0144/1136] Wrap env collection workspace proposed APIs in `try...catch` block (#21846) Closes https://github.com/microsoft/vscode-python/issues/21831 --- package.json | 2 +- .../terminalEnvVarCollectionService.ts | 37 ++++++++++--------- ...rminalEnvVarCollectionService.unit.test.ts | 1 + 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index fe247f8b25c1..3f98b3d08b6c 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.82.0-20230809" + "vscode": "^1.81.0-20230809" }, "enableTelemetry": false, "keywords": [ diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index a45306132bf4..5dc716f82afa 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -20,7 +20,7 @@ import { } from '../../common/types'; import { Deferred, createDeferred } from '../../common/utils/async'; import { Interpreters } from '../../common/utils/localize'; -import { traceDecoratorVerbose, traceVerbose } from '../../logging'; +import { traceDecoratorVerbose, traceVerbose, traceWarn } from '../../logging'; import { IInterpreterService } from '../contracts'; import { defaultShells } from './service'; import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; @@ -62,8 +62,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ public async activate(resource: Resource): Promise { if (!inTerminalEnvVarExperiment(this.experimentService)) { - const workspaceFolder = this.getWorkspaceFolder(resource); - this.context.getEnvironmentVariableCollection({ workspaceFolder }).clear(); + this.context.environmentVariableCollection.clear(); await this.handleMicroVenv(resource); if (!this.registeredOnce) { this.interpreterService.onDidChangeInterpreter( @@ -227,22 +226,26 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } private async handleMicroVenv(resource: Resource) { - const workspaceFolder = this.getWorkspaceFolder(resource); - const interpreter = await this.interpreterService.getActiveInterpreter(resource); - if (interpreter?.envType === EnvironmentType.Venv) { - const activatePath = path.join(path.dirname(interpreter.path), 'activate'); - if (!(await pathExists(activatePath))) { - const envVarCollection = this.context.getEnvironmentVariableCollection({ workspaceFolder }); - const pathVarName = getSearchPathEnvVarNames()[0]; - envVarCollection.replace( - 'PATH', - `${path.dirname(interpreter.path)}${path.delimiter}${process.env[pathVarName]}`, - { applyAtShellIntegration: true, applyAtProcessCreation: true }, - ); - return; + try { + const workspaceFolder = this.getWorkspaceFolder(resource); + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.envType === EnvironmentType.Venv) { + const activatePath = path.join(path.dirname(interpreter.path), 'activate'); + if (!(await pathExists(activatePath))) { + const envVarCollection = this.context.getEnvironmentVariableCollection({ workspaceFolder }); + const pathVarName = getSearchPathEnvVarNames()[0]; + envVarCollection.replace( + 'PATH', + `${path.dirname(interpreter.path)}${path.delimiter}${process.env[pathVarName]}`, + { applyAtShellIntegration: true, applyAtProcessCreation: true }, + ); + return; + } + this.context.getEnvironmentVariableCollection({ workspaceFolder }).clear(); } + } catch (ex) { + traceWarn(`Microvenv failed as it is using proposed API which is constantly changing`, ex); } - this.context.getEnvironmentVariableCollection({ workspaceFolder }).clear(); } private getWorkspaceFolder(resource: Resource): WorkspaceFolder | undefined { diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 86823f16a8c8..b63750e5bf73 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -124,6 +124,7 @@ suite('Terminal Environment Variable Collection Service', () => { test('When not in experiment, do not apply activated variables to the collection and clear it instead', async () => { reset(experimentService); + when(context.environmentVariableCollection).thenReturn(instance(collection)); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); const applyCollectionStub = sinon.stub(terminalEnvVarCollectionService, '_applyCollection'); applyCollectionStub.resolves(); From 021b3620d132ff4bb3646dc136adf56afec909dc Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 18 Aug 2023 13:50:09 -0700 Subject: [PATCH 0145/1136] Update VS Code engine (#21847) For https://github.com/microsoft/vscode-python/issues/21831 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3f98b3d08b6c..fe247f8b25c1 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.81.0-20230809" + "vscode": "^1.82.0-20230809" }, "enableTelemetry": false, "keywords": [ From 0749b203a3bf49daf7c7509586c41d5e17ff015b Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 21 Aug 2023 10:40:23 -0700 Subject: [PATCH 0146/1136] Show `Python: Clear Workspace interpreter` command regardless of whether a Python file is opened (#21858) Closes https://github.com/microsoft/vscode-python/issues/21850 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fe247f8b25c1..9e5c3e596a43 100644 --- a/package.json +++ b/package.json @@ -1787,7 +1787,7 @@ "category": "Python", "command": "python.clearWorkspaceInterpreter", "title": "%python.command.python.clearWorkspaceInterpreter.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" + "when": "!virtualWorkspace && shellExecutionSupported" }, { "category": "Python", From cfbf1f3dabf05a375a50cc46b4d15a7368a2f3e7 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 21 Aug 2023 10:52:32 -0700 Subject: [PATCH 0147/1136] remove usage of pytest CollectReport in rewrite (#21859) as per https://docs.pytest.org/en/7.1.x/reference/reference.html#collectreport, `CollectReport` is experimental and therefore it should not be in our extension. Fixes https://github.com/microsoft/vscode-python/issues/21784 --- pythonFiles/vscode_pytest/__init__.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 49d429662e3a..adf72c134119 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -46,6 +46,15 @@ def __init__(self, message): ERRORS = [] +IS_DISCOVERY = False +map_id_to_path = dict() +collected_tests_so_far = list() + + +def pytest_load_initial_conftests(early_config, parser, args): + if "--collect-only" in args: + global IS_DISCOVERY + IS_DISCOVERY = True def pytest_internalerror(excrepr, excinfo): @@ -70,7 +79,7 @@ def pytest_exception_interact(node, call, report): # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. # call.excinfo.exconly() returns the exception as a string. # If it is during discovery, then add the error to error logs. - if type(report) == pytest.CollectReport: + if IS_DISCOVERY: if call.excinfo and call.excinfo.typename != "AssertionError": if report.outcome == "skipped" and "SkipTest" in str(call): return @@ -168,19 +177,6 @@ class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): tests: Dict[str, TestOutcome] -IS_DISCOVERY = False -map_id_to_path = dict() - - -def pytest_load_initial_conftests(early_config, parser, args): - if "--collect-only" in args: - global IS_DISCOVERY - IS_DISCOVERY = True - - -collected_tests_so_far = list() - - def pytest_report_teststatus(report, config): """ A pytest hook that is called when a test is called. It is called 3 times per test, From 15bb9748a533c148df3e2b5a202fb3e9e57731b6 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 21 Aug 2023 22:39:59 -0700 Subject: [PATCH 0148/1136] Do not filter using scheme when filtering environments (#21862) For https://github.com/microsoft/vscode-python/issues/21825 On codespaces, it was leading to workspace environments not being displayed, which could mess up auto-selection. --- src/client/common/utils/misc.ts | 7 +++---- src/client/pythonEnvironments/base/info/env.ts | 8 +++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/client/common/utils/misc.ts b/src/client/common/utils/misc.ts index c95a3cc75575..455392d28eb1 100644 --- a/src/client/common/utils/misc.ts +++ b/src/client/common/utils/misc.ts @@ -60,7 +60,7 @@ function isUri(resource?: Uri | any): resource is Uri { /** * Create a filter func that determine if the given URI and candidate match. * - * The scheme must match, as well as path. + * Only compares path. * * @param checkParent - if `true`, match if the candidate is rooted under `uri` * or if the candidate matches `uri` exactly. @@ -80,9 +80,8 @@ export function getURIFilter( } const uriRoot = `${uriPath}/`; function filter(candidate: Uri): boolean { - if (candidate.scheme !== uri.scheme) { - return false; - } + // Do not compare schemes as it is sometimes not available, in + // which case file is assumed as scheme. let candidatePath = candidate.path; while (candidatePath.endsWith('/')) { candidatePath = candidatePath.slice(0, -1); diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index 2527f18202cd..12b3e519b944 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -87,7 +87,13 @@ export function areEnvsDeepEqual(env1: PythonEnvInfo, env2: PythonEnvInfo): bool env2Clone.source = env2Clone.source.sort(); const searchLocation1 = env1.searchLocation?.fsPath ?? ''; const searchLocation2 = env2.searchLocation?.fsPath ?? ''; - return isEqual(env1Clone, env2Clone) && arePathsSame(searchLocation1, searchLocation2); + const searchLocation1Scheme = env1.searchLocation?.scheme ?? ''; + const searchLocation2Scheme = env2.searchLocation?.scheme ?? ''; + return ( + isEqual(env1Clone, env2Clone) && + arePathsSame(searchLocation1, searchLocation2) && + searchLocation1Scheme === searchLocation2Scheme + ); } /** From 30e26c2dc47981f03dbcfdfb66440ebe9b211de8 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 23 Aug 2023 11:25:17 -0700 Subject: [PATCH 0149/1136] Update proposed API for env collection (#21819) For https://github.com/microsoft/vscode-python/issues/20822 Blocked on https://github.com/microsoft/vscode/issues/171173#issuecomment-1679208632 --- package-lock.json | 2 +- package.json | 2 +- .../terminalEnvVarCollectionService.ts | 93 ++++++++++++------- ...rminalEnvVarCollectionService.unit.test.ts | 50 ++++++++-- ...scode.proposed.envCollectionWorkspace.d.ts | 50 +++++----- 5 files changed, 127 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index 84357451dc3c..e001c51ee220 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.82.0-20230809" + "vscode": "^1.82.0-20230823" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index 9e5c3e596a43..bc7378aa521b 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.82.0-20230809" + "vscode": "^1.82.0-20230823" }, "enableTelemetry": false, "keywords": [ diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 5dc716f82afa..33d51e7e2ed0 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -3,7 +3,14 @@ import * as path from 'path'; import { inject, injectable } from 'inversify'; -import { ProgressOptions, ProgressLocation, MarkdownString, WorkspaceFolder } from 'vscode'; +import { + ProgressOptions, + ProgressLocation, + MarkdownString, + WorkspaceFolder, + GlobalEnvironmentVariableCollection, + EnvironmentVariableScope, +} from 'vscode'; import { pathExists } from 'fs-extra'; import { IExtensionActivationService } from '../../activation/types'; import { IApplicationShell, IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; @@ -20,7 +27,7 @@ import { } from '../../common/types'; import { Deferred, createDeferred } from '../../common/utils/async'; import { Interpreters } from '../../common/utils/localize'; -import { traceDecoratorVerbose, traceVerbose, traceWarn } from '../../logging'; +import { traceDecoratorVerbose, traceError, traceVerbose, traceWarn } from '../../logging'; import { IInterpreterService } from '../contracts'; import { defaultShells } from './service'; import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; @@ -61,52 +68,56 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ ) {} public async activate(resource: Resource): Promise { - if (!inTerminalEnvVarExperiment(this.experimentService)) { - this.context.environmentVariableCollection.clear(); - await this.handleMicroVenv(resource); + try { + if (!inTerminalEnvVarExperiment(this.experimentService)) { + this.context.environmentVariableCollection.clear(); + await this.handleMicroVenv(resource); + if (!this.registeredOnce) { + this.interpreterService.onDidChangeInterpreter( + async (r) => { + await this.handleMicroVenv(r); + }, + this, + this.disposables, + ); + this.registeredOnce = true; + } + return; + } if (!this.registeredOnce) { this.interpreterService.onDidChangeInterpreter( async (r) => { - await this.handleMicroVenv(r); + this.showProgress(); + await this._applyCollection(r).ignoreErrors(); + this.hideProgress(); + }, + this, + this.disposables, + ); + this.applicationEnvironment.onDidChangeShell( + async (shell: string) => { + this.showProgress(); + this.processEnvVars = undefined; + // Pass in the shell where known instead of relying on the application environment, because of bug + // on VSCode: https://github.com/microsoft/vscode/issues/160694 + await this._applyCollection(undefined, shell).ignoreErrors(); + this.hideProgress(); }, this, this.disposables, ); this.registeredOnce = true; } - return; - } - if (!this.registeredOnce) { - this.interpreterService.onDidChangeInterpreter( - async (r) => { - this.showProgress(); - await this._applyCollection(r).ignoreErrors(); - this.hideProgress(); - }, - this, - this.disposables, - ); - this.applicationEnvironment.onDidChangeShell( - async (shell: string) => { - this.showProgress(); - this.processEnvVars = undefined; - // Pass in the shell where known instead of relying on the application environment, because of bug - // on VSCode: https://github.com/microsoft/vscode/issues/160694 - await this._applyCollection(undefined, shell).ignoreErrors(); - this.hideProgress(); - }, - this, - this.disposables, - ); - this.registeredOnce = true; + this._applyCollection(resource).ignoreErrors(); + } catch (ex) { + traceError(`Activating terminal env collection failed`, ex); } - this._applyCollection(resource).ignoreErrors(); } public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { const workspaceFolder = this.getWorkspaceFolder(resource); const settings = this.configurationService.getSettings(resource); - const envVarCollection = this.context.getEnvironmentVariableCollection({ workspaceFolder }); + const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); // Clear any previously set env vars from collection envVarCollection.clear(); if (!settings.terminal.activateEnvironment) { @@ -221,6 +232,13 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ // PS1 should be set but no PS1 was set. return; } + const config = this.workspaceService + .getConfiguration('terminal') + .get('integrated.shellIntegration.enabled'); + if (!config) { + traceVerbose('PS1 is not set when shell integration is disabled.'); + return; + } } this.terminalPromptIsCorrect(resource); } @@ -232,7 +250,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (interpreter?.envType === EnvironmentType.Venv) { const activatePath = path.join(path.dirname(interpreter.path), 'activate'); if (!(await pathExists(activatePath))) { - const envVarCollection = this.context.getEnvironmentVariableCollection({ workspaceFolder }); + const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); const pathVarName = getSearchPathEnvVarNames()[0]; envVarCollection.replace( 'PATH', @@ -241,13 +259,18 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ ); return; } - this.context.getEnvironmentVariableCollection({ workspaceFolder }).clear(); + this.getEnvironmentVariableCollection({ workspaceFolder }).clear(); } } catch (ex) { traceWarn(`Microvenv failed as it is using proposed API which is constantly changing`, ex); } } + private getEnvironmentVariableCollection(scope: EnvironmentVariableScope = {}) { + const envVarCollection = this.context.environmentVariableCollection as GlobalEnvironmentVariableCollection; + return envVarCollection.getScoped(scope); + } + private getWorkspaceFolder(resource: Resource): WorkspaceFolder | undefined { let workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); if ( diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index b63750e5bf73..7393f4ad07ad 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -9,9 +9,10 @@ import { mock, instance, when, anything, verify, reset } from 'ts-mockito'; import { EnvironmentVariableCollection, EnvironmentVariableMutatorOptions, - EnvironmentVariableScope, + GlobalEnvironmentVariableCollection, ProgressLocation, Uri, + WorkspaceConfiguration, WorkspaceFolder, } from 'vscode'; import { @@ -44,12 +45,12 @@ suite('Terminal Environment Variable Collection Service', () => { let context: IExtensionContext; let shell: IApplicationShell; let experimentService: IExperimentService; - let collection: EnvironmentVariableCollection & { - getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; - }; + let collection: EnvironmentVariableCollection; + let globalCollection: GlobalEnvironmentVariableCollection; let applicationEnvironment: IApplicationEnvironment; let environmentActivationService: IEnvironmentActivationService; let workspaceService: IWorkspaceService; + let workspaceConfig: WorkspaceConfiguration; let terminalEnvVarCollectionService: TerminalEnvVarCollectionService; const progressOptions = { location: ProgressLocation.Window, @@ -62,19 +63,20 @@ suite('Terminal Environment Variable Collection Service', () => { setup(() => { workspaceService = mock(); + workspaceConfig = mock(); when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); when(workspaceService.workspaceFolders).thenReturn(undefined); + when(workspaceService.getConfiguration('terminal')).thenReturn(instance(workspaceConfig)); + when(workspaceConfig.get('integrated.shellIntegration.enabled')).thenReturn(true); platform = mock(); when(platform.osType).thenReturn(getOSType()); interpreterService = mock(); context = mock(); shell = mock(); - collection = mock< - EnvironmentVariableCollection & { - getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection; - } - >(); - when(context.getEnvironmentVariableCollection(anything())).thenReturn(instance(collection)); + globalCollection = mock(); + collection = mock(); + when(context.environmentVariableCollection).thenReturn(instance(globalCollection)); + when(globalCollection.getScoped(anything())).thenReturn(instance(collection)); experimentService = mock(); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); applicationEnvironment = mock(); @@ -291,6 +293,34 @@ suite('Terminal Environment Variable Collection Service', () => { expect(result).to.equal(true); }); + test('Correct track that prompt was set for PS1 if shell integration is disabled', async () => { + reset(workspaceConfig); + when(workspaceConfig.get('integrated.shellIntegration.enabled')).thenReturn(false); + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const ps1Shell = 'bash'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + test('Correct track that prompt was not set for non-Windows zsh where PS1 is set', async () => { when(platform.osType).thenReturn(OSType.Linux); const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; diff --git a/types/vscode.proposed.envCollectionWorkspace.d.ts b/types/vscode.proposed.envCollectionWorkspace.d.ts index 406e5b98cd47..494929ba15eb 100644 --- a/types/vscode.proposed.envCollectionWorkspace.d.ts +++ b/types/vscode.proposed.envCollectionWorkspace.d.ts @@ -4,30 +4,34 @@ *--------------------------------------------------------------------------------------------*/ declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/171173 - // https://github.com/microsoft/vscode/issues/182069 + // export interface ExtensionContext { + // /** + // * Gets the extension's global environment variable collection for this workspace, enabling changes to be + // * applied to terminal environment variables. + // */ + // readonly environmentVariableCollection: GlobalEnvironmentVariableCollection; + // } - export interface ExtensionContext { - /** - * Gets the extension's environment variable collection for this workspace, enabling changes - * to be applied to terminal environment variables. - * - * @deprecated Use {@link getEnvironmentVariableCollection} instead. - */ - readonly environmentVariableCollection: EnvironmentVariableCollection; - /** - * Gets the extension's environment variable collection for this workspace, enabling changes - * to be applied to terminal environment variables. - * - * @param scope The scope to which the environment variable collection applies to. - */ - getEnvironmentVariableCollection(scope?: EnvironmentVariableScope): EnvironmentVariableCollection; - } + export interface GlobalEnvironmentVariableCollection extends EnvironmentVariableCollection { + /** + * Gets scope-specific environment variable collection for the extension. This enables alterations to + * terminal environment variables solely within the designated scope, and is applied in addition to (and + * after) the global collection. + * + * Each object obtained through this method is isolated and does not impact objects for other scopes, + * including the global collection. + * + * @param scope The scope to which the environment variable collection applies to. + */ + getScoped(scope: EnvironmentVariableScope): EnvironmentVariableCollection; + } - export type EnvironmentVariableScope = { - /** - * Any specific workspace folder to get collection for. If unspecified, collection applicable to all workspace folders is returned. - */ - workspaceFolder?: WorkspaceFolder; - }; + export type EnvironmentVariableScope = { + /** + * Any specific workspace folder to get collection for. If unspecified, collection applicable to all workspace folders is returned. + */ + workspaceFolder?: WorkspaceFolder; + }; } From 3fa5d4bda885ed0a430229ef5f3fe5585c9e6249 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 25 Aug 2023 09:44:12 -0700 Subject: [PATCH 0150/1136] Support for Create Env command to re-create env for venv (#21829) Closes https://github.com/microsoft/vscode-python/issues/21827 --- src/client/common/utils/localize.ts | 7 + .../creation/common/commonUtils.ts | 19 +++ .../creation/provider/venvCreationProvider.ts | 141 ++++++++++++----- .../creation/provider/venvDeleteUtils.ts | 99 ++++++++++++ .../creation/provider/venvSwitchPython.ts | 28 ++++ .../creation/provider/venvUtils.ts | 78 +++++++++- src/client/telemetry/constants.ts | 2 + src/client/telemetry/index.ts | 24 +++ .../venvCreationProvider.unit.test.ts | 117 +++++++++++++++ .../provider/venvDeleteUtils.unit.test.ts | 142 ++++++++++++++++++ .../creation/provider/venvUtils.unit.test.ts | 68 ++++++++- 11 files changed, 685 insertions(+), 40 deletions(-) create mode 100644 src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts create mode 100644 src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts create mode 100644 src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 3ec1829201f8..4401cab56176 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -464,6 +464,13 @@ export namespace CreateEnv { export const error = l10n.t('Creating virtual environment failed with error.'); export const tomlExtrasQuickPickTitle = l10n.t('Select optional dependencies to install from pyproject.toml'); export const requirementsQuickPickTitle = l10n.t('Select dependencies to install'); + export const recreate = l10n.t('Recreate'); + export const recreateDescription = l10n.t('Delete existing ".venv" environment and create a new one'); + export const useExisting = l10n.t('Use Existing'); + export const useExistingDescription = l10n.t('Use existing ".venv" environment with no changes to it'); + export const existingVenvQuickPickPlaceholder = l10n.t('Use or Recreate existing environment?'); + export const deletingEnvironmentProgress = l10n.t('Deleting existing ".venv" environment...'); + export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".venv" environment.'); } export namespace Conda { diff --git a/src/client/pythonEnvironments/creation/common/commonUtils.ts b/src/client/pythonEnvironments/creation/common/commonUtils.ts index 0e303e70138e..b4d4a37eae9b 100644 --- a/src/client/pythonEnvironments/creation/common/commonUtils.ts +++ b/src/client/pythonEnvironments/creation/common/commonUtils.ts @@ -1,9 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { WorkspaceFolder } from 'vscode'; import { Commands } from '../../../common/constants'; import { Common } from '../../../common/utils/localize'; import { executeCommand } from '../../../common/vscodeApis/commandApis'; import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; +import { isWindows } from '../../../common/platform/platformService'; export async function showErrorMessageWithLogs(message: string): Promise { const result = await showErrorMessage(message, Common.openOutputPanel, Common.selectPythonInterpreter); @@ -13,3 +17,18 @@ export async function showErrorMessageWithLogs(message: string): Promise { await executeCommand(Commands.Set_Interpreter); } } + +export function getVenvPath(workspaceFolder: WorkspaceFolder): string { + return path.join(workspaceFolder.uri.fsPath, '.venv'); +} + +export async function hasVenv(workspaceFolder: WorkspaceFolder): Promise { + return fs.pathExists(getVenvPath(workspaceFolder)); +} + +export function getVenvExecutable(workspaceFolder: WorkspaceFolder): string { + if (isWindows()) { + return path.join(getVenvPath(workspaceFolder), 'Scripts', 'python.exe'); + } + return path.join(getVenvPath(workspaceFolder), 'bin', 'python'); +} diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index 6c310fd68331..ba47edcf6f30 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -17,8 +17,14 @@ import { MultiStepAction, MultiStepNode, withProgress } from '../../../common/vs import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { VenvProgressAndTelemetry, VENV_CREATED_MARKER, VENV_EXISTING_MARKER } from './venvProgressAndTelemetry'; -import { showErrorMessageWithLogs } from '../common/commonUtils'; -import { IPackageInstallSelection, pickPackagesToInstall } from './venvUtils'; +import { getVenvExecutable, showErrorMessageWithLogs } from '../common/commonUtils'; +import { + ExistingVenvAction, + IPackageInstallSelection, + deleteEnvironment, + pickExistingVenvAction, + pickPackagesToInstall, +} from './venvUtils'; import { InputFlowAction } from '../../../common/utils/multiStepInput'; import { CreateEnvironmentProvider, @@ -150,33 +156,66 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { undefined, ); - let interpreter: string | undefined; - const interpreterStep = new MultiStepNode( + let existingVenvAction: ExistingVenvAction | undefined; + const existingEnvStep = new MultiStepNode( workspaceStep, - async () => { - if (workspace) { + async (context?: MultiStepAction) => { + if (workspace && context === MultiStepAction.Continue) { try { - interpreter = await this.interpreterQuickPick.getInterpreterViaQuickPick( - workspace.uri, - (i: PythonEnvironment) => - [ - EnvironmentType.System, - EnvironmentType.MicrosoftStore, - EnvironmentType.Global, - EnvironmentType.Pyenv, - ].includes(i.envType) && i.type === undefined, // only global intepreters - { - skipRecommended: true, - showBackButton: true, - placeholder: CreateEnv.Venv.selectPythonPlaceHolder, - title: null, - }, - ); + existingVenvAction = await pickExistingVenvAction(workspace); + return MultiStepAction.Continue; } catch (ex) { - if (ex === InputFlowAction.back) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + return MultiStepAction.Continue; + }, + undefined, + ); + workspaceStep.next = existingEnvStep; + + let interpreter: string | undefined; + const interpreterStep = new MultiStepNode( + existingEnvStep, + async (context?: MultiStepAction) => { + if (workspace) { + if ( + existingVenvAction === ExistingVenvAction.Recreate || + existingVenvAction === ExistingVenvAction.Create + ) { + try { + interpreter = await this.interpreterQuickPick.getInterpreterViaQuickPick( + workspace.uri, + (i: PythonEnvironment) => + [ + EnvironmentType.System, + EnvironmentType.MicrosoftStore, + EnvironmentType.Global, + EnvironmentType.Pyenv, + ].includes(i.envType) && i.type === undefined, // only global intepreters + { + skipRecommended: true, + showBackButton: true, + placeholder: CreateEnv.Venv.selectPythonPlaceHolder, + title: null, + }, + ); + } catch (ex) { + if (ex === InputFlowAction.back) { + return MultiStepAction.Back; + } + interpreter = undefined; + } + } else if (existingVenvAction === ExistingVenvAction.UseExisting) { + if (context === MultiStepAction.Back) { return MultiStepAction.Back; } - interpreter = undefined; + interpreter = getVenvExecutable(workspace); } } @@ -189,7 +228,7 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { }, undefined, ); - workspaceStep.next = interpreterStep; + existingEnvStep.next = interpreterStep; let addGitIgnore = true; let installPackages = true; @@ -200,19 +239,23 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { let installInfo: IPackageInstallSelection[] | undefined; const packagesStep = new MultiStepNode( interpreterStep, - async () => { + async (context?: MultiStepAction) => { if (workspace && installPackages) { - try { - installInfo = await pickPackagesToInstall(workspace); - } catch (ex) { - if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { - return ex; + if (existingVenvAction !== ExistingVenvAction.UseExisting) { + try { + installInfo = await pickPackagesToInstall(workspace); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; } - throw ex; - } - if (!installInfo) { - traceVerbose('Virtual env creation exited during dependencies selection.'); - return MultiStepAction.Cancel; + if (!installInfo) { + traceVerbose('Virtual env creation exited during dependencies selection.'); + return MultiStepAction.Cancel; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; } } @@ -227,6 +270,32 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { throw action; } + if (workspace) { + if (existingVenvAction === ExistingVenvAction.Recreate) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'triggered', + }); + if (await deleteEnvironment(workspace, interpreter)) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'deleted', + }); + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'failed', + }); + throw MultiStepAction.Cancel; + } + } else if (existingVenvAction === ExistingVenvAction.UseExisting) { + sendTelemetryEvent(EventName.ENVIRONMENT_REUSE, undefined, { + environmentType: 'venv', + }); + return { path: getVenvExecutable(workspace), workspaceFolder: workspace }; + } + } + const args = generateCommandArgs(installInfo, addGitIgnore); return withProgress( diff --git a/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts b/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts new file mode 100644 index 000000000000..46a0adf0f228 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { WorkspaceFolder } from 'vscode'; +import { traceError, traceInfo } from '../../../logging'; +import { getVenvPath, showErrorMessageWithLogs } from '../common/commonUtils'; +import { CreateEnv } from '../../../common/utils/localize'; +import { sleep } from '../../../common/utils/async'; +import { switchSelectedPython } from './venvSwitchPython'; + +async function tryDeleteFile(file: string): Promise { + try { + if (!(await fs.pathExists(file))) { + return true; + } + await fs.unlink(file); + return true; + } catch (err) { + traceError(`Failed to delete file [${file}]:`, err); + return false; + } +} + +async function tryDeleteDir(dir: string): Promise { + try { + if (!(await fs.pathExists(dir))) { + return true; + } + await fs.rmdir(dir, { + recursive: true, + maxRetries: 10, + retryDelay: 200, + }); + return true; + } catch (err) { + traceError(`Failed to delete directory [${dir}]:`, err); + return false; + } +} + +export async function deleteEnvironmentNonWindows(workspaceFolder: WorkspaceFolder): Promise { + const venvPath = getVenvPath(workspaceFolder); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted venv dir: ${venvPath}`); + return true; + } + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; +} + +export async function deleteEnvironmentWindows( + workspaceFolder: WorkspaceFolder, + interpreter: string | undefined, +): Promise { + const venvPath = getVenvPath(workspaceFolder); + const venvPythonPath = path.join(venvPath, 'Scripts', 'python.exe'); + + if (await tryDeleteFile(venvPythonPath)) { + traceInfo(`Deleted python executable: ${venvPythonPath}`); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted ".venv" dir: ${venvPath}`); + return true; + } + + traceError(`Failed to delete ".venv" dir: ${venvPath}`); + traceError( + 'This happens if the virtual environment is still in use, or some binary in the venv is still running.', + ); + traceError(`Please delete the ".venv" manually: [${venvPath}]`); + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; + } + traceError(`Failed to delete python executable: ${venvPythonPath}`); + traceError('This happens if the virtual environment is still in use.'); + + if (interpreter) { + traceError('We will attempt to switch python temporarily to delete the ".venv"'); + + await switchSelectedPython(interpreter, workspaceFolder.uri, 'temporarily to delete the ".venv"'); + + traceInfo(`Attempting to delete ".venv" again: ${venvPath}`); + const ms = 500; + for (let i = 0; i < 5; i = i + 1) { + traceInfo(`Waiting for ${ms}ms to let processes exit, before a delete attempt.`); + await sleep(ms); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted ".venv" dir: ${venvPath}`); + return true; + } + traceError(`Failed to delete ".venv" dir [${venvPath}] (attempt ${i + 1}/5).`); + } + } else { + traceError(`Please delete the ".venv" dir manually: [${venvPath}]`); + } + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; +} diff --git a/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts b/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts new file mode 100644 index 000000000000..e2567dfd114b --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { Disposable, Uri } from 'vscode'; +import { createDeferred } from '../../../common/utils/async'; +import { getExtension } from '../../../common/vscodeApis/extensionsApi'; +import { PVSC_EXTENSION_ID, PythonExtension } from '../../../api/types'; +import { traceInfo } from '../../../logging'; + +export async function switchSelectedPython(interpreter: string, uri: Uri, purpose: string): Promise { + let dispose: Disposable | undefined; + try { + const deferred = createDeferred(); + const api: PythonExtension = getExtension(PVSC_EXTENSION_ID)?.exports as PythonExtension; + dispose = api.environments.onDidChangeActiveEnvironmentPath(async (e) => { + if (path.normalize(e.path) === path.normalize(interpreter)) { + traceInfo(`Switched to interpreter ${purpose}: ${interpreter}`); + deferred.resolve(); + } + }); + api.environments.updateActiveEnvironmentPath(interpreter, uri); + traceInfo(`Switching interpreter ${purpose}: ${interpreter}`); + await deferred.promise; + } finally { + dispose?.dispose(); + } +} diff --git a/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/src/client/pythonEnvironments/creation/provider/venvUtils.ts index 7c6505082fbc..d7a0be170f99 100644 --- a/src/client/pythonEnvironments/creation/provider/venvUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -5,11 +5,20 @@ import * as tomljs from '@iarna/toml'; import * as fs from 'fs-extra'; import { flatten, isArray } from 'lodash'; import * as path from 'path'; -import { CancellationToken, QuickPickItem, RelativePattern, WorkspaceFolder } from 'vscode'; -import { CreateEnv } from '../../../common/utils/localize'; -import { MultiStepAction, MultiStepNode, showQuickPickWithBack } from '../../../common/vscodeApis/windowApis'; +import { CancellationToken, ProgressLocation, QuickPickItem, RelativePattern, WorkspaceFolder } from 'vscode'; +import { Common, CreateEnv } from '../../../common/utils/localize'; +import { + MultiStepAction, + MultiStepNode, + showQuickPickWithBack, + withProgress, +} from '../../../common/vscodeApis/windowApis'; import { findFiles } from '../../../common/vscodeApis/workspaceApis'; import { traceError, traceVerbose } from '../../../logging'; +import { Commands } from '../../../common/constants'; +import { isWindows } from '../../../common/platform/platformService'; +import { getVenvPath, hasVenv } from '../common/commonUtils'; +import { deleteEnvironmentNonWindows, deleteEnvironmentWindows } from './venvDeleteUtils'; const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**'; async function getPipRequirementsFiles( @@ -226,3 +235,66 @@ export async function pickPackagesToInstall( return packages; } + +export async function deleteEnvironment( + workspaceFolder: WorkspaceFolder, + interpreter: string | undefined, +): Promise { + const venvPath = getVenvPath(workspaceFolder); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.Venv.deletingEnvironmentProgress} ([${Common.showLogs}](command:${Commands.ViewOutput})): ${venvPath}`, + cancellable: false, + }, + async () => { + if (isWindows()) { + return deleteEnvironmentWindows(workspaceFolder, interpreter); + } + return deleteEnvironmentNonWindows(workspaceFolder); + }, + ); +} + +export enum ExistingVenvAction { + Recreate, + UseExisting, + Create, +} + +export async function pickExistingVenvAction( + workspaceFolder: WorkspaceFolder | undefined, +): Promise { + if (workspaceFolder) { + if (await hasVenv(workspaceFolder)) { + const items: QuickPickItem[] = [ + { label: CreateEnv.Venv.recreate, description: CreateEnv.Venv.recreateDescription }, + { + label: CreateEnv.Venv.useExisting, + description: CreateEnv.Venv.useExistingDescription, + }, + ]; + + const selection = (await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Venv.existingVenvQuickPickPlaceholder, + ignoreFocusOut: true, + }, + undefined, + )) as QuickPickItem | undefined; + + if (selection?.label === CreateEnv.Venv.recreate) { + return ExistingVenvAction.Recreate; + } + + if (selection?.label === CreateEnv.Venv.useExisting) { + return ExistingVenvAction.UseExisting; + } + } else { + return ExistingVenvAction.Create; + } + } + + throw MultiStepAction.Cancel; +} diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 159f5690e5c5..a729b3d491e8 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -112,6 +112,8 @@ export enum EventName { ENVIRONMENT_INSTALLED_PACKAGES = 'ENVIRONMENT.INSTALLED_PACKAGES', ENVIRONMENT_INSTALLING_PACKAGES_FAILED = 'ENVIRONMENT.INSTALLING_PACKAGES_FAILED', ENVIRONMENT_BUTTON = 'ENVIRONMENT.BUTTON', + ENVIRONMENT_DELETE = 'ENVIRONMENT.DELETE', + ENVIRONMENT_REUSE = 'ENVIRONMENT.REUSE', TOOLS_EXTENSIONS_ALREADY_INSTALLED = 'TOOLS_EXTENSIONS.ALREADY_INSTALLED', TOOLS_EXTENSIONS_PROMPT_SHOWN = 'TOOLS_EXTENSIONS.PROMPT_SHOWN', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index a068f21d2327..f4947cd73f05 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -2120,6 +2120,30 @@ export interface IEventNamePropertyMapping { "environment.button" : {"owner": "karthiknadig" } */ [EventName.ENVIRONMENT_BUTTON]: never | undefined; + /** + * Telemetry event if user selected to delete the existing environment. + */ + /* __GDPR__ + "environment.delete" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "status" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_DELETE]: { + environmentType: 'venv' | 'conda'; + status: 'triggered' | 'deleted' | 'failed'; + }; + /** + * Telemetry event if user selected to re-use the existing environment. + */ + /* __GDPR__ + "environment.reuse" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_REUSE]: { + environmentType: 'venv' | 'conda'; + }; /** * Telemetry event sent when a linter or formatter extension is already installed. */ diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index 5bd325c51a0f..280d05bf5935 100644 --- a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -35,6 +35,8 @@ suite('venv Creation provider tests', () => { let withProgressStub: sinon.SinonStub; let showErrorMessageWithLogsStub: sinon.SinonStub; let pickPackagesToInstallStub: sinon.SinonStub; + let pickExistingVenvActionStub: sinon.SinonStub; + let deleteEnvironmentStub: sinon.SinonStub; const workspace1 = { uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), @@ -43,6 +45,8 @@ suite('venv Creation provider tests', () => { }; setup(() => { + pickExistingVenvActionStub = sinon.stub(venvUtils, 'pickExistingVenvAction'); + deleteEnvironmentStub = sinon.stub(venvUtils, 'deleteEnvironment'); pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); interpreterQuickPick = typemoq.Mock.ofType(); @@ -54,6 +58,9 @@ suite('venv Creation provider tests', () => { progressMock = typemoq.Mock.ofType(); venvProvider = new VenvCreationProvider(interpreterQuickPick.object); + + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Create); + deleteEnvironmentStub.resolves(true); }); teardown(() => { @@ -70,6 +77,8 @@ suite('venv Creation provider tests', () => { assert.isTrue(pickWorkspaceFolderStub.calledOnce); interpreterQuickPick.verifyAll(); assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(pickExistingVenvActionStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('No Python selected', async () => { @@ -85,6 +94,7 @@ suite('venv Creation provider tests', () => { assert.isTrue(pickWorkspaceFolderStub.calledOnce); interpreterQuickPick.verifyAll(); assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('User pressed Esc while selecting dependencies', async () => { @@ -99,6 +109,7 @@ suite('venv Creation provider tests', () => { await assert.isRejected(venvProvider.createEnvironment()); assert.isTrue(pickPackagesToInstallStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('Create venv with python selected by user no packages selected', async () => { @@ -162,6 +173,7 @@ suite('venv Creation provider tests', () => { interpreterQuickPick.verifyAll(); progressMock.verifyAll(); assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('Create venv failed', async () => { @@ -216,6 +228,7 @@ suite('venv Creation provider tests', () => { _complete!(); await assert.isRejected(promise); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); }); test('Create venv failed (non-zero exit code)', async () => { @@ -274,5 +287,109 @@ suite('venv Creation provider tests', () => { interpreterQuickPick.verifyAll(); progressMock.verifyAll(); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('Create venv with pre-existing .venv, user selects re-create', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Recreate); + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.calledOnce); + }); + + test('Create venv with pre-existing .venv, user selects re-create, delete env failed', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Recreate); + pickWorkspaceFolderStub.resolves(workspace1); + deleteEnvironmentStub.resolves(false); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + await assert.isRejected(venvProvider.createEnvironment()); + + interpreterQuickPick.verifyAll(); + assert.isTrue(withProgressStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.calledOnce); + }); + + test('Create venv with pre-existing .venv, user selects use existing', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.UseExisting); + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.never()); + + pickPackagesToInstallStub.resolves([]); + + interpreterQuickPick.verifyAll(); + assert.isTrue(withProgressStub.notCalled); + assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); }); }); diff --git a/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts new file mode 100644 index 000000000000..d075979b70b1 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as fs from 'fs-extra'; +import * as sinon from 'sinon'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { assert } from 'chai'; +import * as path from 'path'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { + deleteEnvironmentNonWindows, + deleteEnvironmentWindows, +} from '../../../../client/pythonEnvironments/creation/provider/venvDeleteUtils'; +import * as switchPython from '../../../../client/pythonEnvironments/creation/provider/venvSwitchPython'; +import * as asyncApi from '../../../../client/common/utils/async'; + +suite('Test Delete environments (windows)', () => { + let pathExistsStub: sinon.SinonStub; + let rmdirStub: sinon.SinonStub; + let unlinkStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + let switchPythonStub: sinon.SinonStub; + let sleepStub: sinon.SinonStub; + + const workspace1: WorkspaceFolder = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + pathExistsStub.resolves(true); + + rmdirStub = sinon.stub(fs, 'rmdir'); + unlinkStub = sinon.stub(fs, 'unlink'); + + sleepStub = sinon.stub(asyncApi, 'sleep'); + sleepStub.resolves(); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + + switchPythonStub = sinon.stub(switchPython, 'switchSelectedPython'); + switchPythonStub.resolves(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete venv folder succeeded', async () => { + rmdirStub.resolves(); + unlinkStub.resolves(); + assert.ok(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(unlinkStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete python.exe succeeded but venv dir failed', async () => { + rmdirStub.rejects(); + unlinkStub.resolves(); + assert.notOk(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(unlinkStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete python.exe failed first attempt', async () => { + unlinkStub.rejects(); + rmdirStub.resolves(); + assert.ok(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(switchPythonStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete python.exe failed all attempts', async () => { + unlinkStub.rejects(); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentWindows(workspace1, 'python.exe')); + assert.ok(switchPythonStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete python.exe failed no interpreter', async () => { + unlinkStub.rejects(); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentWindows(workspace1, undefined)); + assert.ok(switchPythonStub.notCalled); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); +}); + +suite('Test Delete environments (linux/mac)', () => { + let pathExistsStub: sinon.SinonStub; + let rmdirStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + + const workspace1: WorkspaceFolder = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + rmdirStub = sinon.stub(fs, 'rmdir'); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete venv folder succeeded', async () => { + pathExistsStub.resolves(true); + rmdirStub.resolves(); + + assert.ok(await deleteEnvironmentNonWindows(workspace1)); + + assert.ok(pathExistsStub.calledOnce); + assert.ok(rmdirStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete venv folder failed', async () => { + pathExistsStub.resolves(true); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentNonWindows(workspace1)); + + assert.ok(pathExistsStub.calledOnce); + assert.ok(rmdirStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts index 360bb43fad4b..ae4f43a0296c 100644 --- a/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts @@ -8,7 +8,11 @@ import { Uri } from 'vscode'; import * as path from 'path'; import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; -import { pickPackagesToInstall } from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; +import { + ExistingVenvAction, + pickExistingVenvAction, + pickPackagesToInstall, +} from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; import { CreateEnv } from '../../../../client/common/utils/localize'; @@ -346,3 +350,65 @@ suite('Venv Utils test', () => { assert.isTrue(readFileStub.notCalled); }); }); + +suite('Test pick existing venv action', () => { + let withProgressStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + }); + teardown(() => { + sinon.restore(); + }); + + test('User selects existing venv', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Venv.useExisting, + description: CreateEnv.Venv.useExistingDescription, + }); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.UseExisting); + }); + + test('User presses escape', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves(undefined); + await assert.isRejected(pickExistingVenvAction(workspace1)); + }); + + test('User selects delete venv', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Venv.recreate, + description: CreateEnv.Venv.recreateDescription, + }); + withProgressStub.resolves(true); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.Recreate); + }); + + test('User clicks on back', async () => { + pathExistsStub.resolves(true); + // We use reject with "Back" to simulate the user clicking on back. + showQuickPickWithBackStub.rejects(windowApis.MultiStepAction.Back); + withProgressStub.resolves(false); + await assert.isRejected(pickExistingVenvAction(workspace1)); + }); + + test('No venv found', async () => { + pathExistsStub.resolves(false); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.Create); + }); +}); From 98428cd8449ec73c095eda89065bdb384e4481e4 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 28 Aug 2023 16:31:46 -0700 Subject: [PATCH 0151/1136] Apply custom env variables to terminal when in `pythonTerminalEnvVarActivation` experiment (#21879) For https://github.com/microsoft/vscode-python/issues/944 #20822 We only apply those env vars to terminal which are not in process env variables, hence remove custom env vars from process variables. --- src/client/common/process/processFactory.ts | 6 ++++-- src/client/common/process/types.ts | 2 +- src/client/interpreter/activation/service.ts | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/common/process/processFactory.ts b/src/client/common/process/processFactory.ts index 8681d5073d8e..40204a640dae 100644 --- a/src/client/common/process/processFactory.ts +++ b/src/client/common/process/processFactory.ts @@ -17,8 +17,10 @@ export class ProcessServiceFactory implements IProcessServiceFactory { @inject(IProcessLogger) private readonly processLogger: IProcessLogger, @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, ) {} - public async create(resource?: Uri): Promise { - const customEnvVars = await this.envVarsService.getEnvironmentVariables(resource); + public async create(resource?: Uri, options?: { doNotUseCustomEnvs: boolean }): Promise { + const customEnvVars = options?.doNotUseCustomEnvs + ? undefined + : await this.envVarsService.getEnvironmentVariables(resource); const proc: IProcessService = new ProcessService(customEnvVars); this.disposableRegistry.push(proc); return proc.on('exec', this.processLogger.logProcess.bind(this.processLogger)); diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index 62e787b694b5..d4b742718e36 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -55,7 +55,7 @@ export interface IProcessService extends IDisposable { export const IProcessServiceFactory = Symbol('IProcessServiceFactory'); export interface IProcessServiceFactory { - create(resource?: Uri): Promise; + create(resource?: Uri, options?: { doNotUseCustomEnvs: boolean }): Promise; } export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory'); diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index 4364cc825f78..02d621c0ccda 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -192,7 +192,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi args[i] = arg.toCommandArgumentForPythonExt(); }); const command = `${interpreterPath} ${args.join(' ')}`; - const processService = await this.processServiceFactory.create(resource); + const processService = await this.processServiceFactory.create(resource, { doNotUseCustomEnvs: true }); const result = await processService.shellExec(command, { shell, timeout: ENVIRONMENT_TIMEOUT, From 12040116426b24ffb21bcbbb953f7f8487f3019a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 29 Aug 2023 11:08:49 -0700 Subject: [PATCH 0152/1136] Activate environment when not using integrated terminal for debugging (#21880) For https://github.com/microsoft/vscode-python/issues/4300 --- .../debugger/extension/configuration/resolvers/launch.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts index e4828844a7ad..c4ae6a204d71 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -19,7 +19,7 @@ import { getProgram, IDebugEnvironmentVariablesService } from './helper'; @injectable() export class LaunchConfigurationResolver extends BaseConfigurationResolver { - private isPythonSet = false; + private isCustomPythonSet = false; constructor( @inject(IDiagnosticsService) @@ -38,7 +38,7 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver { - this.isPythonSet = debugConfiguration.python !== undefined; + this.isCustomPythonSet = debugConfiguration.python !== undefined; if ( debugConfiguration.name === undefined && debugConfiguration.type === undefined && @@ -110,7 +110,9 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver Date: Tue, 29 Aug 2023 16:49:49 -0700 Subject: [PATCH 0153/1136] Also show interpreter in status bar when a Python related output channel is opened (#21894) Closes https://github.com/microsoft/vscode-python/issues/21890 --- src/client/interpreter/interpreterService.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 3cfb651977bb..b595fc2365a8 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -31,7 +31,7 @@ import { PythonEnvironmentsChangedEvent, } from './contracts'; import { traceError, traceLog } from '../logging'; -import { Commands, PYTHON_LANGUAGE } from '../common/constants'; +import { Commands, PVSC_EXTENSION_ID, PYTHON_LANGUAGE } from '../common/constants'; import { reportActiveInterpreterChanged } from '../environmentApi'; import { IPythonExecutionFactory } from '../common/process/types'; import { Interpreters } from '../common/utils/localize'; @@ -138,7 +138,12 @@ export class InterpreterService implements Disposable, IInterpreterService { return false; } const document = this.docManager.activeTextEditor?.document; - if (document?.fileName.endsWith('settings.json')) { + // Output channel for MS Python related extensions. These contain "ms-python" in their ID. + const pythonOutputChannelPattern = PVSC_EXTENSION_ID.split('.')[0]; + if ( + document?.fileName.endsWith('settings.json') || + document?.fileName.includes(pythonOutputChannelPattern) + ) { return false; } return document?.languageId !== PYTHON_LANGUAGE; From f255e0261ddd39172d6f8bf0bd94d08adbada1e7 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 29 Aug 2023 16:51:29 -0700 Subject: [PATCH 0154/1136] Call out that env name may not show in terminal activation notification (#21897) Closes https://github.com/microsoft/vscode-python/issues/21887 --- src/client/common/utils/localize.ts | 2 +- .../terminalEnvVarCollectionPrompt.ts | 11 ++++-- ...erminalEnvVarCollectionPrompt.unit.test.ts | 37 ++++++++++++------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 4401cab56176..eb8e94a85a52 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -199,7 +199,7 @@ export namespace Interpreters { export const activatingTerminals = l10n.t('Reactivating terminals...'); export const activateTerminalDescription = l10n.t('Activated environment for'); export const terminalEnvVarCollectionPrompt = l10n.t( - 'The Python extension automatically activates all terminals using the selected environment. You can hover over the terminal tab to see more information about the activation. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', + 'The Python extension automatically activates all terminals using the selected environment, even when the name of the environment{0} is not present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', ); export const activatedCondaEnvLaunch = l10n.t( 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts index 7833f34ce2fb..8b7850514874 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts @@ -9,11 +9,13 @@ import { IDisposableRegistry, IExperimentService, IPersistentStateFactory, + Resource, } from '../../common/types'; import { Common, Interpreters } from '../../common/utils/localize'; import { IExtensionSingleActivationService } from '../../activation/types'; import { ITerminalEnvVarCollectionService } from './types'; import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; +import { IInterpreterService } from '../contracts'; export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; @@ -30,6 +32,7 @@ export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivatio @inject(ITerminalEnvVarCollectionService) private readonly terminalEnvVarCollectionService: ITerminalEnvVarCollectionService, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IExperimentService) private readonly experimentService: IExperimentService, ) {} @@ -52,12 +55,12 @@ export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivatio // No need to show notification if terminal prompt already indicates when env is activated. return; } - await this.notifyUsers(); + await this.notifyUsers(resource); }), ); } - private async notifyUsers(): Promise { + private async notifyUsers(resource: Resource): Promise { const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( terminalEnvCollectionPromptKey, true, @@ -66,8 +69,10 @@ export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivatio return; } const prompts = [Common.doNotShowAgain]; + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const terminalPromptName = interpreter?.envName ? ` (${interpreter.envName})` : ''; const selection = await this.appShell.showInformationMessage( - Interpreters.terminalEnvVarCollectionPrompt, + Interpreters.terminalEnvVarCollectionPrompt.format(terminalPromptName), ...prompts, ); if (!selection) { diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts index 8edaa00dcf73..e1bc2d171226 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts @@ -18,6 +18,8 @@ import { ITerminalEnvVarCollectionService } from '../../../client/interpreter/ac import { Common, Interpreters } from '../../../client/common/utils/localize'; import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; import { sleep } from '../../core'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; suite('Terminal Environment Variable Collection Prompt', () => { let shell: IApplicationShell; @@ -30,12 +32,18 @@ suite('Terminal Environment Variable Collection Prompt', () => { let terminalEventEmitter: EventEmitter; let notificationEnabled: IPersistentState; let configurationService: IConfigurationService; + let interpreterService: IInterpreterService; const prompts = [Common.doNotShowAgain]; - const message = Interpreters.terminalEnvVarCollectionPrompt; + const envName = 'env'; + const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format(` (${envName})`); setup(async () => { shell = mock(); terminalManager = mock(); + interpreterService = mock(); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + envName, + } as unknown) as PythonEnvironment); experimentService = mock(); activeResourceService = mock(); persistentStateFactory = mock(); @@ -61,6 +69,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { instance(activeResourceService), instance(terminalEnvVarCollectionService), instance(configurationService), + instance(interpreterService), instance(experimentService), ); }); @@ -74,13 +83,13 @@ suite('Terminal Environment Variable Collection Prompt', () => { } as unknown) as Terminal; when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); when(notificationEnabled.value).thenReturn(true); - when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); await terminalEnvVarCollectionPrompt.activate(); terminalEventEmitter.fire(terminal); await sleep(1); - verify(shell.showInformationMessage(message, ...prompts)).once(); + verify(shell.showInformationMessage(expectedMessage, ...prompts)).once(); }); test('Do not show notification if automatic terminal activation is turned off', async () => { @@ -98,13 +107,13 @@ suite('Terminal Environment Variable Collection Prompt', () => { } as unknown) as Terminal; when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); when(notificationEnabled.value).thenReturn(true); - when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); await terminalEnvVarCollectionPrompt.activate(); terminalEventEmitter.fire(terminal); await sleep(1); - verify(shell.showInformationMessage(message, ...prompts)).never(); + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); }); test('When not in experiment, do not show notification for the same', async () => { @@ -116,7 +125,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { } as unknown) as Terminal; when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); when(notificationEnabled.value).thenReturn(true); - when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); reset(experimentService); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); @@ -124,7 +133,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { terminalEventEmitter.fire(terminal); await sleep(1); - verify(shell.showInformationMessage(message, ...prompts)).never(); + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); }); test('Do not show notification if notification is disabled', async () => { @@ -136,13 +145,13 @@ suite('Terminal Environment Variable Collection Prompt', () => { } as unknown) as Terminal; when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); when(notificationEnabled.value).thenReturn(false); - when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); await terminalEnvVarCollectionPrompt.activate(); terminalEventEmitter.fire(terminal); await sleep(1); - verify(shell.showInformationMessage(message, ...prompts)).never(); + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); }); test('Do not show notification when a new terminal is opened for which there is prompt set', async () => { @@ -154,13 +163,13 @@ suite('Terminal Environment Variable Collection Prompt', () => { } as unknown) as Terminal; when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(true); when(notificationEnabled.value).thenReturn(true); - when(shell.showInformationMessage(message, ...prompts)).thenResolve(undefined); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); await terminalEnvVarCollectionPrompt.activate(); terminalEventEmitter.fire(terminal); await sleep(1); - verify(shell.showInformationMessage(message, ...prompts)).never(); + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); }); test("Disable notification if `Don't show again` is clicked", async () => { @@ -173,7 +182,9 @@ suite('Terminal Environment Variable Collection Prompt', () => { when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); when(notificationEnabled.value).thenReturn(true); when(notificationEnabled.updateValue(false)).thenResolve(); - when(shell.showInformationMessage(message, ...prompts)).thenReturn(Promise.resolve(Common.doNotShowAgain)); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenReturn( + Promise.resolve(Common.doNotShowAgain), + ); await terminalEnvVarCollectionPrompt.activate(); terminalEventEmitter.fire(terminal); @@ -192,7 +203,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); when(notificationEnabled.value).thenReturn(true); when(notificationEnabled.updateValue(false)).thenResolve(); - when(shell.showInformationMessage(message, ...prompts)).thenReturn(Promise.resolve(undefined)); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(undefined)); await terminalEnvVarCollectionPrompt.activate(); terminalEventEmitter.fire(terminal); From 941fcfa938113b00d14cd944399185fcf9f35085 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 29 Aug 2023 18:55:27 -0700 Subject: [PATCH 0155/1136] Fixes from TPIs (#21896) Closes https://github.com/microsoft/vscode-python/issues/21884 Closes https://github.com/microsoft/vscode-python/issues/21889 --- src/client/common/utils/localize.ts | 4 +++- .../creation/provider/condaCreationProvider.ts | 6 ++++-- .../creation/provider/venvCreationProvider.ts | 7 +++++-- .../creation/provider/condaCreationProvider.unit.test.ts | 6 ++++-- .../creation/provider/venvCreationProvider.unit.test.ts | 6 ++++-- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index eb8e94a85a52..4cda15e15ec0 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -468,7 +468,9 @@ export namespace CreateEnv { export const recreateDescription = l10n.t('Delete existing ".venv" environment and create a new one'); export const useExisting = l10n.t('Use Existing'); export const useExistingDescription = l10n.t('Use existing ".venv" environment with no changes to it'); - export const existingVenvQuickPickPlaceholder = l10n.t('Use or Recreate existing environment?'); + export const existingVenvQuickPickPlaceholder = l10n.t( + 'Choose an option to handle the existing ".venv" environment', + ); export const deletingEnvironmentProgress = l10n.t('Deleting existing ".venv" environment...'); export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".venv" environment.'); } diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 74a42808e609..7ca44c1b7eff 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -128,7 +128,9 @@ async function createCondaEnv( dispose(); if (proc?.exitCode !== 0) { traceError('Error while running venv creation script: ', progressAndTelemetry.getLastError()); - deferred.reject(progressAndTelemetry.getLastError()); + deferred.reject( + progressAndTelemetry.getLastError() || `Conda env creation failed with exitCode: ${proc?.exitCode}`, + ); } else { deferred.resolve(condaEnvPath); } @@ -249,7 +251,7 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise { assert.isDefined(_error); _error!('bad arguments'); _complete!(); - await assert.isRejected(promise); + const result = await promise; + assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); }); @@ -241,7 +242,8 @@ suite('Conda Creation provider tests', () => { _next!({ out: `${CONDA_ENV_CREATED_MARKER}new_environment`, source: 'stdout' }); _complete!(); - await assert.isRejected(promise); + const result = await promise; + assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); }); }); diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index 280d05bf5935..de65887b7edc 100644 --- a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -226,7 +226,8 @@ suite('venv Creation provider tests', () => { assert.isDefined(_error); _error!('bad arguments'); _complete!(); - await assert.isRejected(promise); + const result = await promise; + assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); assert.isTrue(deleteEnvironmentStub.notCalled); }); @@ -283,7 +284,8 @@ suite('venv Creation provider tests', () => { _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); _complete!(); - await assert.isRejected(promise); + const result = await promise; + assert.ok(result?.error); interpreterQuickPick.verifyAll(); progressMock.verifyAll(); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); From 31aa24625b726b5a0af0d7c21666f61b59010c17 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 29 Aug 2023 23:11:38 -0700 Subject: [PATCH 0156/1136] Also show env name for prefixed conda envs in terminal prompt (#21899) --- .../terminalEnvVarCollectionPrompt.ts | 17 ++++++++++++++++- .../terminalEnvVarCollectionPrompt.unit.test.ts | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts index 8b7850514874..b5d42f0e7dbe 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts @@ -3,6 +3,7 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; +import * as path from 'path'; import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../common/application/types'; import { IConfigurationService, @@ -16,6 +17,7 @@ import { IExtensionSingleActivationService } from '../../activation/types'; import { ITerminalEnvVarCollectionService } from './types'; import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; import { IInterpreterService } from '../contracts'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; @@ -70,7 +72,7 @@ export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivatio } const prompts = [Common.doNotShowAgain]; const interpreter = await this.interpreterService.getActiveInterpreter(resource); - const terminalPromptName = interpreter?.envName ? ` (${interpreter.envName})` : ''; + const terminalPromptName = getPromptName(interpreter); const selection = await this.appShell.showInformationMessage( Interpreters.terminalEnvVarCollectionPrompt.format(terminalPromptName), ...prompts, @@ -83,3 +85,16 @@ export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivatio } } } + +function getPromptName(interpreter?: PythonEnvironment) { + if (!interpreter) { + return ''; + } + if (interpreter.envName) { + return ` "(${interpreter.envName})"`; + } + if (interpreter.envPath) { + return ` "(${path.basename(interpreter.envPath)})"`; + } + return ''; +} diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts index e1bc2d171226..7bddbdcbbfe0 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts @@ -35,7 +35,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { let interpreterService: IInterpreterService; const prompts = [Common.doNotShowAgain]; const envName = 'env'; - const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format(` (${envName})`); + const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format(` "(${envName})"`); setup(async () => { shell = mock(); From 7d25ceb8c134c098279f980b1fded2aa2b1c45f6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 30 Aug 2023 09:48:08 -0700 Subject: [PATCH 0157/1136] Remove finalized api proposals from package.json (#21900) Part of microsoft/vscode#191605 --- package.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index bc7378aa521b..e1ceb89d587d 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,7 @@ "envShellEvent", "testObserver", "quickPickItemTooltip", - "envCollectionWorkspace", - "saveEditor", - "envCollectionOptions" + "saveEditor" ], "author": { "name": "Microsoft Corporation" @@ -46,7 +44,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.82.0-20230823" + "vscode": "^1.82.0-20230830" }, "enableTelemetry": false, "keywords": [ From 44f5bf7a158da998776fea1dfd52f3137644bccf Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 30 Aug 2023 13:58:39 -0700 Subject: [PATCH 0158/1136] Set PS1 for conda environments in non-Windows when in `pythonTerminalEnvVarActivation` experiment (#21902) For #20822 ![image](https://github.com/microsoft/vscode-python/assets/13199757/8c9d4c87-54f2-4661-b6c6-c3b49ee3ff7a) --- .../terminalEnvVarCollectionService.ts | 57 ++++++++++++++++--- ...rminalEnvVarCollectionService.unit.test.ts | 16 ++++-- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 33d51e7e2ed0..3849160b7e4b 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -31,7 +31,7 @@ import { traceDecoratorVerbose, traceError, traceVerbose, traceWarn } from '../. import { IInterpreterService } from '../contracts'; import { defaultShells } from './service'; import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; -import { EnvironmentType } from '../../pythonEnvironments/info'; +import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; import { TerminalShellType } from '../../common/terminal/types'; @@ -44,6 +44,15 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ virtualWorkspace: false, }; + /** + * Prompts for these shells cannot be set reliably using variables + */ + private noPromptVariableShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.fish, + ]; + private deferred: Deferred | undefined; private registeredOnce = false; @@ -150,6 +159,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ ); } const processEnv = this.processEnvVars; + + // PS1 in some cases is a shell variable (not an env variable) so "env" might not contain it, calculate it in that case. + env.PS1 = await this.getPS1(shell, resource, env); + Object.keys(env).forEach((key) => { if (shouldSkip(key)) { return; @@ -213,15 +226,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ this.terminalPromptIsCorrect(resource); return; } - // Prompts for these shells cannot be set reliably using variables - const exceptionShells = [ - TerminalShellType.powershell, - TerminalShellType.powershellCore, - TerminalShellType.fish, - TerminalShellType.zsh, // TODO: Remove this once https://github.com/microsoft/vscode/issues/188875 is fixed - ]; const customShellType = identifyShellFromShellPath(shell); - if (exceptionShells.includes(customShellType)) { + if (this.noPromptVariableShells.includes(customShellType)) { return; } if (this.platform.osType !== OSType.Windows) { @@ -243,6 +249,26 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ this.terminalPromptIsCorrect(resource); } + private async getPS1(shell: string, resource: Resource, env: EnvironmentVariables) { + if (env.PS1) { + return env.PS1; + } + const customShellType = identifyShellFromShellPath(shell); + if (this.noPromptVariableShells.includes(customShellType)) { + return undefined; + } + if (this.platform.osType !== OSType.Windows) { + // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const shouldPS1BeSet = interpreter?.type !== undefined; + if (shouldPS1BeSet && !env.PS1) { + // PS1 should be set but no PS1 was set. + return getPromptForEnv(interpreter); + } + } + return undefined; + } + private async handleMicroVenv(resource: Resource) { try { const workspaceFolder = this.getWorkspaceFolder(resource); @@ -313,3 +339,16 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ function shouldSkip(env: string) { return ['_', 'SHLVL'].includes(env); } + +function getPromptForEnv(interpreter: PythonEnvironment | undefined) { + if (!interpreter) { + return undefined; + } + if (interpreter.envName) { + return `(${interpreter.envName}) `; + } + if (interpreter.envPath) { + return `(${path.basename(interpreter.envPath)}) `; + } + return undefined; +} diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 7393f4ad07ad..2e2327bd181c 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -321,9 +321,9 @@ suite('Terminal Environment Variable Collection Service', () => { expect(result).to.equal(false); }); - test('Correct track that prompt was not set for non-Windows zsh where PS1 is set', async () => { + test('Correct track that prompt was set for non-Windows where PS1 is not set but should be set', async () => { when(platform.osType).thenReturn(OSType.Linux); - const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; const ps1Shell = 'zsh'; const resource = Uri.file('a'); const workspaceFolder: WorkspaceFolder = { @@ -332,7 +332,9 @@ suite('Terminal Environment Variable Collection Service', () => { index: 0, }; when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ - type: PythonEnvType.Virtual, + type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', } as unknown) as PythonEnvironment); when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); when( @@ -344,13 +346,13 @@ suite('Terminal Environment Variable Collection Service', () => { const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); - expect(result).to.equal(false); + expect(result).to.equal(true); }); - test('Correct track that prompt was not set for non-Windows where PS1 is not set', async () => { + test('Correct track that prompt was not set for non-Windows fish where PS1 is not set', async () => { when(platform.osType).thenReturn(OSType.Linux); const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; - const ps1Shell = 'zsh'; + const ps1Shell = 'fish'; const resource = Uri.file('a'); const workspaceFolder: WorkspaceFolder = { uri: Uri.file('workspacePath'), @@ -359,6 +361,8 @@ suite('Terminal Environment Variable Collection Service', () => { }; when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', } as unknown) as PythonEnvironment); when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); when( From 7a9294cb2e8ccecf3f98130d6361b99556fbd122 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 30 Aug 2023 16:41:16 -0700 Subject: [PATCH 0159/1136] Apply feedback for terminal activation prompt (#21905) For https://github.com/microsoft/vscode-python/issues/21793 ![image](https://github.com/microsoft/vscode-python/assets/13199757/b3ab6002-0a07-4b3b-8101-a84865ea12e4) --- .../terminalEnvVarCollectionPrompt.ts | 6 ++-- .../terminalEnvVarCollectionService.ts | 4 +++ ...erminalEnvVarCollectionPrompt.unit.test.ts | 4 +-- ...rminalEnvVarCollectionService.unit.test.ts | 28 +++++++++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts index b5d42f0e7dbe..c8aea205a32a 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; +import { Uri, l10n } from 'vscode'; import * as path from 'path'; import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../common/application/types'; import { @@ -91,10 +91,10 @@ function getPromptName(interpreter?: PythonEnvironment) { return ''; } if (interpreter.envName) { - return ` "(${interpreter.envName})"`; + return `, ${l10n.t('i.e')} "(${interpreter.envName})"`; } if (interpreter.envPath) { - return ` "(${path.basename(interpreter.envPath)})"`; + return `, ${l10n.t('i.e')} "(${path.basename(interpreter.envPath)})"`; } return ''; } diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 3849160b7e4b..fa949ff69fad 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -345,6 +345,10 @@ function getPromptForEnv(interpreter: PythonEnvironment | undefined) { return undefined; } if (interpreter.envName) { + if (interpreter.envName === 'base') { + // If conda base environment is selected, it can lead to "(base)" appearing twice if we return the env name. + return undefined; + } return `(${interpreter.envName}) `; } if (interpreter.envPath) { diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts index 7bddbdcbbfe0..baa83c8b11c5 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts @@ -4,7 +4,7 @@ 'use strict'; import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; -import { EventEmitter, Terminal, Uri } from 'vscode'; +import { EventEmitter, Terminal, Uri, l10n } from 'vscode'; import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../../client/common/application/types'; import { IConfigurationService, @@ -35,7 +35,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { let interpreterService: IInterpreterService; const prompts = [Common.doNotShowAgain]; const envName = 'env'; - const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format(` "(${envName})"`); + const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format(`, ${l10n.t('i.e')} "(${envName})"`); setup(async () => { shell = mock(); diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 2e2327bd181c..b3a017031765 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -349,6 +349,34 @@ suite('Terminal Environment Variable Collection Service', () => { expect(result).to.equal(true); }); + test('Correct track that prompt was not set for non-Windows where PS1 is not set but env name is base', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'base', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + test('Correct track that prompt was not set for non-Windows fish where PS1 is not set', async () => { when(platform.osType).thenReturn(OSType.Linux); const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; From d9b9c88f3e555eab540aef5af0f47619c749322e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 31 Aug 2023 10:23:21 -0700 Subject: [PATCH 0160/1136] Always prepend to PATH instead of replacing it (#21906) For #20822 #11039 Replacing as-is has its problems, for eg. pyenv asks their users to manipulate `PATH` in their init script: https://github.com/pyenv/pyenv#set-up-your-shell-environment-for-pyenv, which we could end up replacing. ![image](https://github.com/microsoft/vscode-python/assets/13199757/cc904f76-8d42-47e1-a6c8-6cfff6543db8) Particularly for pyenv, it mean users not being able to find pyenv: ![image](https://github.com/microsoft/vscode-python/assets/13199757/26100328-c227-435b-a4f2-ec168099f4c1) Prepending solves it for cases where initial PATH value is suffix of the final value: ![image](https://github.com/microsoft/vscode-python/assets/13199757/a95e4ffd-68dc-4e73-905e-504b3051324f) But, in other cases, this means that we end up with the whole `PATH` thrice. This is because it prepends it twice: - Once in shell integration script - Once when creating a process So the final value could be: ``` PATH= ``` where `` refers to value of `PATH` environment variable post activation. eg. ![image](https://github.com/microsoft/vscode-python/assets/13199757/7e771f62-eb53-49be-b261-d259096008f3) --- .../terminalEnvVarCollectionService.ts | 22 ++++++- ...rminalEnvVarCollectionService.unit.test.ts | 64 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index fa949ff69fad..9015dd7b9388 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -167,7 +167,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (shouldSkip(key)) { return; } - const value = env[key]; + let value = env[key]; const prevValue = processEnv[key]; if (prevValue !== value) { if (value !== undefined) { @@ -180,6 +180,26 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ }); return; } + if (key === 'PATH') { + if (processEnv.PATH && env.PATH?.endsWith(processEnv.PATH)) { + // Prefer prepending to PATH instead of replacing it, as we do not want to replace any + // changes to PATH users might have made it in their init scripts (~/.bashrc etc.) + const prependedPart = env.PATH.slice(0, -processEnv.PATH.length); + value = prependedPart; + traceVerbose(`Prepending environment variable ${key} in collection with ${value}`); + envVarCollection.prepend(key, value, { + applyAtShellIntegration: true, + applyAtProcessCreation: true, + }); + } else { + traceVerbose(`Prepending environment variable ${key} in collection to ${value}`); + envVarCollection.prepend(key, value, { + applyAtShellIntegration: true, + applyAtProcessCreation: true, + }); + } + return; + } traceVerbose(`Setting environment variable ${key} in collection to ${value}`); envVarCollection.replace(key, value, { applyAtShellIntegration: true, diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index b3a017031765..1513be676ee4 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -216,6 +216,70 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); + test('Prepend only "prepend portion of PATH" where applicable', async () => { + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const prependedPart = 'path/to/activate/dir:'; + const envVars: NodeJS.ProcessEnv = { PATH: `${prependedPart}${processEnv.PATH}` }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', prependedPart, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + + test('Prepend full PATH otherwise', async () => { + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const finalPath = 'hello/3/2/1'; + const envVars: NodeJS.ProcessEnv = { PATH: finalPath }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', finalPath, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + test('Verify envs are not applied if env activation is disabled', async () => { const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; when( From 8c612511b99cb5ebc78153684de8fe595bd154dc Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 5 Sep 2023 11:44:29 -0700 Subject: [PATCH 0161/1136] Update version for release candidate (#21919) --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e001c51ee220..cfccb197a736 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.15.0-dev", + "version": "2023.16.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.15.0-dev", + "version": "2023.16.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.82.0-20230823" + "vscode": "^1.82.0-20230830" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index e1ceb89d587d..f915aad147a4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", - "version": "2023.15.0-dev", + "version": "2023.16.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From e8d0ee5bb668b92c75cea40a1395c190d16391cf Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 5 Sep 2023 12:00:28 -0700 Subject: [PATCH 0162/1136] Update main to next pre-release version (#21921) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfccb197a736..41754245460e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.16.0-rc", + "version": "2023.17.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.16.0-rc", + "version": "2023.17.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index f915aad147a4..384502899d85 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", - "version": "2023.16.0-rc", + "version": "2023.17.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From c45036bc4e0717af62f2e3b293cc8133dc396e42 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 5 Sep 2023 13:33:27 -0700 Subject: [PATCH 0163/1136] Update paths for dependabot (#21920) For https://github.com/microsoft/vscode-python/issues/21139 --- .github/dependabot.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d54cf6b74a53..de5ebfe9158b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,27 @@ updates: labels: - 'no-changelog' + - package-ecosystem: 'github-actions' + directory: .github/actions/build-vsix + schedule: + interval: daily + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/lint + schedule: + interval: daily + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/smoke-test + schedule: + interval: daily + labels: + - 'no-changelog' + # Not skipping the news for some Python dependencies in case it's actually useful to communicate to users. - package-ecosystem: 'pip' directory: / From 6e9e656eca2c6b7557ec3337d84a132ec9d25b3b Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 5 Sep 2023 13:33:44 -0700 Subject: [PATCH 0164/1136] Remove activation triggers that are handled automatically (#21918) Closes https://github.com/microsoft/vscode-python/issues/21901 --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index 384502899d85..898f6e05ede1 100644 --- a/package.json +++ b/package.json @@ -67,9 +67,6 @@ "onLanguage:python", "onDebugDynamicConfigurations:python", "onDebugResolve:python", - "onWalkthrough:pythonWelcome", - "onWalkthrough:pythonWelcome2", - "onWalkthrough:pythonDataScienceWelcome", "workspaceContains:mspythonconfig.json", "workspaceContains:pyproject.toml", "workspaceContains:Pipfile", From 2cbafbbb052b16229db5d9d9e236ed9e8615212e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:37:08 -0700 Subject: [PATCH 0165/1136] Bump actions/setup-node from 2 to 3 in /.github/actions/lint (#21923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2 to 3.
Release notes

Sourced from actions/setup-node's releases.

Add support for asdf format and update actions/cache version to 3.0.0

In scope of this release we updated actions/cache package as the new version contains fixes for caching error handling. Moreover, we added support for asdf format as Node.js version file actions/setup-node#373. Besides, we introduced new output node-version and added npm-shrinkwrap.json to dependency file patterns: actions/setup-node#439

Update actions/cache version to 2.0.2

In scope of this release we updated actions/cache package as the new version contains fixes related to GHES 3.5 (actions/setup-node#460)

v3.0.0

In scope of this release we changed version of the runtime Node.js for the setup-node action and updated package-lock.json file to v2.

Breaking Changes

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/lint/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml index 1efa6aab79a5..e80c78960907 100644 --- a/.github/actions/lint/action.yml +++ b/.github/actions/lint/action.yml @@ -10,7 +10,7 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ inputs.node_version }} cache: 'npm' From b5d4249c2b810b38d7b179e49470fab3ef155475 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:37:32 -0700 Subject: [PATCH 0166/1136] Bump actions/upload-artifact from 2 to 3 in /.github/actions/build-vsix (#21924) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3.
Release notes

Sourced from actions/upload-artifact's releases.

v3.0.0

What's Changed

  • Update default runtime to node16 (#293)
  • Update package-lock.json file version to 2 (#302)

Breaking Changes

With the update to Node 16, all scripts will now be run with Node 16 rather than Node 12.

v2.3.1

Fix for empty fails on Windows failing on upload #281

v2.3.0 Upload Artifact

  • Optimizations for faster uploads of larger files that are already compressed
  • Significantly improved logging when there are chunked uploads
  • Clarifications in logs around the upload size and prohibited characters that aren't allowed in the artifact name or any uploaded files
  • Various other small bugfixes & optimizations

v2.2.4

  • Retry on HTTP 500 responses from the service

v2.2.3

  • Fixes for proxy related issues

v2.2.2

  • Improved retryability and error handling

v2.2.1

  • Update used actions/core package to the latest version

v2.2.0

  • Support for artifact retention

v2.1.4

  • Add Third Party License Information

v2.1.3

  • Use updated version of the @action/artifact NPM package

v2.1.2

  • Increase upload chunk size from 4MB to 8MB
  • Detect case insensitive file uploads

v2.1.1

  • Fix for certain symlinks not correctly being identified as directories before starting uploads

v2.1.0

  • Support for uploading artifacts with multiple paths
  • Support for using exclude paths
  • Updates to dependencies

... (truncated)

Commits
  • 0b7f8ab ci(github): update action/download-artifact from v1 to v3 (#312)
  • 013d2b8 Create devcontainer for codespaces + update all dev dependencies (#375)
  • 055b8b3 Bump Actions NPM dependencies (#374)
  • 7a5d483 ci(github): update action/checkout from v2 to v3 (#315)
  • e0057a5 README: Bump actions/checkout to v3 (#352)
  • 7fe6c13 Update to latest actions/publish-action (#363)
  • 83fd05a Bump actions-core to v1.10.0 (#356)
  • 3cea537 Merge pull request #327 from actions/robherley/artifact-1.1.0
  • 849aa77 nvm use 12 & npm run release
  • 4d39869 recompile with correct ncc version
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-vsix/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index 6c4621c7eb9b..ed410c64455c 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -84,7 +84,7 @@ runs: shell: bash - name: Upload VSIX - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ inputs.artifact_name }} path: ${{ inputs.vsix_name }} From 835bc16859c76e04f57decbe359d0aed03438626 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:37:52 -0700 Subject: [PATCH 0167/1136] Bump actions/setup-python from 2 to 4 in /.github/actions/lint (#21925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4.
Release notes

Sourced from actions/setup-python's releases.

v4.0.0

What's Changed

  • Support for python-version-file input: #336

Example of usage:

- uses: actions/setup-python@v4
  with:
python-version-file: '.python-version' # Read python version from a file
- run: python my_script.py

There is no default python version for this setup-python major version, the action requires to specify either python-version input or python-version-file input. If the python-version input is not specified the action will try to read required version from file from python-version-file input.

  • Use pypyX.Y for PyPy python-version input: #349

Example of usage:

- uses: actions/setup-python@v4
  with:
    python-version: 'pypy3.9' # pypy-X.Y kept for backward compatibility
- run: python my_script.py
  • RUNNER_TOOL_CACHE environment variable is equal AGENT_TOOLSDIRECTORY: #338

  • Bugfix: create missing pypyX.Y symlinks: #347

  • PKG_CONFIG_PATH environment variable: #400

  • Added python-path output: #405 python-path output contains Python executable path.

  • Updated zeit/ncc to vercel/ncc package: #393

  • Bugfix: fixed output for prerelease version of poetry: #409

  • Made pythonLocation environment variable consistent for Python and PyPy: #418

  • Bugfix for 3.x-dev syntax: #417

  • Other improvements: #318 #396 #384 #387 #388

v3.1.4

What's Changed

In the scope of this patch release, the warning for deprecating Python 2.x was added in actions/setup-python#674 by @​dmitry-shibanov

For more information, check out actions/setup-python#672

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=2&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/lint/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml index e80c78960907..1d302b055bee 100644 --- a/.github/actions/lint/action.yml +++ b/.github/actions/lint/action.yml @@ -36,7 +36,7 @@ runs: shell: bash - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.x' cache: 'pip' From d5c077c696dc3f08707317b898ee7e30b1e70bf3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:38:17 -0700 Subject: [PATCH 0168/1136] Bump actions/setup-node from 2 to 3 in /.github/actions/build-vsix (#21927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2 to 3.
Release notes

Sourced from actions/setup-node's releases.

Add support for asdf format and update actions/cache version to 3.0.0

In scope of this release we updated actions/cache package as the new version contains fixes for caching error handling. Moreover, we added support for asdf format as Node.js version file actions/setup-node#373. Besides, we introduced new output node-version and added npm-shrinkwrap.json to dependency file patterns: actions/setup-node#439

Update actions/cache version to 2.0.2

In scope of this release we updated actions/cache package as the new version contains fixes related to GHES 3.5 (actions/setup-node#460)

v3.0.0

In scope of this release we changed version of the runtime Node.js for the setup-node action and updated package-lock.json file to v2.

Breaking Changes

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-vsix/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index ed410c64455c..962fd85fc355 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -16,7 +16,7 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ inputs.node_version }} cache: 'npm' From 0cf25478c70ee25c8fbb1720ab2aa754b6e02a87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:38:40 -0700 Subject: [PATCH 0169/1136] Bump actions/setup-python from 2 to 4 in /.github/actions/build-vsix (#21926) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4.
Release notes

Sourced from actions/setup-python's releases.

v4.0.0

What's Changed

  • Support for python-version-file input: #336

Example of usage:

- uses: actions/setup-python@v4
  with:
python-version-file: '.python-version' # Read python version from a file
- run: python my_script.py

There is no default python version for this setup-python major version, the action requires to specify either python-version input or python-version-file input. If the python-version input is not specified the action will try to read required version from file from python-version-file input.

  • Use pypyX.Y for PyPy python-version input: #349

Example of usage:

- uses: actions/setup-python@v4
  with:
    python-version: 'pypy3.9' # pypy-X.Y kept for backward compatibility
- run: python my_script.py
  • RUNNER_TOOL_CACHE environment variable is equal AGENT_TOOLSDIRECTORY: #338

  • Bugfix: create missing pypyX.Y symlinks: #347

  • PKG_CONFIG_PATH environment variable: #400

  • Added python-path output: #405 python-path output contains Python executable path.

  • Updated zeit/ncc to vercel/ncc package: #393

  • Bugfix: fixed output for prerelease version of poetry: #409

  • Made pythonLocation environment variable consistent for Python and PyPy: #418

  • Bugfix for 3.x-dev syntax: #417

  • Other improvements: #318 #396 #384 #387 #388

v3.1.4

What's Changed

In the scope of this patch release, the warning for deprecating Python 2.x was added in actions/setup-python#674 by @​dmitry-shibanov

For more information, check out actions/setup-python#672

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=2&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-vsix/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index 962fd85fc355..4c783fed1c42 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -23,7 +23,7 @@ runs: # Jedi LS depends on dataclasses which is not in the stdlib in Python 3.7. - name: Use Python 3.7 for JediLSP - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 cache: 'pip' From a72ebb2ca4c8f6ad48c7652769e9ae9df9f92bf6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 17:48:04 +0000 Subject: [PATCH 0170/1136] Bump actions/checkout from 3 to 4 (#21912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
Release notes

Sourced from actions/checkout's releases.

v4.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3...v4.0.0

v3.6.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3.5.3...v3.6.0

v3.5.3

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3...v3.5.3

v3.5.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v3.5.1...v3.5.2

v3.5.1

What's Changed

New Contributors

... (truncated)

Changelog

Sourced from actions/checkout's changelog.

Changelog

v4.0.0

v3.6.0

v3.5.3

v3.5.2

v3.5.1

v3.5.0

v3.4.0

v3.3.0

v3.2.0

v3.1.0

v3.0.2

v3.0.1

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 10 +++++----- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/info-needed-closer.yml | 2 +- .github/workflows/issue-labels.yml | 2 +- .github/workflows/pr-check.yml | 12 ++++++------ .github/workflows/test-plan-item-validator.yml | 2 +- .github/workflows/triage-info-needed.yml | 4 ++-- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24d91b94da10..873b81fa66f8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build VSIX uses: ./.github/actions/build-vsix @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Lint uses: ./.github/actions/lint @@ -82,7 +82,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install core Python requirements uses: brettcannon/pip-secure-install@v1 @@ -125,7 +125,7 @@ jobs: test-suite: [ts-unit, python-unit, venv, single-workspace, multi-workspace, debugger, functional] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: ${{ env.special-working-directory-relative }} @@ -333,7 +333,7 @@ jobs: os: [ubuntu-latest, windows-latest] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Smoke tests uses: ./.github/actions/smoke-tests diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 278c2cf22e4a..5b037d5a1d0b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/info-needed-closer.yml b/.github/workflows/info-needed-closer.yml index c0b130be803b..442799cd7a16 100644 --- a/.github/workflows/info-needed-closer.yml +++ b/.github/workflows/info-needed-closer.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index d54015d94e46..7817ed62bd9b 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index aa9cae2aa474..c7335702e991 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build VSIX uses: ./.github/actions/build-vsix @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Lint uses: ./.github/actions/lint @@ -56,7 +56,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install base Python requirements uses: brettcannon/pip-secure-install@v1 @@ -100,7 +100,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: ${{ env.special-working-directory-relative }} @@ -302,7 +302,7 @@ jobs: steps: # Need the source to have the tests available. - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Smoke tests uses: ./.github/actions/smoke-tests @@ -323,7 +323,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Node uses: actions/setup-node@v3 diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml index 9d0805a9db9b..17f1740345f2 100644 --- a/.github/workflows/test-plan-item-validator.yml +++ b/.github/workflows/test-plan-item-validator.yml @@ -12,7 +12,7 @@ jobs: if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml index c717d7ec94b3..24ad2ed2c480 100644 --- a/.github/workflows/triage-info-needed.yml +++ b/.github/workflows/triage-info-needed.yml @@ -13,7 +13,7 @@ jobs: if: contains(github.event.issue.labels.*.name, 'triage-needed') && !contains(github.event.issue.labels.*.name, 'info-needed') steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable From b4c545d52a6e0a91d995c0adc171799705d258c3 Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:34:03 -0700 Subject: [PATCH 0171/1136] Update telemetry package (#21914) This PR updates the [telemetry package](https://github.com/microsoft/vscode-extension-telemetry) to the latest version and fixes the formatting of a line --- package-lock.json | 1055 +++++++++++++++++++++++++-------------------- package.json | 6 +- 2 files changed, 602 insertions(+), 459 deletions(-) diff --git a/package-lock.json b/package-lock.json index 41754245460e..70205a90f00e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", - "@vscode/extension-telemetry": "^0.7.7", + "@vscode/extension-telemetry": "^0.8.4", "@vscode/jupyter-lsp-middleware": "^0.2.50", "arch": "^2.1.0", "diff-match-patch": "^1.0.0", @@ -165,41 +165,43 @@ } }, "node_modules/@azure/abort-controller/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/core-auth": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.4.0.tgz", - "integrity": "sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", + "integrity": "sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw==", "dependencies": { "@azure/abort-controller": "^1.0.0", + "@azure/core-util": "^1.1.0", "tslib": "^2.2.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/@azure/core-auth/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.3.tgz", - "integrity": "sha512-AMQb0ttiGJ0MIV/r+4TVra6U4+90mPeOveehFnrqKlo7dknPJYdJ61wOzYJXJjDxF8LcCtSogfRelkq+fCGFTw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.1.tgz", + "integrity": "sha512-Kji9k6TOFRDB5ZMTw8qUf2IJ+CeJtsuMdAHox9eqpTf1cefiNMpzrfnF6sINEBZJsaVaWgQ0o48B6kcUH68niA==", "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.4.0", "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.3.0", + "@azure/core-util": "^1.0.0", "@azure/logger": "^1.0.0", "form-data": "^4.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", - "tslib": "^2.2.0" + "tslib": "^2.2.0", + "uuid": "^8.3.0" }, "engines": { "node": ">=14.0.0" @@ -243,9 +245,9 @@ } }, "node_modules/@azure/core-rest-pipeline/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/core-tracing": { "version": "1.0.1", @@ -259,14 +261,14 @@ } }, "node_modules/@azure/core-tracing/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/core-util": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.3.0.tgz", - "integrity": "sha512-ANP0Er7R2KHHHjwmKzPF9wbd0gXvOX7yRRHeYL1eNd/OaNrMLyfZH/FQasHRVAf6rMXX+EAUpvYwLMFDHDI5Gw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.2.0.tgz", + "integrity": "sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng==", "dependencies": { "@azure/abort-controller": "^1.0.0", "tslib": "^2.2.0" @@ -276,9 +278,9 @@ } }, "node_modules/@azure/core-util/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@azure/logger": { "version": "1.0.4", @@ -292,9 +294,30 @@ } }, "node_modules/@azure/logger/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@azure/opentelemetry-instrumentation-azure-sdk": { + "version": "1.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0-beta.5.tgz", + "integrity": "sha512-fsUarKQDvjhmBO4nIfaZkfNSApm1hZBzcvpNbSrXdcUBxu7lRvKsV5DnwszX7cnhLyVOW9yl1uigtRQ1yDANjA==", + "dependencies": { + "@azure/core-tracing": "^1.0.0", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/instrumentation": "^0.41.2", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/opentelemetry-instrumentation-azure-sdk/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@babel/code-frame": { "version": "7.12.11", @@ -957,83 +980,122 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.9.tgz", - "integrity": "sha512-3pCfM2TzHn3gU9pxHztduKcVRdb/nzruvPFfHPZD0IM0mb0h6TGo2isELF3CTMahTx50RAC51ojNIw2/7VRkOg==", + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz", + "integrity": "sha512-CluYTRWcEk0ObG5EWFNWhs87e2qchJUn0p2D21ZUa3PWojPZfPSBs4//WIE0MYV8Qg1Hdif2ZTwlM7TbYUjfAg==", "dependencies": { - "@microsoft/applicationinsights-core-js": "2.8.10", + "@microsoft/applicationinsights-core-js": "2.8.15", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.9.tgz", - "integrity": "sha512-D/RtqkQ2Nr4cuoGqmhi5QTmi3cBlxehIThJ1u3BaH9H/YkLNTKEcHZRWTXy14bXheCefNHciLuadg37G2Kekcg==", + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.13.tgz", + "integrity": "sha512-HgS574fdD19Bo2vPguyznL4eDw7Pcm1cVNpvbvBLWiW3x4e1FCQ3VMXChWnAxCae8Hb0XqlA2sz332ZobBavTA==", "dependencies": { - "@microsoft/1ds-core-js": "3.2.9", + "@microsoft/1ds-core-js": "3.2.13", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.8.11.tgz", - "integrity": "sha512-DGDNzT4DMlSvUzWjA4y3tDg47+QYOPV+W07vlfdPwGgLwrl4n6Q4crrW8Y/IOpthHAKDU8rolSAUvP3NqxPi4Q==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.2.tgz", + "integrity": "sha512-jDBNKbCHsJgmpv0CKNhJ/uN9ZphvfGdb93Svk+R4LjO8L3apNNMbDDPxBvXXi0uigRmA1TBcmyBG4IRKjabGhw==", "dependencies": { - "@microsoft/applicationinsights-common": "2.8.11", - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "dependencies": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, + "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, "node_modules/@microsoft/applicationinsights-common": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.8.11.tgz", - "integrity": "sha512-Cxu4gRajkYv9buEtrcLGHK97AqGK62feN9jH9/JSjUSiSFhbnWtYvEg1EMqMI/P4pneu53yLJloITB+TKwmK7A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.2.tgz", + "integrity": "sha512-y+WXWop+OVim954Cu1uyYMnNx6PWO8okHpZIQi/1YSqtqaYdtJVPv4P0AVzwJdohxzVfgzKvqj9nec/VWqE2Zg==", "dependencies": { - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, "node_modules/@microsoft/applicationinsights-common/node_modules/@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "dependencies": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, + "node_modules/@microsoft/applicationinsights-common/node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-common/node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.10.tgz", - "integrity": "sha512-jQrufDW0+sV8fBhRvzIPNGiCC6dELH+Ug0DM5CfN9757TBqZJz8CSWyDjex39as8+jD0F/8HRU9QdmrVgq5vFg==", + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.15.tgz", + "integrity": "sha512-yYAs9MyjGr2YijQdUSN9mVgT1ijI1FPMgcffpaPmYbHAVbQmF7bXudrBWHxmLzJlwl5rfep+Zgjli2e67lwUqQ==", "dependencies": { "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/dynamicproto-js": "^1.1.9" }, "peerDependencies": { "tslib": "*" @@ -1045,32 +1107,52 @@ "integrity": "sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg==" }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-2.8.11.tgz", - "integrity": "sha512-11T7bbP4ifIBg95E9mYZv1g/vcWvw/KaWKRcGMREP3+vBTLBwMB8r2e9Zd583bOVx+9/gRvfIg+Z/lInQqAfbA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.2.tgz", + "integrity": "sha512-6Lq0DE/pZp9RvSV+weGbcxN1NDmfczj6gNPhvZKV2YSQ3RK0LZE3+wjTWLXfuStq8a+nCBdsRpWk8tOKgsoxcg==", "dependencies": { - "@microsoft/applicationinsights-channel-js": "2.8.11", - "@microsoft/applicationinsights-common": "2.8.11", - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-channel-js": "3.0.2", + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, "node_modules/@microsoft/applicationinsights-web-basic/node_modules/@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "dependencies": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "peerDependencies": { "tslib": "*" } }, + "node_modules/@microsoft/applicationinsights-web-basic/node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-web-basic/node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, "node_modules/@microsoft/applicationinsights-web-snippet": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-snippet/-/applicationinsights-web-snippet-1.0.1.tgz", @@ -1081,6 +1163,19 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, + "node_modules/@nevware21/ts-async": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", + "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + } + }, + "node_modules/@nevware21/ts-utils": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", + "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + }, "node_modules/@nicolo-ribaudo/semver-v6": { "version": "6.3.3", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", @@ -1134,11 +1229,11 @@ } }, "node_modules/@opentelemetry/core": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.11.0.tgz", - "integrity": "sha512-aP1wHSb+YfU0pM63UAkizYPuS4lZxzavHHw5KJfFNN2oWQ79HSm6JR3CzwFKHwKhSzHN8RE9fgP1IdVJ8zmo1w==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.15.2.tgz", + "integrity": "sha512-+gBv15ta96WqkHZaPpcDHiaz0utiiHZVfm2YOYSqFGrUaJpPkMoSuLBB58YFQGi6Rsb9EHos84X6X5+9JspmLw==", "dependencies": { - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/semantic-conventions": "1.15.2" }, "engines": { "node": ">=14" @@ -1147,13 +1242,31 @@ "@opentelemetry/api": ">=1.0.0 <1.5.0" } }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz", + "integrity": "sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw==", + "dependencies": { + "@types/shimmer": "^1.0.2", + "import-in-the-middle": "1.4.2", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.1", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/resources": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.11.0.tgz", - "integrity": "sha512-y0z2YJTqk0ag+hGT4EXbxH/qPhDe8PfwltYb4tXIEsozgEFfut/bqW7H7pDvylmCjBRMG4NjtLp57V1Ev++brA==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.15.2.tgz", + "integrity": "sha512-xmMRLenT9CXmm5HMbzpZ1hWhaUowQf8UB4jMjFlAxx1QzQcsD3KFNAVX/CAWzFPtllTyTplrA4JrQ7sCH3qmYw==", "dependencies": { - "@opentelemetry/core": "1.11.0", - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/core": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" }, "engines": { "node": ">=14" @@ -1163,13 +1276,13 @@ } }, "node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.11.0.tgz", - "integrity": "sha512-DV8e5/Qo42V8FMBlQ0Y0Liv6Hl/Pp5bAZ73s7r1euX8w4bpRes1B7ACiA4yujADbWMJxBgSo4fGbi4yjmTMG2A==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.15.2.tgz", + "integrity": "sha512-BEaxGZbWtvnSPchV98qqqqa96AOcb41pjgvhfzDij10tkBhIu9m0Jd6tZ1tJB5ZHfHbTffqYVYE0AOGobec/EQ==", "dependencies": { - "@opentelemetry/core": "1.11.0", - "@opentelemetry/resources": "1.11.0", - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/core": "1.15.2", + "@opentelemetry/resources": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" }, "engines": { "node": ">=14" @@ -1179,9 +1292,9 @@ } }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.11.0.tgz", - "integrity": "sha512-fG4D0AktoHyHwGhFGv+PzKrZjxbKJfckJauTJdq2A+ej5cTazmNYjJVAODXXkYyrsI10muMl+B1iO2q1R6Lp+w==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.15.2.tgz", + "integrity": "sha512-CjbOKwk2s+3xPIMcd5UNYQzsf+v94RczbdNix9/kQh38WiQkM90sUOi3if8eyHFgiBjBjhwXrA7W3ydiSQP9mw==", "engines": { "node": ">=14" } @@ -1451,6 +1564,11 @@ "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", "dev": true }, + "node_modules/@types/shimmer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.2.tgz", + "integrity": "sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg==" + }, "node_modules/@types/shortid": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz", @@ -1564,21 +1682,6 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/experimental-utils": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", @@ -1688,21 +1791,6 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/visitor-keys": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", @@ -1726,14 +1814,14 @@ "dev": true }, "node_modules/@vscode/extension-telemetry": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.7.7.tgz", - "integrity": "sha512-uW508BPjkWDBOKvvvSym3ZmGb7kHIiWaAfB/1PHzLz2x9TrC33CfjmFEI+CywIL/jBv4bqZxxjN4tfefB61F+g==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz", + "integrity": "sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA==", "dependencies": { - "@microsoft/1ds-core-js": "^3.2.9", - "@microsoft/1ds-post-js": "^3.2.9", - "@microsoft/applicationinsights-web-basic": "^2.8.11", - "applicationinsights": "2.5.0" + "@microsoft/1ds-core-js": "^3.2.13", + "@microsoft/1ds-post-js": "^3.2.13", + "@microsoft/applicationinsights-web-basic": "^3.0.2", + "applicationinsights": "^2.7.1" }, "engines": { "vscode": "^1.75.0" @@ -2069,7 +2157,6 @@ "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2078,10 +2165,9 @@ } }, "node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "peerDependencies": { "acorn": "^8" } @@ -2279,21 +2365,23 @@ } }, "node_modules/applicationinsights": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.5.0.tgz", - "integrity": "sha512-6kIFmpANRok+6FhCOmO7ZZ/mh7fdNKn17BaT13cg/RV5roLPJlA6q8srWexayHd3MPcwMb9072e8Zp0P47s/pw==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.7.3.tgz", + "integrity": "sha512-JY8+kTEkjbA+kAVNWDtpfW2lqsrDALfDXuxOs74KLPu2y13fy/9WB52V4LfYVTVcW1/jYOXjTxNS2gPZIDh1iw==", "dependencies": { - "@azure/core-auth": "^1.4.0", - "@azure/core-rest-pipeline": "^1.10.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-rest-pipeline": "1.10.1", + "@azure/core-util": "1.2.0", + "@azure/opentelemetry-instrumentation-azure-sdk": "^1.0.0-beta.5", "@microsoft/applicationinsights-web-snippet": "^1.0.1", - "@opentelemetry/api": "^1.0.4", - "@opentelemetry/core": "^1.0.1", - "@opentelemetry/sdk-trace-base": "^1.0.1", - "@opentelemetry/semantic-conventions": "^1.0.1", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/sdk-trace-base": "^1.15.2", + "@opentelemetry/semantic-conventions": "^1.15.2", "cls-hooked": "^4.2.2", "continuation-local-storage": "^3.2.1", - "diagnostic-channel": "1.1.0", - "diagnostic-channel-publishers": "1.0.5" + "diagnostic-channel": "1.1.1", + "diagnostic-channel-publishers": "1.0.7" }, "engines": { "node": ">=8.0.0" @@ -3813,6 +3901,11 @@ "deprecated": "CircularJSON is in maintenance only, flatted is its successor.", "dev": true }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" + }, "node_modules/class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -4771,29 +4864,21 @@ } }, "node_modules/diagnostic-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.0.tgz", - "integrity": "sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz", + "integrity": "sha512-r2HV5qFkUICyoaKlBEpLKHjxMXATUf/l+h8UZPGBHGLy4DDiY2sOLcIctax4eRnTw5wH2jTMExLntGPJ8eOJxw==", "dependencies": { - "semver": "^5.3.0" + "semver": "^7.5.3" } }, "node_modules/diagnostic-channel-publishers": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.5.tgz", - "integrity": "sha512-dJwUS0915pkjjimPJVDnS/QQHsH0aOYhnZsLJdnZIMOrB+csj8RnZhWTuwnm8R5v3Z7OZs+ksv5luC14DGB7eg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.7.tgz", + "integrity": "sha512-SEECbY5AiVt6DfLkhkaHNeshg1CogdLLANA8xlG/TKvS+XUgvIKl7VspJGYiEdL5OUyzMVnr7o0AwB7f+/Mjtg==", "peerDependencies": { "diagnostic-channel": "*" } }, - "node_modules/diagnostic-channel/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -5827,21 +5912,6 @@ "node": ">=0.4.0" } }, - "node_modules/eslint/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint/node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6908,8 +6978,7 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/functional-red-black-tree": { "version": "1.0.1", @@ -7506,7 +7575,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -7852,6 +7920,17 @@ "node": ">=4" } }, + "node_modules/import-in-the-middle": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz", + "integrity": "sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-assertions": "^1.9.0", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -8057,10 +8136,9 @@ } }, "node_modules/is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dev": true, + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "dependencies": { "has": "^1.0.3" }, @@ -9982,6 +10060,11 @@ "node": ">=10" } }, + "node_modules/module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" + }, "node_modules/mrmime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.0.tgz", @@ -10141,22 +10224,6 @@ "node": ">=10" } }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "optional": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", @@ -11218,8 +11285,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-root": { "version": "0.1.1", @@ -11963,6 +12029,35 @@ "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", + "integrity": "sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==", + "dependencies": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/require-in-the-middle/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", @@ -11970,12 +12065,11 @@ "dev": true }, "node_modules/resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", "dependencies": { - "is-core-module": "^2.8.1", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -12215,9 +12309,9 @@ } }, "node_modules/semver": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", - "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -13131,7 +13225,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -13715,21 +13808,6 @@ "node": ">=8.6" } }, - "node_modules/ts-loader/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ts-loader/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -14400,7 +14478,6 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, "bin": { "uuid": "dist/bin/uuid" } @@ -14576,20 +14653,6 @@ "vscode": "^1.67.0" } }, - "node_modules/vscode-languageclient/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/vscode-languageserver": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz", @@ -15415,42 +15478,44 @@ }, "dependencies": { "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, "@azure/core-auth": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.4.0.tgz", - "integrity": "sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", + "integrity": "sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw==", "requires": { "@azure/abort-controller": "^1.0.0", + "@azure/core-util": "^1.1.0", "tslib": "^2.2.0" }, "dependencies": { "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, "@azure/core-rest-pipeline": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.3.tgz", - "integrity": "sha512-AMQb0ttiGJ0MIV/r+4TVra6U4+90mPeOveehFnrqKlo7dknPJYdJ61wOzYJXJjDxF8LcCtSogfRelkq+fCGFTw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.1.tgz", + "integrity": "sha512-Kji9k6TOFRDB5ZMTw8qUf2IJ+CeJtsuMdAHox9eqpTf1cefiNMpzrfnF6sINEBZJsaVaWgQ0o48B6kcUH68niA==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.4.0", "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.3.0", + "@azure/core-util": "^1.0.0", "@azure/logger": "^1.0.0", "form-data": "^4.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", - "tslib": "^2.2.0" + "tslib": "^2.2.0", + "uuid": "^8.3.0" }, "dependencies": { "@tootallnate/once": { @@ -15477,9 +15542,9 @@ } }, "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, @@ -15492,25 +15557,25 @@ }, "dependencies": { "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, "@azure/core-util": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.3.0.tgz", - "integrity": "sha512-ANP0Er7R2KHHHjwmKzPF9wbd0gXvOX7yRRHeYL1eNd/OaNrMLyfZH/FQasHRVAf6rMXX+EAUpvYwLMFDHDI5Gw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.2.0.tgz", + "integrity": "sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng==", "requires": { "@azure/abort-controller": "^1.0.0", "tslib": "^2.2.0" }, "dependencies": { "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, @@ -15523,9 +15588,29 @@ }, "dependencies": { "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@azure/opentelemetry-instrumentation-azure-sdk": { + "version": "1.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0-beta.5.tgz", + "integrity": "sha512-fsUarKQDvjhmBO4nIfaZkfNSApm1hZBzcvpNbSrXdcUBxu7lRvKsV5DnwszX7cnhLyVOW9yl1uigtRQ1yDANjA==", + "requires": { + "@azure/core-tracing": "^1.0.0", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/instrumentation": "^0.41.2", + "tslib": "^2.2.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, @@ -16031,75 +16116,114 @@ } }, "@microsoft/1ds-core-js": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.9.tgz", - "integrity": "sha512-3pCfM2TzHn3gU9pxHztduKcVRdb/nzruvPFfHPZD0IM0mb0h6TGo2isELF3CTMahTx50RAC51ojNIw2/7VRkOg==", + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz", + "integrity": "sha512-CluYTRWcEk0ObG5EWFNWhs87e2qchJUn0p2D21ZUa3PWojPZfPSBs4//WIE0MYV8Qg1Hdif2ZTwlM7TbYUjfAg==", "requires": { - "@microsoft/applicationinsights-core-js": "2.8.10", + "@microsoft/applicationinsights-core-js": "2.8.15", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "@microsoft/1ds-post-js": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.9.tgz", - "integrity": "sha512-D/RtqkQ2Nr4cuoGqmhi5QTmi3cBlxehIThJ1u3BaH9H/YkLNTKEcHZRWTXy14bXheCefNHciLuadg37G2Kekcg==", + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.13.tgz", + "integrity": "sha512-HgS574fdD19Bo2vPguyznL4eDw7Pcm1cVNpvbvBLWiW3x4e1FCQ3VMXChWnAxCae8Hb0XqlA2sz332ZobBavTA==", "requires": { - "@microsoft/1ds-core-js": "3.2.9", + "@microsoft/1ds-core-js": "3.2.13", "@microsoft/applicationinsights-shims": "^2.0.2", "@microsoft/dynamicproto-js": "^1.1.7" } }, "@microsoft/applicationinsights-channel-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.8.11.tgz", - "integrity": "sha512-DGDNzT4DMlSvUzWjA4y3tDg47+QYOPV+W07vlfdPwGgLwrl4n6Q4crrW8Y/IOpthHAKDU8rolSAUvP3NqxPi4Q==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.2.tgz", + "integrity": "sha512-jDBNKbCHsJgmpv0CKNhJ/uN9ZphvfGdb93Svk+R4LjO8L3apNNMbDDPxBvXXi0uigRmA1TBcmyBG4IRKjabGhw==", "requires": { - "@microsoft/applicationinsights-common": "2.8.11", - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "dependencies": { "@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", + "requires": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + } + }, + "@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "requires": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", "requires": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } } } }, "@microsoft/applicationinsights-common": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.8.11.tgz", - "integrity": "sha512-Cxu4gRajkYv9buEtrcLGHK97AqGK62feN9jH9/JSjUSiSFhbnWtYvEg1EMqMI/P4pneu53yLJloITB+TKwmK7A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.2.tgz", + "integrity": "sha512-y+WXWop+OVim954Cu1uyYMnNx6PWO8okHpZIQi/1YSqtqaYdtJVPv4P0AVzwJdohxzVfgzKvqj9nec/VWqE2Zg==", "requires": { - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "dependencies": { "@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", + "requires": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + } + }, + "@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", "requires": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "requires": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } } } }, "@microsoft/applicationinsights-core-js": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.10.tgz", - "integrity": "sha512-jQrufDW0+sV8fBhRvzIPNGiCC6dELH+Ug0DM5CfN9757TBqZJz8CSWyDjex39as8+jD0F/8HRU9QdmrVgq5vFg==", + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.15.tgz", + "integrity": "sha512-yYAs9MyjGr2YijQdUSN9mVgT1ijI1FPMgcffpaPmYbHAVbQmF7bXudrBWHxmLzJlwl5rfep+Zgjli2e67lwUqQ==", "requires": { "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/dynamicproto-js": "^1.1.9" } }, "@microsoft/applicationinsights-shims": { @@ -16108,24 +16232,44 @@ "integrity": "sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg==" }, "@microsoft/applicationinsights-web-basic": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-2.8.11.tgz", - "integrity": "sha512-11T7bbP4ifIBg95E9mYZv1g/vcWvw/KaWKRcGMREP3+vBTLBwMB8r2e9Zd583bOVx+9/gRvfIg+Z/lInQqAfbA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.0.2.tgz", + "integrity": "sha512-6Lq0DE/pZp9RvSV+weGbcxN1NDmfczj6gNPhvZKV2YSQ3RK0LZE3+wjTWLXfuStq8a+nCBdsRpWk8tOKgsoxcg==", "requires": { - "@microsoft/applicationinsights-channel-js": "2.8.11", - "@microsoft/applicationinsights-common": "2.8.11", - "@microsoft/applicationinsights-core-js": "2.8.11", - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-channel-js": "3.0.2", + "@microsoft/applicationinsights-common": "3.0.2", + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" }, "dependencies": { "@microsoft/applicationinsights-core-js": { - "version": "2.8.11", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.11.tgz", - "integrity": "sha512-6ScXplyb9Zb0K6TQRfqStm20j5lIe/Dslf65ozows6ibDcKkWl2ZdqzFhymVJZz1WRNpSyD4aA8qnqmslIER6g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", "requires": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + } + }, + "@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "requires": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "requires": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } } } @@ -16140,6 +16284,19 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, + "@nevware21/ts-async": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", + "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "requires": { + "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + } + }, + "@nevware21/ts-utils": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", + "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + }, "@nicolo-ribaudo/semver-v6": { "version": "6.3.3", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", @@ -16178,36 +16335,48 @@ "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==" }, "@opentelemetry/core": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.11.0.tgz", - "integrity": "sha512-aP1wHSb+YfU0pM63UAkizYPuS4lZxzavHHw5KJfFNN2oWQ79HSm6JR3CzwFKHwKhSzHN8RE9fgP1IdVJ8zmo1w==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.15.2.tgz", + "integrity": "sha512-+gBv15ta96WqkHZaPpcDHiaz0utiiHZVfm2YOYSqFGrUaJpPkMoSuLBB58YFQGi6Rsb9EHos84X6X5+9JspmLw==", "requires": { - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/semantic-conventions": "1.15.2" + } + }, + "@opentelemetry/instrumentation": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz", + "integrity": "sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw==", + "requires": { + "@types/shimmer": "^1.0.2", + "import-in-the-middle": "1.4.2", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.1", + "shimmer": "^1.2.1" } }, "@opentelemetry/resources": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.11.0.tgz", - "integrity": "sha512-y0z2YJTqk0ag+hGT4EXbxH/qPhDe8PfwltYb4tXIEsozgEFfut/bqW7H7pDvylmCjBRMG4NjtLp57V1Ev++brA==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.15.2.tgz", + "integrity": "sha512-xmMRLenT9CXmm5HMbzpZ1hWhaUowQf8UB4jMjFlAxx1QzQcsD3KFNAVX/CAWzFPtllTyTplrA4JrQ7sCH3qmYw==", "requires": { - "@opentelemetry/core": "1.11.0", - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/core": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" } }, "@opentelemetry/sdk-trace-base": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.11.0.tgz", - "integrity": "sha512-DV8e5/Qo42V8FMBlQ0Y0Liv6Hl/Pp5bAZ73s7r1euX8w4bpRes1B7ACiA4yujADbWMJxBgSo4fGbi4yjmTMG2A==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.15.2.tgz", + "integrity": "sha512-BEaxGZbWtvnSPchV98qqqqa96AOcb41pjgvhfzDij10tkBhIu9m0Jd6tZ1tJB5ZHfHbTffqYVYE0AOGobec/EQ==", "requires": { - "@opentelemetry/core": "1.11.0", - "@opentelemetry/resources": "1.11.0", - "@opentelemetry/semantic-conventions": "1.11.0" + "@opentelemetry/core": "1.15.2", + "@opentelemetry/resources": "1.15.2", + "@opentelemetry/semantic-conventions": "1.15.2" } }, "@opentelemetry/semantic-conventions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.11.0.tgz", - "integrity": "sha512-fG4D0AktoHyHwGhFGv+PzKrZjxbKJfckJauTJdq2A+ej5cTazmNYjJVAODXXkYyrsI10muMl+B1iO2q1R6Lp+w==" + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.15.2.tgz", + "integrity": "sha512-CjbOKwk2s+3xPIMcd5UNYQzsf+v94RczbdNix9/kQh38WiQkM90sUOi3if8eyHFgiBjBjhwXrA7W3ydiSQP9mw==" }, "@polka/url": { "version": "1.0.0-next.21", @@ -16468,6 +16637,11 @@ "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", "dev": true }, + "@types/shimmer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.2.tgz", + "integrity": "sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg==" + }, "@types/shortid": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz", @@ -16556,15 +16730,6 @@ "requires": { "ms": "2.1.2" } - }, - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } } } }, @@ -16624,15 +16789,6 @@ "requires": { "ms": "2.1.2" } - }, - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } } } }, @@ -16652,14 +16808,14 @@ "dev": true }, "@vscode/extension-telemetry": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.7.7.tgz", - "integrity": "sha512-uW508BPjkWDBOKvvvSym3ZmGb7kHIiWaAfB/1PHzLz2x9TrC33CfjmFEI+CywIL/jBv4bqZxxjN4tfefB61F+g==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz", + "integrity": "sha512-UqM9+KZDDK3MyoHTsg6XNM+XO6pweQxzCpqJz33BoBEYAGsbBviRYcVpJglgay2oReuDD2pOI1Nio3BKNDLhWA==", "requires": { - "@microsoft/1ds-core-js": "^3.2.9", - "@microsoft/1ds-post-js": "^3.2.9", - "@microsoft/applicationinsights-web-basic": "^2.8.11", - "applicationinsights": "2.5.0" + "@microsoft/1ds-core-js": "^3.2.13", + "@microsoft/1ds-post-js": "^3.2.13", + "@microsoft/applicationinsights-web-basic": "^3.0.2", + "applicationinsights": "^2.7.1" } }, "@vscode/jupyter-lsp-middleware": { @@ -16951,14 +17107,12 @@ "acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==" }, "acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "requires": {} }, "acorn-jsx": { @@ -17108,21 +17262,23 @@ } }, "applicationinsights": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.5.0.tgz", - "integrity": "sha512-6kIFmpANRok+6FhCOmO7ZZ/mh7fdNKn17BaT13cg/RV5roLPJlA6q8srWexayHd3MPcwMb9072e8Zp0P47s/pw==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.7.3.tgz", + "integrity": "sha512-JY8+kTEkjbA+kAVNWDtpfW2lqsrDALfDXuxOs74KLPu2y13fy/9WB52V4LfYVTVcW1/jYOXjTxNS2gPZIDh1iw==", "requires": { - "@azure/core-auth": "^1.4.0", - "@azure/core-rest-pipeline": "^1.10.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-rest-pipeline": "1.10.1", + "@azure/core-util": "1.2.0", + "@azure/opentelemetry-instrumentation-azure-sdk": "^1.0.0-beta.5", "@microsoft/applicationinsights-web-snippet": "^1.0.1", - "@opentelemetry/api": "^1.0.4", - "@opentelemetry/core": "^1.0.1", - "@opentelemetry/sdk-trace-base": "^1.0.1", - "@opentelemetry/semantic-conventions": "^1.0.1", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.2", + "@opentelemetry/sdk-trace-base": "^1.15.2", + "@opentelemetry/semantic-conventions": "^1.15.2", "cls-hooked": "^4.2.2", "continuation-local-storage": "^3.2.1", - "diagnostic-channel": "1.1.0", - "diagnostic-channel-publishers": "1.0.5" + "diagnostic-channel": "1.1.1", + "diagnostic-channel-publishers": "1.0.7" } }, "arch": { @@ -18288,6 +18444,11 @@ "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", "dev": true }, + "cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -19086,24 +19247,17 @@ "optional": true }, "diagnostic-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.0.tgz", - "integrity": "sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz", + "integrity": "sha512-r2HV5qFkUICyoaKlBEpLKHjxMXATUf/l+h8UZPGBHGLy4DDiY2sOLcIctax4eRnTw5wH2jTMExLntGPJ8eOJxw==", "requires": { - "semver": "^5.3.0" - }, - "dependencies": { - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" - } + "semver": "^7.5.3" } }, "diagnostic-channel-publishers": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.5.tgz", - "integrity": "sha512-dJwUS0915pkjjimPJVDnS/QQHsH0aOYhnZsLJdnZIMOrB+csj8RnZhWTuwnm8R5v3Z7OZs+ksv5luC14DGB7eg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.7.tgz", + "integrity": "sha512-SEECbY5AiVt6DfLkhkaHNeshg1CogdLLANA8xlG/TKvS+XUgvIKl7VspJGYiEdL5OUyzMVnr7o0AwB7f+/Mjtg==", "requires": {} }, "diff": { @@ -19653,15 +19807,6 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -20775,8 +20920,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -21253,7 +21397,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -21504,6 +21647,17 @@ } } }, + "import-in-the-middle": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz", + "integrity": "sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==", + "requires": { + "acorn": "^8.8.2", + "acorn-import-assertions": "^1.9.0", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, "import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -21657,10 +21811,9 @@ "dev": true }, "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dev": true, + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "requires": { "has": "^1.0.3" } @@ -23134,6 +23287,11 @@ } } }, + "module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" + }, "mrmime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.0.tgz", @@ -23274,18 +23432,6 @@ "optional": true, "requires": { "semver": "^7.3.5" - }, - "dependencies": { - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "optional": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "node-addon-api": { @@ -24124,8 +24270,7 @@ "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-root": { "version": "0.1.1", @@ -24703,6 +24848,26 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, + "require-in-the-middle": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", + "integrity": "sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==", + "requires": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } + } + }, "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", @@ -24710,12 +24875,11 @@ "dev": true }, "resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", "requires": { - "is-core-module": "^2.8.1", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -24894,9 +25058,9 @@ } }, "semver": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", - "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -25604,8 +25768,7 @@ "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, "sver-compat": { "version": "1.5.0", @@ -26054,15 +26217,6 @@ "picomatch": "^2.3.1" } }, - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -26589,8 +26743,7 @@ "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "v8-compile-cache": { "version": "2.3.0", @@ -26736,16 +26889,6 @@ "minimatch": "^5.1.0", "semver": "^7.3.7", "vscode-languageserver-protocol": "3.17.3" - }, - "dependencies": { - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "vscode-languageserver": { diff --git a/package.json b/package.json index 898f6e05ede1..7e044a95f7af 100644 --- a/package.json +++ b/package.json @@ -1142,7 +1142,7 @@ "scope": "machine", "type": "string" }, - "python.missingPackage.severity":{ + "python.missingPackage.severity": { "default": "Hint", "description": "%python.missingPackage.severity.description%", "enum": [ @@ -2104,7 +2104,7 @@ }, "dependencies": { "@iarna/toml": "^2.2.5", - "@vscode/extension-telemetry": "^0.7.7", + "@vscode/extension-telemetry": "^0.8.4", "@vscode/jupyter-lsp-middleware": "^0.2.50", "arch": "^2.1.0", "diff-match-patch": "^1.0.0", @@ -2221,4 +2221,4 @@ "webpack-require-from": "^1.8.6", "yargs": "^15.3.1" } -} +} \ No newline at end of file From 69e8e7d13b96c55ba38f9c3c6599fde9be0bdf83 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 7 Sep 2023 15:13:30 -0700 Subject: [PATCH 0172/1136] Catch errors when looking up python binaries in a PATH (#21948) Closes https://github.com/microsoft/vscode-python/issues/21944 --- src/client/pythonEnvironments/common/posixUtils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/client/pythonEnvironments/common/posixUtils.ts b/src/client/pythonEnvironments/common/posixUtils.ts index eb60fc029949..0e79ec9d590e 100644 --- a/src/client/pythonEnvironments/common/posixUtils.ts +++ b/src/client/pythonEnvironments/common/posixUtils.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import { uniq } from 'lodash'; import { getSearchPathEntries } from '../../common/utils/exec'; import { resolveSymbolicLink } from './externalDependencies'; -import { traceError, traceInfo, traceVerbose } from '../../logging'; +import { traceError, traceInfo, traceVerbose, traceWarn } from '../../logging'; /** * Determine if the given filename looks like the simplest Python executable. @@ -117,7 +117,10 @@ function pickShortestPath(pythonPaths: string[]) { export async function getPythonBinFromPosixPaths(searchDirs: string[]): Promise { const binToLinkMap = new Map(); for (const searchDir of searchDirs) { - const paths = await findPythonBinariesInDir(searchDir); + const paths = await findPythonBinariesInDir(searchDir).catch((ex) => { + traceWarn('Looking for python binaries within', searchDir, 'failed with', ex); + return []; + }); for (const filepath of paths) { // Ensure that we have a collection of unique global binaries by From 7196a36b98d92726fc9bc5901cd06e9cd05be05f Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 8 Sep 2023 11:47:05 -0700 Subject: [PATCH 0173/1136] Update Python extension API version (#21953) --- pythonExtensionApi/package-lock.json | 4 ++-- pythonExtensionApi/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index 9b4847457b20..1f4098e1b0de 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vscode/python-extension", - "version": "1.0.4", + "version": "1.0.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vscode/python-extension", - "version": "1.0.4", + "version": "1.0.5", "license": "MIT", "devDependencies": { "@types/vscode": "^1.78.0", diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index 86ac58f42f20..baabf85d6549 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -1,7 +1,7 @@ { "name": "@vscode/python-extension", "description": "An API facade for the Python extension in VS Code", - "version": "1.0.4", + "version": "1.0.5", "author": { "name": "Microsoft Corporation" }, From 30c83a3d32b6ab0225ec06a06904285c93efd48c Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Sat, 9 Sep 2023 01:59:07 -0700 Subject: [PATCH 0174/1136] Added git settings for branch name suggestion, protection, pull, and mergeEditor (#21954) VS Code repository, specifically in the .vscode/settings.json, has some nice git features such as: Issue: #21955 "git.branchRandomName.enable" (for suggesting random branch name when creating a new branch, comes in very handy when person wants to make and try quick changes in Codespaces), "git.branchProtection" (for branch protection), "git.pullBeforeCheckout": (for pulling before checking out a branch), "git.mergeEditor": (for making easier when in times of resolving merge conflicts) which I found could be useful to the Python extension repository as well. Credits to @karrtikr for suggesting random name, and branch protection. --- .vscode/settings.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 174a850c901e..06011b3d13cd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -65,5 +65,14 @@ "--max-line-length=88" ], "typescript.preferences.importModuleSpecifier": "relative", - "debug.javascript.usePreview": false + "debug.javascript.usePreview": false, + // Branch name suggestion. + "git.branchRandomName.enable": true, + "git.branchProtection": [ + "main", + "release/*" + ], + "git.pullBeforeCheckout": true, + // Open merge editor for resolving conflicts. + "git.mergeEditor": true, } From 8543dd356312fb3ea6b4b66561d896408de7faab Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 11 Sep 2023 11:21:44 -0400 Subject: [PATCH 0175/1136] Fix unittest subtest names that have spaces (#21947) fixes https://github.com/microsoft/vscode-python/issues/21733#issuecomment-1707804763 --- .../testController/common/resultResolver.ts | 8 +++++--- .../testController/resultResolver.unit.test.ts | 15 ++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 6e875473c836..aa9f2a541f51 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -216,9 +216,11 @@ export class PythonResultResolver implements ITestResultResolver { throw new Error('Parent test item not found'); } } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { - // split on " " since the subtest ID has the parent test ID in the first part of the ID. - const parentTestCaseId = keyTemp.split(' ')[0]; - const subtestId = keyTemp.split(' ')[1]; + // split only on first " [" since the subtest ID has the parent test ID in the first part of the ID. + const index = keyTemp.indexOf(' ['); + const parentTestCaseId = keyTemp.substring(0, index); + // add one to index to remove the space from the start of the subtest ID + const subtestId = keyTemp.substring(index + 1, keyTemp.length); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); // find the subtest's parent test item diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts index 09a68128167d..694fdacb8049 100644 --- a/src/test/testing/testController/resultResolver.unit.test.ts +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -270,15 +270,16 @@ suite('Result Resolver tests', () => { testProvider, workspaceUri, ); - const mockSubtestItem = createMockTestItem('parentTest subTest'); + const subtestName = 'parentTest [subTest with spaces and [brackets]]'; + const mockSubtestItem = createMockTestItem(subtestName); // add a mock test item to the map of known VSCode ids to run ids resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); // creates a mock test item with a space which will be used to split the runId - resultResolver.runIdToVSid.set('parentTest subTest', 'parentTest subTest'); + resultResolver.runIdToVSid.set(subtestName, subtestName); // add this mock test to the map of known test items resultResolver.runIdToTestItem.set('parentTest', mockTestItem2); - resultResolver.runIdToTestItem.set('parentTest subTest', mockSubtestItem); + resultResolver.runIdToTestItem.set(subtestName, mockSubtestItem); let generatedId: string | undefined; testControllerMock @@ -294,12 +295,12 @@ suite('Result Resolver tests', () => { cwd: workspaceUri.fsPath, status: 'success', result: { - 'parentTest subTest': { - test: 'test', + 'parentTest [subTest with spaces and [brackets]]': { + test: 'parentTest', outcome: 'subtest-success', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess message: 'message', traceback: 'traceback', - subtest: 'subtest', + subtest: subtestName, }, }, error: '', @@ -310,7 +311,7 @@ suite('Result Resolver tests', () => { // verify that the passed function was called for the single test item assert.ok(generatedId); - assert.strictEqual(generatedId, 'subTest'); + assert.strictEqual(generatedId, '[subTest with spaces and [brackets]]'); }); test('resolveExecution handles failed tests correctly', async () => { // test specific constants used expected values From e32657f83eb5d9c65f5190b91573405e91f12e6c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 11 Sep 2023 09:37:21 -0700 Subject: [PATCH 0176/1136] incorrect print included for absolute path calculations (#21932) an additional print statement was left in the pytest plugin which unnecessarily printed all absolute paths calculated. --- pythonFiles/vscode_pytest/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index adf72c134119..3d5bde44204f 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -126,7 +126,6 @@ def get_absolute_test_id(test_id: str, testPath: pathlib.Path) -> str: """ split_id = test_id.split("::")[1:] absolute_test_id = "::".join([str(testPath), *split_id]) - print("absolute path", absolute_test_id) return absolute_test_id From d9a23181279e39905fb7969fcd930fb46085c900 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Sep 2023 10:34:28 -0700 Subject: [PATCH 0177/1136] Do not assume casing of activated environment variables Python returns (#21970) For #20950 --- .../activation/terminalEnvVarCollectionService.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 9015dd7b9388..39bee239806b 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -133,12 +133,13 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); return; } - const env = await this.environmentActivationService.getActivatedEnvironmentVariables( + const activatedEnv = await this.environmentActivationService.getActivatedEnvironmentVariables( resource, undefined, undefined, shell, ); + const env = activatedEnv ? normCaseKeys(activatedEnv) : undefined; if (!env) { const shellType = identifyShellFromShellPath(shell); const defaultShell = defaultShells[this.platform.osType]; @@ -158,7 +159,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ shell, ); } - const processEnv = this.processEnvVars; + const processEnv = normCaseKeys(this.processEnvVars); // PS1 in some cases is a shell variable (not an env variable) so "env" might not contain it, calculate it in that case. env.PS1 = await this.getPS1(shell, resource, env); @@ -376,3 +377,11 @@ function getPromptForEnv(interpreter: PythonEnvironment | undefined) { } return undefined; } + +function normCaseKeys(env: EnvironmentVariables): EnvironmentVariables { + const result: EnvironmentVariables = {}; + Object.keys(env).forEach((key) => { + result[key.toUpperCase()] = env[key]; + }); + return result; +} From 91b2c113f168069f76a2f26c37a2d2f8f759a682 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 12 Sep 2023 10:52:46 -0700 Subject: [PATCH 0178/1136] Drop Python 3.7 support (#21962) Drop Python 3.7 support, and replace with Python 3.8 Resolves: #21532 /vscode-python/requirements.txt generated same hash even when running: ```pip-compile --generate-hashes requirements.in``` from the Python3.8 virtual environment. - Same result with pythonFiles/jedilsp_requirements/requirements.txt when running: ```pip-compile --generate-hashes pythonFiles/jedilsp_requirements/requirements.in``` --- .github/actions/build-vsix/action.yml | 4 ++-- build/azure-pipeline.pre-release.yml | 2 +- build/azure-pipeline.stable.yml | 2 +- build/ci/conda_env_1.yml | 2 +- pythonFiles/jedilsp_requirements/requirements.in | 2 +- pythonFiles/jedilsp_requirements/requirements.txt | 2 +- pythonFiles/tests/test_data/missing-deps.data | 2 +- pythonFiles/tests/test_data/no-missing-deps.data | 2 +- pythonFiles/tests/test_data/pyproject-missing-deps.data | 2 +- .../tests/test_data/pyproject-no-missing-deps.data | 2 +- requirements.txt | 2 +- scripts/onCreateCommand.sh | 8 ++++---- .../pythonEnvironments/creation/provider/condaUtils.ts | 2 +- .../common/envlayouts/pipenv/project1/CustomPipfileName | 2 +- .../common/envlayouts/pipenv/project2/Pipfile | 2 +- .../common/envlayouts/pipenv/project3/Pipfile | 2 +- .../common/envlayouts/workspace/folder1/Pipfile | 2 +- .../creation/installedPackagesDiagnostics.unit.test.ts | 4 ++-- 18 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index 4c783fed1c42..ae7b8fddba69 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -22,10 +22,10 @@ runs: cache: 'npm' # Jedi LS depends on dataclasses which is not in the stdlib in Python 3.7. - - name: Use Python 3.7 for JediLSP + - name: Use Python 3.8 for JediLSP uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.8 cache: 'pip' cache-dependency-path: | requirements.txt diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index d4fe0ac376fb..eed32b70c35d 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -38,7 +38,7 @@ extends: - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + versionSpec: '3.8' addToPath: true architecture: 'x64' displayName: Select Python version diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 05f83aa81824..c147f8b55164 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -33,7 +33,7 @@ extends: - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + versionSpec: '3.8' addToPath: true architecture: 'x64' displayName: Select Python version diff --git a/build/ci/conda_env_1.yml b/build/ci/conda_env_1.yml index df5c917dcf4f..e9d08d0820a4 100644 --- a/build/ci/conda_env_1.yml +++ b/build/ci/conda_env_1.yml @@ -1,4 +1,4 @@ name: conda_env_1 dependencies: - - python=3.7 + - python=3.8 - pip diff --git a/pythonFiles/jedilsp_requirements/requirements.in b/pythonFiles/jedilsp_requirements/requirements.in index 7ad7ca14fa90..0167b757cf7b 100644 --- a/pythonFiles/jedilsp_requirements/requirements.in +++ b/pythonFiles/jedilsp_requirements/requirements.in @@ -1,6 +1,6 @@ # This file is used to generate requirements.txt. # To update requirements.txt, run the following commands. -# Use Python 3.7 when creating the environment or using pip-tools +# Use Python 3.8 when creating the environment or using pip-tools # 1) pip install pip-tools # 2) pip-compile --generate-hashes --upgrade pythonFiles\jedilsp_requirements\requirements.in diff --git a/pythonFiles/jedilsp_requirements/requirements.txt b/pythonFiles/jedilsp_requirements/requirements.txt index 90d10b467640..64b1bb958f67 100644 --- a/pythonFiles/jedilsp_requirements/requirements.txt +++ b/pythonFiles/jedilsp_requirements/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --generate-hashes 'pythonFiles\jedilsp_requirements\requirements.in' diff --git a/pythonFiles/tests/test_data/missing-deps.data b/pythonFiles/tests/test_data/missing-deps.data index c42d23c7dd67..c8c911f218a8 100644 --- a/pythonFiles/tests/test_data/missing-deps.data +++ b/pythonFiles/tests/test_data/missing-deps.data @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --generate-hashes --resolver=backtracking requirements-test.in diff --git a/pythonFiles/tests/test_data/no-missing-deps.data b/pythonFiles/tests/test_data/no-missing-deps.data index 5c2f1178bbdf..d5d04476dec0 100644 --- a/pythonFiles/tests/test_data/no-missing-deps.data +++ b/pythonFiles/tests/test_data/no-missing-deps.data @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --generate-hashes --resolver=backtracking requirements-test.in diff --git a/pythonFiles/tests/test_data/pyproject-missing-deps.data b/pythonFiles/tests/test_data/pyproject-missing-deps.data index f217a0bdade6..e4d6f9eb10d3 100644 --- a/pythonFiles/tests/test_data/pyproject-missing-deps.data +++ b/pythonFiles/tests/test_data/pyproject-missing-deps.data @@ -5,5 +5,5 @@ build-backend = "flit_core.buildapi" [project] name = "something" version = "2023.0.0" -requires-python = ">=3.7" +requires-python = ">=3.8" dependencies = ["pytest==7.3.1", "flake8-csv"] diff --git a/pythonFiles/tests/test_data/pyproject-no-missing-deps.data b/pythonFiles/tests/test_data/pyproject-no-missing-deps.data index 729bc9169e6f..64dadf6fdf2e 100644 --- a/pythonFiles/tests/test_data/pyproject-no-missing-deps.data +++ b/pythonFiles/tests/test_data/pyproject-no-missing-deps.data @@ -5,5 +5,5 @@ build-backend = "flit_core.buildapi" [project] name = "something" version = "2023.0.0" -requires-python = ">=3.7" +requires-python = ">=3.8" dependencies = [jedi-language-server"] diff --git a/requirements.txt b/requirements.txt index f2af0ca4204b..8ea311d28cb1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --generate-hashes requirements.in diff --git a/scripts/onCreateCommand.sh b/scripts/onCreateCommand.sh index a90a5366417d..6303d21ef486 100644 --- a/scripts/onCreateCommand.sh +++ b/scripts/onCreateCommand.sh @@ -12,15 +12,15 @@ command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" source ~/.bashrc # Install Python via pyenv . -pyenv install 3.7:latest 3.8:latest 3.9:latest 3.10:latest 3.11:latest +pyenv install 3.8:latest 3.9:latest 3.10:latest 3.11:latest -# Set default Python version to 3.7 . -pyenv global 3.7.17 +# Set default Python version to 3.8 . +pyenv global 3.8.18 npm ci # Create Virutal environment. -pyenv exec python3.7 -m venv .venv +pyenv exec python3.8 -m venv .venv # Activate Virtual environment. source /workspaces/vscode-python/.venv/bin/activate diff --git a/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/src/client/pythonEnvironments/creation/provider/condaUtils.ts index e00a1c8dca09..8f14ffafcdaa 100644 --- a/src/client/pythonEnvironments/creation/provider/condaUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -39,7 +39,7 @@ export async function getCondaBaseEnv(): Promise { } export async function pickPythonVersion(token?: CancellationToken): Promise { - const items: QuickPickItem[] = ['3.10', '3.11', '3.9', '3.8', '3.7'].map((v) => ({ + const items: QuickPickItem[] = ['3.10', '3.11', '3.9', '3.8'].map((v) => ({ label: v === RECOMMENDED_CONDA_PYTHON ? `${Octicons.Star} Python` : 'Python', description: v, })); diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName b/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName index b723d0199f86..b5846df18ca8 100644 --- a/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile b/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile b/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile index b723d0199f86..b5846df18ca8 100644 --- a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile @@ -8,4 +8,4 @@ verify_ssl = true [packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts b/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts index 10fe06bba442..4addb5687085 100644 --- a/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts +++ b/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts @@ -62,7 +62,7 @@ function getPyProjectTomlFile(): typemoq.IMock { .setup((p) => p.getText(typemoq.It.isAny())) .returns( () => - '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[project]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.7"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[project]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.8"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', ); return someFile; } @@ -76,7 +76,7 @@ function getSomeTomlFile(): typemoq.IMock { .setup((p) => p.getText(typemoq.It.isAny())) .returns( () => - '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[something]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.7"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[something]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.8"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', ); return someFile; } From 7aa6660d58f66afa929169c7436a992fdaefe93f Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Sep 2023 11:30:16 -0700 Subject: [PATCH 0179/1136] Clear environment collection only after all async operations are done (#21975) For #20950 --- .../activation/terminalEnvVarCollectionService.ts | 6 ++++-- .../activation/terminalEnvVarCollectionService.unit.test.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 39bee239806b..75ef8168484b 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -127,9 +127,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ const workspaceFolder = this.getWorkspaceFolder(resource); const settings = this.configurationService.getSettings(resource); const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); - // Clear any previously set env vars from collection - envVarCollection.clear(); if (!settings.terminal.activateEnvironment) { + envVarCollection.clear(); traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); return; } @@ -150,6 +149,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ return; } await this.trackTerminalPrompt(shell, resource, env); + envVarCollection.clear(); this.processEnvVars = undefined; return; } @@ -164,6 +164,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ // PS1 in some cases is a shell variable (not an env variable) so "env" might not contain it, calculate it in that case. env.PS1 = await this.getPS1(shell, resource, env); + // Clear any previously set env vars from collection + envVarCollection.clear(); Object.keys(env).forEach((key) => { if (shouldSkip(key)) { return; diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 1513be676ee4..cedc2701112f 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -570,7 +570,7 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); - verify(collection.clear()).twice(); + verify(collection.clear()).once(); }); test('If no activated variables are returned for default shell, clear collection', async () => { From df0b493b0a342ae03bfa377025578f450575fab2 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 12 Sep 2023 13:26:10 -0700 Subject: [PATCH 0180/1136] handle subprocess segfaults for testAdapters (#21963) closes: https://github.com/microsoft/vscode-python/issues/21662 Not only does this make sure segfaults are correct for unittest but also for pytest. --- .../testing/testController/common/server.ts | 18 ++- .../testing/testController/common/types.ts | 2 + .../testing/testController/common/utils.ts | 26 +++- .../pytest/pytestExecutionAdapter.ts | 13 +- .../unittest/testDiscoveryAdapter.ts | 2 +- .../unittest/testExecutionAdapter.ts | 2 +- .../testing/common/testingAdapter.test.ts | 130 ++++++++++++++++-- .../errorWorkspace/test_seg_fault.py | 18 +++ .../test_parameterized_subtest.py | 2 +- 9 files changed, 191 insertions(+), 22 deletions(-) create mode 100644 src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 8797a861fb4a..d0a225ae5712 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -15,7 +15,7 @@ import { traceError, traceInfo, traceLog } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; -import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER } from './utils'; +import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER, createExecutionErrorPayload } from './utils'; import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { @@ -133,6 +133,10 @@ export class PythonTestServer implements ITestServer, Disposable { return this._onDiscoveryDataReceived.event; } + public triggerRunDataReceivedEvent(payload: DataReceivedEvent): void { + this._onRunDataReceived.fire(payload); + } + public dispose(): void { this.server.close(); this._onDataReceived.dispose(); @@ -146,6 +150,7 @@ export class PythonTestServer implements ITestServer, Disposable { options: TestCommandOptions, runTestIdPort?: string, runInstance?: TestRun, + testIds?: string[], callback?: () => void, ): Promise { const { uuid } = options; @@ -218,8 +223,15 @@ export class PythonTestServer implements ITestServer, Disposable { result?.proc?.stderr?.on('data', (data) => { spawnOptions?.outputChannel?.append(data.toString()); }); - result?.proc?.on('exit', () => { - traceLog('Exec server closed.', uuid); + result?.proc?.on('exit', (code, signal) => { + // if the child has testIds then this is a run request + if (code !== 0 && testIds) { + // if the child process exited with a non-zero exit code, then we need to send the error payload. + this._onRunDataReceived.fire({ + uuid, + data: JSON.stringify(createExecutionErrorPayload(code, signal, testIds, options.cwd)), + }); + } deferred.resolve({ stdout: '', stderr: '' }); callback?.(); }); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 16c0bd0e3cee..610c3d76d17b 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -178,12 +178,14 @@ export interface ITestServer { options: TestCommandOptions, runTestIdsPort?: string, runInstance?: TestRun, + testIds?: string[], callback?: () => void, ): Promise; serverReady(): Promise; getPort(): number; createUUID(cwd: string): string; deleteUUID(uuid: string): void; + triggerRunDataReceivedEvent(data: DataReceivedEvent): void; } export interface ITestResultResolver { runIdToVSid: Map; diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index f98550d3e72b..dd1b51551a45 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -9,7 +9,7 @@ import { EnableTestAdapterRewrite } from '../../../common/experiments/groups'; import { IExperimentService } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilities'; -import { DiscoveredTestItem, DiscoveredTestNode, ITestResultResolver } from './types'; +import { DiscoveredTestItem, DiscoveredTestNode, ExecutionTestPayload, ITestResultResolver } from './types'; export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); @@ -188,3 +188,27 @@ export function populateTestTree( function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem { return test.type_ === 'test'; } + +export function createExecutionErrorPayload( + code: number | null, + signal: NodeJS.Signals | null, + testIds: string[], + cwd: string, +): ExecutionTestPayload { + const etp: ExecutionTestPayload = { + cwd, + status: 'error', + error: 'Test run failed, the python test process was terminated before it could exit on its own.', + result: {}, + }; + // add error result for each attempted test. + for (let i = 0; i < testIds.length; i = i + 1) { + const test = testIds[i]; + etp.result![test] = { + test, + outcome: 'error', + message: ` \n The python test process was terminated before it could exit on its own, the process errored with: Code: ${code}, Signal: ${signal}`, + }; + } + return etp; +} diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 96d53db22c1c..9705374d74af 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -5,7 +5,7 @@ import { TestRun, Uri } from 'vscode'; import * as path from 'path'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred } from '../../../common/utils/async'; -import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, @@ -130,7 +130,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { if (debugBool && !testArgs.some((a) => a.startsWith('--capture') || a === '-s')) { testArgs.push('--capture', 'no'); } - traceLog(`Running PYTEST execution for the following test ids: ${testIds}`); const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); if (spawnOptions.extraVariables) @@ -175,7 +174,15 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { this.outputChannel?.append(data.toString()); }); - result?.proc?.on('exit', () => { + result?.proc?.on('exit', (code, signal) => { + // if the child has testIds then this is a run request + if (code !== 0 && testIds) { + // if the child process exited with a non-zero exit code, then we need to send the error payload. + this.testServer.triggerRunDataReceivedEvent({ + uuid, + data: JSON.stringify(utils.createExecutionErrorPayload(code, signal, testIds, cwd)), + }); + } deferredExec.resolve({ stdout: '', stderr: '' }); deferred.resolve(); disposeDataReceiver?.(this.testServer); diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 1cbad7ef65ef..9820aa89626c 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -64,7 +64,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } private async callSendCommand(options: TestCommandOptions, callback: () => void): Promise { - await this.testServer.sendCommand(options, undefined, undefined, callback); + await this.testServer.sendCommand(options, undefined, undefined, [], callback); const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; return discoveryPayload; } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 9af9e593c246..bc5e41d19d9d 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -83,7 +83,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const runTestIdsPort = await startTestIdServer(testIds); - await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, () => { + await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, testIds, () => { deferred.resolve(); disposeDataReceiver?.(this.testServer); }); diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 1334085e4cea..5af7e59f6a46 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -13,7 +13,7 @@ import { ITestDebugLauncher } from '../../../client/testing/common/types'; import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize'; -import { traceError, traceLog } from '../../../client/logging'; +import { traceLog } from '../../../client/logging'; import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; @@ -39,6 +39,12 @@ suite('End to End Tests: test adapters', () => { 'testTestingRootWkspc', 'largeWorkspace', ); + const rootPathErrorWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'errorWorkspace', + ); suiteSetup(async () => { serviceContainer = (await initialize()).serviceContainer; }); @@ -86,10 +92,21 @@ suite('End to End Tests: test adapters', () => { await discoveryAdapter.discoverTests(workspaceUri).finally(() => { // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); + // resultResolver.verify( + // (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), + // typeMoq.Times.once(), + // ); + + // 1. Check the status is "success" + assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + // 2. Confirm no errors + assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + }); + + await discoveryAdapter.discoverTests(Uri.parse(rootPathErrorWorkspace)).finally(() => { + // verification after discovery is complete // 1. Check the status is "success" assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); @@ -191,7 +208,6 @@ suite('End to End Tests: test adapters', () => { resultResolver .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns((data) => { - traceLog(`resolveDiscovery ${data}`); actualData = data; return Promise.resolve(); }); @@ -231,7 +247,6 @@ suite('End to End Tests: test adapters', () => { resultResolver .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns((data) => { - traceLog(`resolveExecution ${data}`); actualData = data; return Promise.resolve(); }); @@ -263,7 +278,6 @@ suite('End to End Tests: test adapters', () => { (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), typeMoq.Times.once(), ); - // 1. Check the status is "success" assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); // 2. Confirm tests are found @@ -275,7 +289,6 @@ suite('End to End Tests: test adapters', () => { resultResolver .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns((data) => { - traceError(`resolveExecution ${data}`); traceLog(`resolveExecution ${data}`); // do the following asserts for each time resolveExecution is called, should be called once per test. // 1. Check the status, can be subtest success or failure @@ -328,7 +341,6 @@ suite('End to End Tests: test adapters', () => { resultResolver .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns((data) => { - traceLog(`resolveExecution ${data}`); actualData = data; return Promise.resolve(); }); @@ -366,7 +378,6 @@ suite('End to End Tests: test adapters', () => { (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), typeMoq.Times.once(), ); - // 1. Check the status is "success" assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); // 2. Confirm no errors @@ -379,7 +390,6 @@ suite('End to End Tests: test adapters', () => { resultResolver .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns((data) => { - traceLog(`resolveExecution ${data}`); // do the following asserts for each time resolveExecution is called, should be called once per test. // 1. Check the status is "success" assert.strictEqual(data.status, 'success', "Expected status to be 'success'"); @@ -424,4 +434,100 @@ suite('End to End Tests: test adapters', () => { ); }); }); + test('unittest execution adapter seg fault error handling', async () => { + const testId = `test_seg_fault.TestSegmentationFault.test_segfault`; + const testIds: string[] = [testId]; + resultResolver + .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + // 1. Check the status is "success" + assert.strictEqual(data.status, 'error', "Expected status to be 'error'"); + // 2. Confirm no errors + assert.ok(data.error, "Expected errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(data.result, 'Expected results to be present'); + // 4. make sure the testID is found in the results + assert.notDeepEqual( + JSON.stringify(data).search('test_seg_fault.TestSegmentationFault.test_segfault'), + -1, + 'Expected testId to be present', + ); + return Promise.resolve(); + }); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathErrorWorkspace); + + // run pytest execution + const executionAdapter = new UnittestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object).finally(() => { + resultResolver.verify( + (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.exactly(1), + ); + }); + }); + test('pytest execution adapter seg fault error handling', async () => { + const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; + const testIds: string[] = [testId]; + resultResolver + .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((data) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + // 1. Check the status is "success" + assert.strictEqual(data.status, 'error', "Expected status to be 'error'"); + // 2. Confirm no errors + assert.ok(data.error, "Expected errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(data.result, 'Expected results to be present'); + // 4. make sure the testID is found in the results + assert.notDeepEqual( + JSON.stringify(data).search('test_seg_fault.py::TestSegmentationFault::test_segfault'), + -1, + 'Expected testId to be present', + ); + return Promise.resolve(); + }); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathErrorWorkspace); + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel, + resultResolver.object, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).finally(() => { + resultResolver.verify( + (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), + typeMoq.Times.exactly(1), + ); + }); + }); }); diff --git a/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py b/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py new file mode 100644 index 000000000000..bad7ff8fcbbd --- /dev/null +++ b/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +import ctypes + + +class TestSegmentationFault(unittest.TestCase): + def cause_segfault(self): + ctypes.string_at(0) # Dereference a NULL pointer + + def test_segfault(self): + assert True + self.cause_segfault() + + +if __name__ == "__main__": + unittest.main() diff --git a/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py index 3e84df0a2d9f..8c6a29adf495 100644 --- a/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py +++ b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py @@ -6,7 +6,7 @@ @pytest.mark.parametrize("num", range(0, 200)) def test_odd_even(num): - return num % 2 == 0 + assert num % 2 == 0 class NumbersTest(unittest.TestCase): From 2268d5382b31b5500286f02bff2f12590b069369 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 12 Sep 2023 15:57:11 -0700 Subject: [PATCH 0181/1136] Add support to delete and re-create .conda environments (#21977) Fix https://github.com/microsoft/vscode-python/issues/21828 --- src/client/common/utils/localize.ts | 16 +++- .../creation/common/commonUtils.ts | 8 ++ .../provider/condaCreationProvider.ts | 81 ++++++++++++---- .../creation/provider/condaDeleteUtils.ts | 37 ++++++++ .../creation/provider/condaUtils.ts | 93 ++++++++++++++++++- .../condaCreationProvider.unit.test.ts | 8 ++ .../provider/condaDeleteUtils.unit.test.ts | 71 ++++++++++++++ .../creation/provider/condaUtils.unit.test.ts | 69 +++++++++++++- 8 files changed, 354 insertions(+), 29 deletions(-) create mode 100644 src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts create mode 100644 src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 4cda15e15ec0..7ec517f82277 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -464,8 +464,10 @@ export namespace CreateEnv { export const error = l10n.t('Creating virtual environment failed with error.'); export const tomlExtrasQuickPickTitle = l10n.t('Select optional dependencies to install from pyproject.toml'); export const requirementsQuickPickTitle = l10n.t('Select dependencies to install'); - export const recreate = l10n.t('Recreate'); - export const recreateDescription = l10n.t('Delete existing ".venv" environment and create a new one'); + export const recreate = l10n.t('Delete and Recreate'); + export const recreateDescription = l10n.t( + 'Delete existing ".venv" directory and create a new ".venv" environment', + ); export const useExisting = l10n.t('Use Existing'); export const useExistingDescription = l10n.t('Use existing ".venv" environment with no changes to it'); export const existingVenvQuickPickPlaceholder = l10n.t( @@ -485,6 +487,16 @@ export namespace CreateEnv { ); export const creating = l10n.t('Creating conda environment...'); export const providerDescription = l10n.t('Creates a `.conda` Conda environment in the current workspace'); + + export const recreate = l10n.t('Delete and Recreate'); + export const recreateDescription = l10n.t('Delete existing ".conda" environment and create a new one'); + export const useExisting = l10n.t('Use Existing'); + export const useExistingDescription = l10n.t('Use existing ".conda" environment with no changes to it'); + export const existingCondaQuickPickPlaceholder = l10n.t( + 'Choose an option to handle the existing ".conda" environment', + ); + export const deletingEnvironmentProgress = l10n.t('Deleting existing ".conda" environment...'); + export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".conda" environment.'); } } diff --git a/src/client/pythonEnvironments/creation/common/commonUtils.ts b/src/client/pythonEnvironments/creation/common/commonUtils.ts index b4d4a37eae9b..16d8015e3f26 100644 --- a/src/client/pythonEnvironments/creation/common/commonUtils.ts +++ b/src/client/pythonEnvironments/creation/common/commonUtils.ts @@ -32,3 +32,11 @@ export function getVenvExecutable(workspaceFolder: WorkspaceFolder): string { } return path.join(getVenvPath(workspaceFolder), 'bin', 'python'); } + +export function getPrefixCondaEnvPath(workspaceFolder: WorkspaceFolder): string { + return path.join(workspaceFolder.uri.fsPath, '.conda'); +} + +export async function hasPrefixCondaEnv(workspaceFolder: WorkspaceFolder): Promise { + return fs.pathExists(getPrefixCondaEnvPath(workspaceFolder)); +} diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 7ca44c1b7eff..86e0b56801cd 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -9,11 +9,18 @@ import { CreateEnvironmentProgress } from '../types'; import { pickWorkspaceFolder } from '../common/workspaceSelection'; import { execObservable } from '../../../common/process/rawProcessApis'; import { createDeferred } from '../../../common/utils/async'; -import { getEnvironmentVariable, getOSType, OSType } from '../../../common/utils/platform'; +import { getOSType, OSType } from '../../../common/utils/platform'; import { createCondaScript } from '../../../common/process/internal/scripts'; import { Common, CreateEnv } from '../../../common/utils/localize'; -import { getCondaBaseEnv, pickPythonVersion } from './condaUtils'; -import { showErrorMessageWithLogs } from '../common/commonUtils'; +import { + ExistingCondaAction, + deleteEnvironment, + getCondaBaseEnv, + getPathEnvVariableForConda, + pickExistingCondaAction, + pickPythonVersion, +} from './condaUtils'; +import { getPrefixCondaEnvPath, showErrorMessageWithLogs } from '../common/commonUtils'; import { MultiStepAction, MultiStepNode, withProgress } from '../../../common/vscodeApis/windowApis'; import { EventName } from '../../../telemetry/constants'; import { sendTelemetryEvent } from '../../../telemetry'; @@ -83,22 +90,7 @@ async function createCondaEnv( }); const deferred = createDeferred(); - let pathEnv = getEnvironmentVariable('PATH') || getEnvironmentVariable('Path') || ''; - if (getOSType() === OSType.Windows) { - // On windows `conda.bat` is used, which adds the following bin directories to PATH - // then launches `conda.exe` which is a stub to `python.exe -m conda`. Here, we are - // instead using the `python.exe` that ships with conda to run a python script that - // handles conda env creation and package installation. - // See conda issue: https://github.com/conda/conda/issues/11399 - const root = path.dirname(command); - const libPath1 = path.join(root, 'Library', 'bin'); - const libPath2 = path.join(root, 'Library', 'mingw-w64', 'bin'); - const libPath3 = path.join(root, 'Library', 'usr', 'bin'); - const libPath4 = path.join(root, 'bin'); - const libPath5 = path.join(root, 'Scripts'); - const libPath = [libPath1, libPath2, libPath3, libPath4, libPath5].join(path.delimiter); - pathEnv = `${libPath}${path.delimiter}${pathEnv}`; - } + const pathEnv = getPathEnvVariableForConda(command); traceLog('Running Conda Env creation script: ', [command, ...args]); const { proc, out, dispose } = execObservable(command, args, { mergeStdOutErr: true, @@ -182,6 +174,29 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise { + if (workspace && context === MultiStepAction.Continue) { + try { + existingCondaAction = await pickExistingCondaAction(workspace); + return MultiStepAction.Continue; + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + return MultiStepAction.Continue; + }, + undefined, + ); + workspaceStep.next = existingEnvStep; + let version: string | undefined; const versionStep = new MultiStepNode( workspaceStep, @@ -204,13 +219,39 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise { + const condaEnvPath = getPrefixCondaEnvPath(workspace); + const command = interpreter; + const args = ['-m', 'conda', 'env', 'remove', '--prefix', condaEnvPath, '--yes']; + try { + traceInfo(`Deleting conda environment: ${condaEnvPath}`); + traceInfo(`Running command: ${command} ${args.join(' ')}`); + const result = await plainExec(command, args, { mergeStdOutErr: true }, { ...process.env, PATH: pathEnvVar }); + traceInfo(result.stdout); + if (await hasPrefixCondaEnv(workspace)) { + // If conda cannot delete files it will name the files as .conda_trash. + // These need to be deleted manually. + traceError(`Conda environment ${condaEnvPath} could not be deleted.`); + traceError(`Please delete the environment manually: ${condaEnvPath}`); + showErrorMessageWithLogs(CreateEnv.Conda.errorDeletingEnvironment); + return false; + } + } catch (err) { + showErrorMessageWithLogs(CreateEnv.Conda.errorDeletingEnvironment); + traceError(`Deleting conda environment ${condaEnvPath} Failed with error: `, err); + return false; + } + return true; +} diff --git a/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/src/client/pythonEnvironments/creation/provider/condaUtils.ts index 8f14ffafcdaa..51c55e414245 100644 --- a/src/client/pythonEnvironments/creation/provider/condaUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -1,14 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, QuickPickItem, Uri } from 'vscode'; -import { Common } from '../../../browser/localize'; -import { Octicons } from '../../../common/constants'; -import { CreateEnv } from '../../../common/utils/localize'; +import * as path from 'path'; +import { CancellationToken, ProgressLocation, QuickPickItem, Uri, WorkspaceFolder } from 'vscode'; +import { Commands, Octicons } from '../../../common/constants'; +import { Common, CreateEnv } from '../../../common/utils/localize'; import { executeCommand } from '../../../common/vscodeApis/commandApis'; -import { showErrorMessage, showQuickPickWithBack } from '../../../common/vscodeApis/windowApis'; +import { + MultiStepAction, + showErrorMessage, + showQuickPickWithBack, + withProgress, +} from '../../../common/vscodeApis/windowApis'; import { traceLog } from '../../../logging'; import { Conda } from '../../common/environmentManagers/conda'; +import { getPrefixCondaEnvPath, hasPrefixCondaEnv } from '../common/commonUtils'; +import { OSType, getEnvironmentVariable, getOSType } from '../../../common/utils/platform'; +import { deleteCondaEnvironment } from './condaDeleteUtils'; const RECOMMENDED_CONDA_PYTHON = '3.10'; @@ -59,3 +67,78 @@ export async function pickPythonVersion(token?: CancellationToken): Promise { + const condaEnvPath = getPrefixCondaEnvPath(workspaceFolder); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.Conda.deletingEnvironmentProgress} ([${Common.showLogs}](command:${Commands.ViewOutput})): ${condaEnvPath}`, + cancellable: false, + }, + async () => deleteCondaEnvironment(workspaceFolder, interpreter, getPathEnvVariableForConda(interpreter)), + ); +} + +export enum ExistingCondaAction { + Recreate, + UseExisting, + Create, +} + +export async function pickExistingCondaAction( + workspaceFolder: WorkspaceFolder | undefined, +): Promise { + if (workspaceFolder) { + if (await hasPrefixCondaEnv(workspaceFolder)) { + const items: QuickPickItem[] = [ + { label: CreateEnv.Conda.recreate, description: CreateEnv.Conda.recreateDescription }, + { + label: CreateEnv.Conda.useExisting, + description: CreateEnv.Conda.useExistingDescription, + }, + ]; + + const selection = (await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Conda.existingCondaQuickPickPlaceholder, + ignoreFocusOut: true, + }, + undefined, + )) as QuickPickItem | undefined; + + if (selection?.label === CreateEnv.Conda.recreate) { + return ExistingCondaAction.Recreate; + } + + if (selection?.label === CreateEnv.Conda.useExisting) { + return ExistingCondaAction.UseExisting; + } + } else { + return ExistingCondaAction.Create; + } + } + + throw MultiStepAction.Cancel; +} diff --git a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts index e1ac1bafe6ac..3195d1f88ea9 100644 --- a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -35,6 +35,7 @@ suite('Conda Creation provider tests', () => { let execObservableStub: sinon.SinonStub; let withProgressStub: sinon.SinonStub; let showErrorMessageWithLogsStub: sinon.SinonStub; + let pickExistingCondaActionStub: sinon.SinonStub; setup(() => { pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); @@ -46,6 +47,9 @@ suite('Conda Creation provider tests', () => { showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); showErrorMessageWithLogsStub.resolves(); + pickExistingCondaActionStub = sinon.stub(condaUtils, 'pickExistingCondaAction'); + pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.Create); + progressMock = typemoq.Mock.ofType(); condaProvider = condaCreationProvider(); }); @@ -77,6 +81,7 @@ suite('Conda Creation provider tests', () => { pickPythonVersionStub.resolves(undefined); await assert.isRejected(condaProvider.createEnvironment()); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); test('Create conda environment', async () => { @@ -136,6 +141,7 @@ suite('Conda Creation provider tests', () => { workspaceFolder: workspace1, }); assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); test('Create conda environment failed', async () => { @@ -188,6 +194,7 @@ suite('Conda Creation provider tests', () => { const result = await promise; assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); test('Create conda environment failed (non-zero exit code)', async () => { @@ -245,5 +252,6 @@ suite('Conda Creation provider tests', () => { const result = await promise; assert.ok(result?.error); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(pickExistingCondaActionStub.calledOnce); }); }); diff --git a/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts new file mode 100644 index 000000000000..b1acd0678714 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { deleteCondaEnvironment } from '../../../../client/pythonEnvironments/creation/provider/condaDeleteUtils'; + +suite('Conda Delete test', () => { + let plainExecStub: sinon.SinonStub; + let getPrefixCondaEnvPathStub: sinon.SinonStub; + let hasPrefixCondaEnvStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); + getPrefixCondaEnvPathStub = sinon.stub(commonUtils, 'getPrefixCondaEnvPath'); + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete conda env ', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(false); + plainExecStub.resolves({ stdout: 'stdout' }); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isTrue(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.calledOnce); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete conda env with error', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(true); + plainExecStub.resolves({ stdout: 'stdout' }); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isFalse(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.calledOnce); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete conda env with exception', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(false); + plainExecStub.rejects(new Error('error')); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isFalse(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts index 3f115f9f58ed..a3f4a1abe905 100644 --- a/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts @@ -3,9 +3,17 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; -import { CancellationTokenSource } from 'vscode'; +import * as path from 'path'; +import { CancellationTokenSource, Uri } from 'vscode'; import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; -import { pickPythonVersion } from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; +import { + ExistingCondaAction, + pickExistingCondaAction, + pickPythonVersion, +} from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { CreateEnv } from '../../../../client/common/utils/localize'; suite('Conda Utils test', () => { let showQuickPickWithBackStub: sinon.SinonStub; @@ -43,3 +51,60 @@ suite('Conda Utils test', () => { assert.isUndefined(actual); }); }); + +suite('Existing .conda env test', () => { + let hasPrefixCondaEnvStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No .conda found', async () => { + hasPrefixCondaEnvStub.resolves(false); + showQuickPickWithBackStub.resolves(undefined); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.Create); + assert.isTrue(showQuickPickWithBackStub.notCalled); + }); + + test('User presses escape', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves(undefined); + await assert.isRejected(pickExistingCondaAction(workspace1)); + }); + + test('.conda found and user selected to re-create', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Conda.recreate, + description: CreateEnv.Conda.recreateDescription, + }); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.Recreate); + }); + + test('.conda found and user selected to re-use', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Conda.useExisting, + description: CreateEnv.Conda.useExistingDescription, + }); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.UseExisting); + }); +}); From 9ebc5eb3a26f16cb963935ff01d86b03e1f34e7e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Sep 2023 18:01:29 -0700 Subject: [PATCH 0182/1136] Fix `${command:python.interpreterPath}` in tasks.json in multiroot workspaces (#21980) Closes https://github.com/microsoft/vscode-python/issues/21915 --- .../launch.json/interpreterPathCommand.ts | 2 +- .../interpreterPathCommand.unit.test.ts | 14 ++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts b/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts index e4c14de407c9..21c8d0f1147b 100644 --- a/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts +++ b/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts @@ -41,7 +41,7 @@ export class InterpreterPathCommand implements IExtensionSingleActivationService let workspaceFolderUri; try { - workspaceFolderUri = workspaceFolder ? Uri.parse(workspaceFolder) : undefined; + workspaceFolderUri = workspaceFolder ? Uri.file(workspaceFolder) : undefined; } catch (ex) { workspaceFolderUri = undefined; } diff --git a/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts index c773e1cbd5bc..77077ad945fb 100644 --- a/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts +++ b/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts @@ -45,7 +45,7 @@ suite('Interpreter Path Command', () => { test('If `workspaceFolder` property exists in `args`, it is used to retrieve setting from config', async () => { const args = { workspaceFolder: 'folderPath' }; when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { - assert.deepEqual(arg, Uri.parse('folderPath')); + assert.deepEqual(arg, Uri.file('folderPath')); return Promise.resolve({ path: 'settingValue' }) as unknown; }); @@ -56,7 +56,7 @@ suite('Interpreter Path Command', () => { test('If `args[1]` is defined, it is used to retrieve setting from config', async () => { const args = ['command', 'folderPath']; when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { - assert.deepEqual(arg, Uri.parse('folderPath')); + assert.deepEqual(arg, Uri.file('folderPath')); return Promise.resolve({ path: 'settingValue' }) as unknown; }); @@ -73,14 +73,4 @@ suite('Interpreter Path Command', () => { const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); expect(setting).to.equal('settingValue'); }); - - test('If `args[1]` is not a valid uri', async () => { - const args = ['command', '${input:some_input}']; - when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { - assert.deepEqual(arg, undefined); - return Promise.resolve({ path: 'settingValue' }) as unknown; - }); - const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); - expect(setting).to.equal('settingValue'); - }); }); From 221b769c084462952a724e42e314f26506f76688 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 13 Sep 2023 12:01:12 -0700 Subject: [PATCH 0183/1136] Open requirement files (#21917) Closes https://github.com/microsoft/vscode-python/issues/21984 ![image](https://github.com/microsoft/vscode-python/assets/3840081/a5cc4991-7d65-4980-b35e-6453a85f516d) --- src/client/common/utils/localize.ts | 1 + src/client/common/vscodeApis/windowApis.ts | 12 ++++ .../creation/provider/venvUtils.ts | 40 ++++++++--- .../creation/provider/venvUtils.unit.test.ts | 71 +++++++++++++++++-- 4 files changed, 112 insertions(+), 12 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 7ec517f82277..05b525bdf5bf 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -475,6 +475,7 @@ export namespace CreateEnv { ); export const deletingEnvironmentProgress = l10n.t('Deleting existing ".venv" environment...'); export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".venv" environment.'); + export const openRequirementsFile = l10n.t('Open requirements file'); } export namespace Conda { diff --git a/src/client/common/vscodeApis/windowApis.ts b/src/client/common/vscodeApis/windowApis.ts index 1c242314cb87..c761ff60fa65 100644 --- a/src/client/common/vscodeApis/windowApis.ts +++ b/src/client/common/vscodeApis/windowApis.ts @@ -16,9 +16,15 @@ import { TextEditor, window, Disposable, + QuickPickItemButtonEvent, + Uri, } from 'vscode'; import { createDeferred, Deferred } from '../utils/async'; +export function showTextDocument(uri: Uri): Thenable { + return window.showTextDocument(uri); +} + export function showQuickPick( items: readonly T[] | Thenable, options?: QuickPickOptions, @@ -91,6 +97,7 @@ export async function showQuickPickWithBack( items: readonly T[], options?: QuickPickOptions, token?: CancellationToken, + itemButtonHandler?: (e: QuickPickItemButtonEvent) => void, ): Promise { const quickPick: QuickPick = window.createQuickPick(); const disposables: Disposable[] = [quickPick]; @@ -130,6 +137,11 @@ export async function showQuickPickWithBack( deferred.resolve(undefined); } }), + quickPick.onDidTriggerItemButton((e) => { + if (itemButtonHandler) { + itemButtonHandler(e); + } + }), ); if (token) { disposables.push( diff --git a/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/src/client/pythonEnvironments/creation/provider/venvUtils.ts index d7a0be170f99..723337b2a7fa 100644 --- a/src/client/pythonEnvironments/creation/provider/venvUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -5,12 +5,22 @@ import * as tomljs from '@iarna/toml'; import * as fs from 'fs-extra'; import { flatten, isArray } from 'lodash'; import * as path from 'path'; -import { CancellationToken, ProgressLocation, QuickPickItem, RelativePattern, WorkspaceFolder } from 'vscode'; +import { + CancellationToken, + ProgressLocation, + QuickPickItem, + QuickPickItemButtonEvent, + RelativePattern, + ThemeIcon, + Uri, + WorkspaceFolder, +} from 'vscode'; import { Common, CreateEnv } from '../../../common/utils/localize'; import { MultiStepAction, MultiStepNode, showQuickPickWithBack, + showTextDocument, withProgress, } from '../../../common/vscodeApis/windowApis'; import { findFiles } from '../../../common/vscodeApis/workspaceApis'; @@ -20,6 +30,10 @@ import { isWindows } from '../../../common/platform/platformService'; import { getVenvPath, hasVenv } from '../common/commonUtils'; import { deleteEnvironmentNonWindows, deleteEnvironmentWindows } from './venvDeleteUtils'; +export const OPEN_REQUIREMENTS_BUTTON = { + iconPath: new ThemeIcon('go-to-file'), + tooltip: CreateEnv.Venv.openRequirementsFile, +}; const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**'; async function getPipRequirementsFiles( workspaceFolder: WorkspaceFolder, @@ -78,8 +92,13 @@ async function pickTomlExtras(extras: string[], token?: CancellationToken): Prom return undefined; } -async function pickRequirementsFiles(files: string[], token?: CancellationToken): Promise { +async function pickRequirementsFiles( + files: string[], + root: string, + token?: CancellationToken, +): Promise { const items: QuickPickItem[] = files + .map((p) => path.relative(root, p)) .sort((a, b) => { const al: number = a.split(/[\\\/]/).length; const bl: number = b.split(/[\\\/]/).length; @@ -91,7 +110,10 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken) } return al - bl; }) - .map((e) => ({ label: e })); + .map((e) => ({ + label: e, + buttons: [OPEN_REQUIREMENTS_BUTTON], + })); const selection = await showQuickPickWithBack( items, @@ -101,6 +123,11 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken) canPickMany: true, }, token, + async (e: QuickPickItemButtonEvent) => { + if (e.item.label) { + await showTextDocument(Uri.file(path.join(root, e.item.label))); + } + }, ); if (selection && isArray(selection)) { @@ -195,14 +222,11 @@ export async function pickPackagesToInstall( tomlStep, async (context?: MultiStepAction) => { traceVerbose('Looking for pip requirements.'); - const requirementFiles = (await getPipRequirementsFiles(workspaceFolder, token))?.map((p) => - path.relative(workspaceFolder.uri.fsPath, p), - ); - + const requirementFiles = await getPipRequirementsFiles(workspaceFolder, token); if (requirementFiles && requirementFiles.length > 0) { traceVerbose('Found pip requirements.'); try { - const result = await pickRequirementsFiles(requirementFiles, token); + const result = await pickRequirementsFiles(requirementFiles, workspaceFolder.uri.fsPath, token); const installList = result?.map((p) => path.join(workspaceFolder.uri.fsPath, p)); if (installList) { installList.forEach((i) => { diff --git a/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts index ae4f43a0296c..1671026d5dd4 100644 --- a/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts @@ -10,11 +10,13 @@ import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; import { ExistingVenvAction, + OPEN_REQUIREMENTS_BUTTON, pickExistingVenvAction, pickPackagesToInstall, } from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; import { CreateEnv } from '../../../../client/common/utils/localize'; +import { createDeferred } from '../../../../client/common/utils/async'; chaiUse(chaiAsPromised); @@ -23,6 +25,7 @@ suite('Venv Utils test', () => { let showQuickPickWithBackStub: sinon.SinonStub; let pathExistsStub: sinon.SinonStub; let readFileStub: sinon.SinonStub; + let showTextDocumentStub: sinon.SinonStub; const workspace1 = { uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), @@ -35,6 +38,7 @@ suite('Venv Utils test', () => { showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); pathExistsStub = sinon.stub(fs, 'pathExists'); readFileStub = sinon.stub(fs, 'readFile'); + showTextDocumentStub = sinon.stub(windowApis, 'showTextDocument'); }); teardown(() => { @@ -224,13 +228,18 @@ suite('Venv Utils test', () => { await assert.isRejected(pickPackagesToInstall(workspace1)); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.isTrue(readFileStub.calledOnce); @@ -257,13 +266,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.deepStrictEqual(actual, []); @@ -290,13 +304,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.deepStrictEqual(actual, [ @@ -328,13 +347,18 @@ suite('Venv Utils test', () => { const actual = await pickPackagesToInstall(workspace1); assert.isTrue( showQuickPickWithBackStub.calledWithExactly( - [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], { placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, ignoreFocusOut: true, canPickMany: true, }, undefined, + sinon.match.func, ), ); assert.deepStrictEqual(actual, [ @@ -349,6 +373,45 @@ suite('Venv Utils test', () => { ]); assert.isTrue(readFileStub.notCalled); }); + + test('User clicks button to open requirements.txt', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + const deferred = createDeferred(); + showQuickPickWithBackStub.callsFake(async (_items, _options, _token, callback) => { + callback({ + button: OPEN_REQUIREMENTS_BUTTON, + item: { label: 'requirements.txt' }, + }); + await deferred.promise; + return [{ label: 'requirements.txt' }]; + }); + + let uri: Uri | undefined; + showTextDocumentStub.callsFake((arg: Uri) => { + uri = arg; + deferred.resolve(); + return Promise.resolve(); + }); + + await pickPackagesToInstall(workspace1); + assert.deepStrictEqual( + uri?.toString(), + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')).toString(), + ); + }); }); suite('Test pick existing venv action', () => { From 1040f3c842798fd0efb1c51aafd31395179ac153 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 13 Sep 2023 23:32:54 -0700 Subject: [PATCH 0184/1136] Use stdin if workspace has large number of requirements (#21988) Closes https://github.com/microsoft/vscode-python/issues/21480 --- pythonFiles/create_venv.py | 24 ++- pythonFiles/tests/test_create_venv.py | 51 ++++++ .../creation/provider/venvCreationProvider.ts | 37 +++-- .../venvCreationProvider.unit.test.ts | 155 +++++++++++++++++- 4 files changed, 253 insertions(+), 14 deletions(-) diff --git a/pythonFiles/create_venv.py b/pythonFiles/create_venv.py index cac084fd2222..68f21a38b980 100644 --- a/pythonFiles/create_venv.py +++ b/pythonFiles/create_venv.py @@ -3,6 +3,7 @@ import argparse import importlib.util as import_util +import json import os import pathlib import subprocess @@ -56,6 +57,12 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace: metavar="NAME", action="store", ) + parser.add_argument( + "--stdin", + action="store_true", + default=False, + help="Read arguments from stdin.", + ) return parser.parse_args(argv) @@ -152,6 +159,16 @@ def install_pip(name: str): ) +def get_requirements_from_args(args: argparse.Namespace) -> List[str]: + requirements = [] + if args.stdin: + data = json.loads(sys.stdin.read()) + requirements = data.get("requirements", []) + if args.requirements: + requirements.extend(args.requirements) + return requirements + + def main(argv: Optional[Sequence[str]] = None) -> None: if argv is None: argv = [] @@ -223,9 +240,10 @@ def main(argv: Optional[Sequence[str]] = None) -> None: print(f"VENV_INSTALLING_PYPROJECT: {args.toml}") install_toml(venv_path, args.extras) - if args.requirements: - print(f"VENV_INSTALLING_REQUIREMENTS: {args.requirements}") - install_requirements(venv_path, args.requirements) + requirements = get_requirements_from_args(args) + if requirements: + print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}") + install_requirements(venv_path, requirements) if __name__ == "__main__": diff --git a/pythonFiles/tests/test_create_venv.py b/pythonFiles/tests/test_create_venv.py index ae3f18be6f3c..772fc02708f8 100644 --- a/pythonFiles/tests/test_create_venv.py +++ b/pythonFiles/tests/test_create_venv.py @@ -1,7 +1,11 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import argparse +import contextlib import importlib +import io +import json import os import sys @@ -224,3 +228,50 @@ def run_process(args, error_message): create_venv.run_process = run_process create_venv.main([]) + + +@contextlib.contextmanager +def redirect_io(stream: str, new_stream): + """Redirect stdio streams to a custom stream.""" + old_stream = getattr(sys, stream) + setattr(sys, stream, new_stream) + yield + setattr(sys, stream, old_stream) + + +class CustomIO(io.TextIOWrapper): + """Custom stream object to replace stdio.""" + + name: str = "customio" + + def __init__(self, name: str, encoding="utf-8", newline=None): + self._buffer = io.BytesIO() + self._buffer.name = name + super().__init__(self._buffer, encoding=encoding, newline=newline) + + def close(self): + """Provide this close method which is used by some tools.""" + # This is intentionally empty. + + def get_value(self) -> str: + """Returns value from the buffer as string.""" + self.seek(0) + return self.read() + + +def test_requirements_from_stdin(): + importlib.reload(create_venv) + + cli_requirements = [f"cli-requirement{i}.txt" for i in range(3)] + args = argparse.Namespace() + args.__dict__.update({"stdin": True, "requirements": cli_requirements}) + + stdin_requirements = [f"stdin-requirement{i}.txt" for i in range(20)] + text = json.dumps({"requirements": stdin_requirements}) + str_input = CustomIO("", encoding="utf-8", newline="\n") + with redirect_io("stdin", str_input): + str_input.write(text) + str_input.seek(0) + actual = create_venv.get_requirements_from_args(args) + + assert actual == stdin_requirements + cli_requirements diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index edbdcd7d84a6..61850e404c3d 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -32,8 +32,14 @@ import { CreateEnvironmentResult, } from '../proposed.createEnvApis'; -function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgnore?: boolean): string[] { +interface IVenvCommandArgs { + argv: string[]; + stdin: string | undefined; +} + +function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgnore?: boolean): IVenvCommandArgs { const command: string[] = [createVenvScript()]; + let stdin: string | undefined; if (addGitIgnore) { command.push('--git-ignore'); @@ -52,14 +58,21 @@ function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgn }); const requirements = installInfo.filter((i) => i.installType === 'requirements').map((i) => i.installItem); - requirements.forEach((r) => { - if (r) { - command.push('--requirements', r); - } - }); + + if (requirements.length < 10) { + requirements.forEach((r) => { + if (r) { + command.push('--requirements', r); + } + }); + } else { + command.push('--stdin'); + // Too many requirements can cause the command line to be too long error. + stdin = JSON.stringify({ requirements }); + } } - return command; + return { argv: command, stdin }; } function getVenvFromOutput(output: string): string | undefined { @@ -81,7 +94,7 @@ function getVenvFromOutput(output: string): string | undefined { async function createVenv( workspace: WorkspaceFolder, command: string, - args: string[], + args: IVenvCommandArgs, progress: CreateEnvironmentProgress, token?: CancellationToken, ): Promise { @@ -94,11 +107,15 @@ async function createVenv( }); const deferred = createDeferred(); - traceLog('Running Env creation script: ', [command, ...args]); - const { proc, out, dispose } = execObservable(command, args, { + traceLog('Running Env creation script: ', [command, ...args.argv]); + if (args.stdin) { + traceLog('Requirements passed in via stdin: ', args.stdin); + } + const { proc, out, dispose } = execObservable(command, args.argv, { mergeStdOutErr: true, token, cwd: workspace.uri.fsPath, + stdinStr: args.stdin, }); const progressAndTelemetry = new VenvProgressAndTelemetry(progress); diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index de65887b7edc..72914b9118e2 100644 --- a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -15,7 +15,7 @@ import * as rawProcessApis from '../../../../client/common/process/rawProcessApi import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; import { createDeferred } from '../../../../client/common/utils/async'; -import { Output } from '../../../../client/common/process/types'; +import { Output, SpawnOptions } from '../../../../client/common/process/types'; import { VENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry'; import { CreateEnv } from '../../../../client/common/utils/localize'; import * as venvUtils from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; @@ -394,4 +394,157 @@ suite('venv Creation provider tests', () => { assert.isTrue(showErrorMessageWithLogsStub.notCalled); assert.isTrue(deleteEnvironmentStub.notCalled); }); + + test('Create venv with 1000 requirement files', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const requirements = Array.from({ length: 1000 }, (_, i) => ({ + installType: 'requirements', + installItem: `requirements${i}.txt`, + })); + pickPackagesToInstallStub.resolves(requirements); + const expected = JSON.stringify({ requirements: requirements.map((r) => r.installItem) }); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + let stdin: undefined | string; + let hasStdinArg = false; + execObservableStub.callsFake((_c, argv: string[], options) => { + stdin = options?.stdinStr; + hasStdinArg = argv.includes('--stdin'); + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + assert.strictEqual(stdin, expected); + assert.isTrue(hasStdinArg); + }); + + test('Create venv with 5 requirement files', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const requirements = Array.from({ length: 5 }, (_, i) => ({ + installType: 'requirements', + installItem: `requirements${i}.txt`, + })); + pickPackagesToInstallStub.resolves(requirements); + const expectedRequirements = requirements.map((r) => r.installItem).sort(); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + let stdin: undefined | string; + let hasStdinArg = false; + let actualRequirements: string[] = []; + execObservableStub.callsFake((_c, argv: string[], options: SpawnOptions) => { + stdin = options?.stdinStr; + actualRequirements = argv.filter((arg) => arg.startsWith('requirements')).sort(); + hasStdinArg = argv.includes('--stdin'); + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + assert.isUndefined(stdin); + assert.deepStrictEqual(actualRequirements, expectedRequirements); + assert.isFalse(hasStdinArg); + }); }); From 203f58bc169afc5a731557633d25722bed038bd1 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 14 Sep 2023 11:54:59 -0700 Subject: [PATCH 0185/1136] Fix bug in rawprocess where stdinStr was not passed (#21993) --- src/client/common/process/rawProcessApis.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/common/process/rawProcessApis.ts b/src/client/common/process/rawProcessApis.ts index 6f3e40d68736..23c2f3253bb1 100644 --- a/src/client/common/process/rawProcessApis.ts +++ b/src/client/common/process/rawProcessApis.ts @@ -257,6 +257,10 @@ export function execObservable( subscriber.error(ex); internalDisposables.forEach((d) => d.dispose()); }); + if (options.stdinStr !== undefined) { + proc.stdin?.write(options.stdinStr); + proc.stdin?.end(); + } }); return { From f3f48a2e662759b04857aa801d7b8abf7cdbca30 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 15 Sep 2023 04:54:05 -0700 Subject: [PATCH 0186/1136] Remove envShellEvent proposal usage (#21997) It's been finalized Part of microsoft/vscode#193181 --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7e044a95f7af..98f97c4a1905 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "enabledApiProposals": [ "contribEditorContentMenu", "quickPickSortByLabel", - "envShellEvent", "testObserver", "quickPickItemTooltip", "saveEditor" @@ -44,7 +43,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.82.0-20230830" + "vscode": "^1.83.0-20230915" }, "enableTelemetry": false, "keywords": [ @@ -2221,4 +2220,4 @@ "webpack-require-from": "^1.8.6", "yargs": "^15.3.1" } -} \ No newline at end of file +} From 187ca86a5411fbf886128b585fe4e58439c5bd47 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 15 Sep 2023 13:34:05 -0700 Subject: [PATCH 0187/1136] Do not upper case custom env variables (#22004) For #20950 closes https://github.com/microsoft/vscode-python/issues/22005 --- .../interpreter/activation/terminalEnvVarCollectionService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 75ef8168484b..9bc95ee6d2e3 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -36,6 +36,7 @@ import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; import { TerminalShellType } from '../../common/terminal/types'; import { OSType } from '../../common/utils/platform'; +import { normCase } from '../../common/platform/fs-paths'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { @@ -383,7 +384,7 @@ function getPromptForEnv(interpreter: PythonEnvironment | undefined) { function normCaseKeys(env: EnvironmentVariables): EnvironmentVariables { const result: EnvironmentVariables = {}; Object.keys(env).forEach((key) => { - result[key.toUpperCase()] = env[key]; + result[normCase(key)] = env[key]; }); return result; } From 5838ea64a365f9abc1fa623cb8e372d4d9628a50 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 15 Sep 2023 15:10:10 -0700 Subject: [PATCH 0188/1136] pytest complicated parameterize test parsing (#22001) fixes https://github.com/microsoft/vscode-python/issues/22000 --- .../pytestadapter/.data/parametrize_tests.py | 8 +++++--- .../expected_discovery_test_output.py | 20 +++++++++---------- pythonFiles/tests/pytestadapter/helpers.py | 4 ++++ pythonFiles/vscode_pytest/__init__.py | 6 +++--- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py index a39b7c26de9f..c4dbadc32d6e 100644 --- a/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py +++ b/pythonFiles/tests/pytestadapter/.data/parametrize_tests.py @@ -15,6 +15,8 @@ def test_adding(actual, expected): # Testing pytest with parametrized tests. All three pass. # The tests ids are parametrize_tests.py::test_under_ten[1] and so on. -@pytest.mark.parametrize("num", range(1, 3)) # test_marker--test_under_ten -def test_under_ten(num): - assert num < 10 +@pytest.mark.parametrize( # test_marker--test_string + "string", ["hello", "complicated split [] ()"] +) +def test_string(string): + assert string == "hello" diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 2b2c07ab8ea7..31686d2b3b5d 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -594,46 +594,46 @@ ], }, { - "name": "test_under_ten", + "name": "test_string", "path": os.fspath(parameterize_tests_path), "type_": "function", "children": [ { - "name": "[1]", + "name": "[hello]", "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( - "test_under_ten[1]", + "test_string[hello]", parameterize_tests_path, ), "type_": "test", "id_": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[1]", + "parametrize_tests.py::test_string[hello]", parameterize_tests_path, ), "runID": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[1]", + "parametrize_tests.py::test_string[hello]", parameterize_tests_path, ), }, { - "name": "[2]", + "name": "[complicated split [] ()]", "path": os.fspath(parameterize_tests_path), "lineno": find_test_line_number( - "test_under_ten[2]", + "test_string[1]", parameterize_tests_path, ), "type_": "test", "id_": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[2]", + "parametrize_tests.py::test_string[complicated split [] ()]", parameterize_tests_path, ), "runID": get_absolute_test_id( - "parametrize_tests.py::test_under_ten[2]", + "parametrize_tests.py::test_string[complicated split [] ()]", parameterize_tests_path, ), }, ], - "id_": "parametrize_tests.py::test_under_ten", + "id_": "parametrize_tests.py::test_string", }, ], }, diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index 7195cfe43ea5..b534e950945a 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -12,6 +12,10 @@ import uuid from typing import Any, Dict, List, Optional, Tuple +script_dir = pathlib.Path(__file__).parent.parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) + TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" from typing_extensions import TypedDict diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 3d5bde44204f..9e315d7919bb 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -392,9 +392,9 @@ def build_test_tree(session: pytest.Session) -> TestNode: elif hasattr(test_case, "callspec"): # This means it is a parameterized test. function_name: str = "" # parameterized test cases cut the repetitive part of the name off. - name_split = test_node["name"].split("[") - test_node["name"] = "[" + name_split[1] - parent_path = os.fspath(get_node_path(test_case)) + "::" + name_split[0] + parent_part, parameterized_section = test_node["name"].split("[", 1) + test_node["name"] = "[" + parameterized_section + parent_path = os.fspath(get_node_path(test_case)) + "::" + parent_part try: function_name = test_case.originalname # type: ignore function_test_case = function_nodes_dict[parent_path] From 6b3dec49fa5d9fe3046d3c4bdbefaec7e7d3b806 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 15 Sep 2023 15:47:18 -0700 Subject: [PATCH 0189/1136] Support EOT for testing (#21876) Adds support for the end of transmission (EOT) operator to all pytest and unittest responses. this PR includes: - addition of an EOT that is added to run and discovery returns and processed by the extension to initiate cleanup after run/discovery finishes - updates to all tests to support the use of EOT - redesign of how cleanup works following run/discover to make it more streamlined - full functional tests that check multiple different types of payload splitting from the buffer - tests for the cancellation token during run and debug modes --- pythonFiles/testing_tools/socket_manager.py | 8 +- .../tests/pytestadapter/test_discovery.py | 129 +++-- .../tests/pytestadapter/test_execution.py | 110 ++-- pythonFiles/unittestadapter/discovery.py | 41 +- pythonFiles/unittestadapter/execution.py | 43 +- pythonFiles/vscode_pytest/__init__.py | 147 +++-- .../vscode_pytest/run_pytest_script.py | 2 +- .../testController/common/resultResolver.ts | 50 +- .../testing/testController/common/server.ts | 116 ++-- .../testing/testController/common/types.ts | 20 +- .../testing/testController/common/utils.ts | 105 +++- .../testing/testController/controller.ts | 1 + .../pytest/pytestDiscoveryAdapter.ts | 22 +- .../pytest/pytestExecutionAdapter.ts | 62 +- .../unittest/testDiscoveryAdapter.ts | 9 +- .../unittest/testExecutionAdapter.ts | 28 +- .../testing/common/testingAdapter.test.ts | 533 +++++++++-------- .../testing/common/testingPayloadsEot.test.ts | 210 +++++++ .../testController/payloadTestCases.ts | 149 +++++ .../pytestExecutionAdapter.unit.test.ts | 44 +- .../resultResolver.unit.test.ts | 31 +- .../testController/server.unit.test.ts | 538 ++++++------------ .../testCancellationRunAdapters.unit.test.ts | 350 ++++++++++++ .../testing/testController/utils.unit.test.ts | 22 +- .../test_parameterized_subtest.py | 4 +- 25 files changed, 1882 insertions(+), 892 deletions(-) create mode 100644 src/test/testing/common/testingPayloadsEot.test.ts create mode 100644 src/test/testing/testController/payloadTestCases.ts create mode 100644 src/test/testing/testController/testCancellationRunAdapters.unit.test.ts diff --git a/pythonFiles/testing_tools/socket_manager.py b/pythonFiles/testing_tools/socket_manager.py index 372a50b5e012..b2afbf0e5a17 100644 --- a/pythonFiles/testing_tools/socket_manager.py +++ b/pythonFiles/testing_tools/socket_manager.py @@ -23,6 +23,12 @@ def __init__(self, addr): self.socket = None def __enter__(self): + return self.connect() + + def __exit__(self, *_): + self.close() + + def connect(self): self.socket = socket.socket( socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP ) @@ -35,7 +41,7 @@ def __enter__(self): return self - def __exit__(self, *_): + def close(self): if self.socket: try: self.socket.shutdown(socket.SHUT_RDWR) diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 8d785be27c8b..674d92ac0545 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -29,15 +29,26 @@ def test_import_error(tmp_path): temp_dir.mkdir() p = temp_dir / "error_pytest_import.py" shutil.copyfile(file_path, p) - actual_list: Optional[List[Dict[str, Any]]] = runner( - ["--collect-only", os.fspath(p)] - ) - assert actual_list - for actual in actual_list: - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + actual: Optional[List[Dict[str, Any]]] = runner(["--collect-only", os.fspath(p)]) + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + assert False def test_syntax_error(tmp_path): @@ -60,13 +71,25 @@ def test_syntax_error(tmp_path): p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) actual = runner(["--collect-only", os.fspath(p)]) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + assert False def test_parameterized_error_collect(): @@ -76,12 +99,25 @@ def test_parameterized_error_collect(): """ file_path_str = "error_parametrize_discovery.py" actual = runner(["--collect-only", file_path_str]) - if actual: - actual = actual[0] - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 2 + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + assert False @pytest.mark.parametrize( @@ -146,13 +182,16 @@ def test_pytest_collect(file, expected_const): os.fspath(TEST_DATA_PATH / file), ] ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert actual["tests"] == expected_const + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + actual_item = actual_list.pop(0) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + assert actual_item.get("tests") == expected_const def test_pytest_root_dir(): @@ -168,14 +207,16 @@ def test_pytest_root_dir(): ], TEST_DATA_PATH / "root", ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + actual_item = actual_list.pop(0) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH / "root") assert ( - actual["tests"] + actual_item.get("tests") == expected_discovery_test_output.root_with_config_expected_output ) @@ -193,13 +234,15 @@ def test_pytest_config_file(): ], TEST_DATA_PATH / "root", ) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "tests")) - assert actual["status"] == "success" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "root") + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + actual_item = actual_list.pop(0) + assert all(item in actual_item.keys() for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH / "root") assert ( - actual["tests"] + actual_item.get("tests") == expected_discovery_test_output.root_with_config_expected_output ) diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index 07354b01709b..37a392f66d4b 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import os import shutil +from typing import Any, Dict, List import pytest @@ -23,14 +24,19 @@ def test_config_file(): expected_execution_test_output.config_file_pytest_expected_execution_output ) assert actual - assert len(actual) == len(expected_const) + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + assert len(actual_list) == len(expected_const) actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(new_cwd) - actual_result_dict.update(a["result"]) - assert actual_result_dict == expected_const + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "result") + ) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(new_cwd) + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const def test_rootdir_specified(): @@ -43,14 +49,19 @@ def test_rootdir_specified(): expected_execution_test_output.config_file_pytest_expected_execution_output ) assert actual - assert len(actual) == len(expected_const) + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + assert len(actual_list) == len(expected_const) actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(new_cwd) - actual_result_dict.update(a["result"]) - assert actual_result_dict == expected_const + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "result") + ) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(new_cwd) + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const def test_syntax_error_execution(tmp_path): @@ -73,13 +84,23 @@ def test_syntax_error_execution(tmp_path): p = temp_dir / "error_syntax_discovery.py" shutil.copyfile(file_path, p) actual = runner(["error_syntax_discover.py::test_function"]) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 1 + assert actual + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 1 + else: + assert False def test_bad_id_error_execution(): @@ -88,13 +109,23 @@ def test_bad_id_error_execution(): The json should still be returned but the errors list should be present. """ actual = runner(["not/a/real::test_id"]) - if actual: - actual = actual[0] - assert actual - assert all(item in actual for item in ("status", "cwd", "error")) - assert actual["status"] == "error" - assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["error"]) == 1 + assert actual + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "error") + ) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 1 + else: + assert False @pytest.mark.parametrize( @@ -195,7 +226,8 @@ def test_pytest_execution(test_ids, expected_const): 3. uf_single_method_execution_expected_output: test run on a single method in a file. 4. uf_non_adjacent_tests_execution_expected_output: test run on unittests in two files with single selection in test explorer. 5. unit_pytest_same_file_execution_expected_output: test run on a file with both unittest and pytest tests. - 6. dual_level_nested_folder_execution_expected_output: test run on a file with one test file at the top level and one test file in a nested folder. + 6. dual_level_nested_folder_execution_expected_output: test run on a file with one test file + at the top level and one test file in a nested folder. 7. double_nested_folder_expected_execution_output: test run on a double nested folder. 8. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. 9. single_parametrize_tests_expected_execution_output: test run on single parametrize test. @@ -205,18 +237,22 @@ def test_pytest_execution(test_ids, expected_const): Keyword arguments: test_ids -- an array of test_ids to run. expected_const -- a dictionary of the expected output from running pytest discovery on the files. - """ # noqa: E501 + """ args = test_ids actual = runner(args) assert actual - print(actual) - assert len(actual) == len(expected_const) + actual_list: List[Dict[str, Any]] = actual + assert actual_list.pop(-1).get("eot") + assert len(actual_list) == len(expected_const) actual_result_dict = dict() - for a in actual: - assert all(item in a for item in ("status", "cwd", "result")) - assert a["status"] == "success" - assert a["cwd"] == os.fspath(TEST_DATA_PATH) - actual_result_dict.update(a["result"]) + if actual_list is not None: + for actual_item in actual_list: + assert all( + item in actual_item.keys() for item in ("status", "cwd", "result") + ) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + actual_result_dict.update(actual_item["result"]) for key in actual_result_dict: if ( actual_result_dict[key]["outcome"] == "failure" diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index cbad40ad1838..6208d24bee9f 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -48,6 +48,13 @@ class PayloadDict(TypedDict): error: NotRequired[List[str]] +class EOTPayloadDict(TypedDict): + """A dictionary that is used to send a end of transmission post request to the server.""" + + command_type: Literal["discovery"] | Literal["execution"] + eot: bool + + def discover_tests( start_dir: str, pattern: str, top_level_dir: Optional[str], uuid: Optional[str] ) -> PayloadDict: @@ -106,17 +113,7 @@ def discover_tests( return payload -if __name__ == "__main__": - # Get unittest discovery arguments. - argv = sys.argv[1:] - index = argv.index("--udiscovery") - - start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) - - # Perform test discovery. - port, uuid = parse_discovery_cli_args(argv[:index]) - payload = discover_tests(start_dir, pattern, top_level_dir, uuid) - +def post_response(payload: PayloadDict | EOTPayloadDict, port: int, uuid: str) -> None: # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) data = json.dumps(payload) @@ -132,3 +129,25 @@ def discover_tests( except Exception as e: print(f"Error sending response: {e}") print(f"Request data: {request}") + + +if __name__ == "__main__": + # Get unittest discovery arguments. + argv = sys.argv[1:] + index = argv.index("--udiscovery") + + start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) + + # Perform test discovery. + port, uuid = parse_discovery_cli_args(argv[:index]) + # Post this discovery payload. + if uuid is not None: + payload = discover_tests(start_dir, pattern, top_level_dir, uuid) + post_response(payload, port, uuid) + # Post EOT token. + eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} + post_response(eot_payload, port, uuid) + else: + print("Error: no uuid provided or parsed.") + eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} + post_response(eot_payload, port, "") diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index f239f81c2d87..7bbc97e78f31 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import argparse +import atexit import enum import json import os @@ -17,7 +18,7 @@ sys.path.append(os.fspath(script_dir)) sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from typing_extensions import NotRequired, TypeAlias, TypedDict +from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict from testing_tools import process_json_util, socket_manager from unittestadapter.utils import parse_unittest_args @@ -168,6 +169,13 @@ class PayloadDict(TypedDict): error: NotRequired[str] +class EOTPayloadDict(TypedDict): + """A dictionary that is used to send a end of transmission post request to the server.""" + + command_type: Literal["discovery"] | Literal["execution"] + eot: bool + + # Args: start_path path to a directory or a file, list of ids that may be empty. # Edge cases: # - if tests got deleted since the VS Code side last ran discovery and the current test run, @@ -225,8 +233,11 @@ def run_tests( return payload +__socket = None +atexit.register(lambda: __socket.close() if __socket else None) + + def send_run_data(raw_data, port, uuid): - # Build the request data (it has to be a POST request or the Node side will not process it), and send it. status = raw_data["outcome"] cwd = os.path.abspath(START_DIR) if raw_data["subtest"]: @@ -236,7 +247,20 @@ def send_run_data(raw_data, port, uuid): test_dict = {} test_dict[test_id] = raw_data payload: PayloadDict = {"cwd": cwd, "status": status, "result": test_dict} + post_response(payload, port, uuid) + + +def post_response(payload: PayloadDict | EOTPayloadDict, port: int, uuid: str) -> None: + # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) + global __socket + if __socket is None: + try: + __socket = socket_manager.SocketManager(addr) + __socket.connect() + except Exception as error: + print(f"Plugin error connection error[vscode-pytest]: {error}") + __socket = None data = json.dumps(payload) request = f"""Content-Length: {len(data)} Content-Type: application/json @@ -244,11 +268,10 @@ def send_run_data(raw_data, port, uuid): {data}""" try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Error sending response: {e}") + if __socket is not None and __socket.socket is not None: + __socket.socket.sendall(request.encode("utf-8")) + except Exception as ex: + print(f"Error sending response: {ex}") print(f"Request data: {request}") @@ -312,3 +335,9 @@ def send_run_data(raw_data, port, uuid): "error": "No test ids received from buffer", "result": None, } + eot_payload: EOTPayloadDict = {"command_type": "execution", "eot": True} + if UUID is None: + print("Error sending response, uuid unknown to python server.") + post_response(eot_payload, PORT, "unknown") + else: + post_response(eot_payload, PORT, UUID) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 9e315d7919bb..21165a02bf4b 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -1,7 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import atexit import json import os import pathlib import sys +import time import traceback import pytest @@ -301,12 +306,6 @@ def pytest_sessionfinish(session, exitstatus): 4: Pytest encountered an internal error or exception during test execution. 5: Pytest was unable to find any tests to run. """ - print( - "pytest session has finished, exit status: ", - exitstatus, - "in discovery? ", - IS_DISCOVERY, - ) cwd = pathlib.Path.cwd() if IS_DISCOVERY: if not (exitstatus == 0 or exitstatus == 1 or exitstatus == 5): @@ -352,6 +351,10 @@ def pytest_sessionfinish(session, exitstatus): exitstatus_bool, None, ) + # send end of transmission token + command_type = "discovery" if IS_DISCOVERY else "execution" + payload: EOTPayloadDict = {"command_type": command_type, "eot": True} + send_post_request(payload) def build_test_tree(session: pytest.Session) -> TestNode: @@ -603,44 +606,60 @@ class ExecutionPayloadDict(Dict): error: Union[str, None] # Currently unused need to check +class EOTPayloadDict(TypedDict): + """A dictionary that is used to send a end of transmission post request to the server.""" + + command_type: Literal["discovery"] | Literal["execution"] + eot: bool + + def get_node_path(node: Any) -> pathlib.Path: + """A function that returns the path of a node given the switch to pathlib.Path.""" return getattr(node, "path", pathlib.Path(node.fspath)) +__socket = None +atexit.register(lambda: __socket.close() if __socket else None) + + def execution_post( - cwd: str, - status: Literal["success", "error"], - tests: Union[testRunResultDict, None], + cwd: str, status: Literal["success", "error"], tests: Union[testRunResultDict, None] ): """ - Sends a post request to the server after the tests have been executed. - Keyword arguments: - cwd -- the current working directory. - session_node -- the status of running the tests - tests -- the tests that were run and their status. + Sends a POST request with execution payload details. + + Args: + cwd (str): Current working directory. + status (Literal["success", "error"]): Execution status indicating success or error. + tests (Union[testRunResultDict, None]): Test run results, if available. """ - testPort = os.getenv("TEST_PORT", 45454) - testuuid = os.getenv("TEST_UUID") + payload: ExecutionPayloadDict = ExecutionPayloadDict( cwd=cwd, status=status, result=tests, not_found=None, error=None ) if ERRORS: payload["error"] = ERRORS + send_post_request(payload) - addr = ("localhost", int(testPort)) - data = json.dumps(payload) - request = f"""Content-Length: {len(data)} -Content-Type: application/json -Request-uuid: {testuuid} -{data}""" - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Plugin error connection error[vscode-pytest]: {e}") - print(f"[vscode-pytest] data: {request}") +def post_response(cwd: str, session_node: TestNode) -> None: + """ + Sends a POST request with test session details in payload. + + Args: + cwd (str): Current working directory. + session_node (TestNode): Node information of the test session. + """ + + payload: DiscoveryPayloadDict = { + "cwd": cwd, + "status": "success" if not ERRORS else "error", + "tests": session_node, + "error": [], + } + if ERRORS is not None: + payload["error"] = ERRORS + send_post_request(payload, cls_encoder=PathEncoder) class PathEncoder(json.JSONEncoder): @@ -652,35 +671,55 @@ def default(self, obj): return super().default(obj) -def post_response(cwd: str, session_node: TestNode) -> None: - """Sends a post request to the server. +def send_post_request( + payload: ExecutionPayloadDict | DiscoveryPayloadDict | EOTPayloadDict, + cls_encoder=None, +): + """ + Sends a post request to the server. Keyword arguments: - cwd -- the current working directory. - session_node -- the session node, which is the top of the testing tree. - errors -- a list of errors that occurred during test collection. + payload -- the payload data to be sent. + cls_encoder -- a custom encoder if needed. """ - payload: DiscoveryPayloadDict = { - "cwd": cwd, - "status": "success" if not ERRORS else "error", - "tests": session_node, - "error": [], - } - if ERRORS is not None: - payload["error"] = ERRORS - test_port: Union[str, int] = os.getenv("TEST_PORT", 45454) - test_uuid: Union[str, None] = os.getenv("TEST_UUID") - addr = "localhost", int(test_port) - data = json.dumps(payload, cls=PathEncoder) + testPort = os.getenv("TEST_PORT", 45454) + testuuid = os.getenv("TEST_UUID") + addr = ("localhost", int(testPort)) + global __socket + + if __socket is None: + try: + __socket = socket_manager.SocketManager(addr) + __socket.connect() + except Exception as error: + print(f"Plugin error connection error[vscode-pytest]: {error}") + __socket = None + + data = json.dumps(payload, cls=cls_encoder) request = f"""Content-Length: {len(data)} Content-Type: application/json -Request-uuid: {test_uuid} +Request-uuid: {testuuid} {data}""" - try: - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) - except Exception as e: - print(f"Plugin error connection error[vscode-pytest]: {e}") - print(f"[vscode-pytest] data: {request}") + + max_retries = 3 + retries = 0 + while retries < max_retries: + try: + if __socket is not None and __socket.socket is not None: + __socket.socket.sendall(request.encode("utf-8")) + # print("Post request sent successfully!") + # print("data sent", payload, "end of data") + break # Exit the loop if the send was successful + else: + print("Plugin error connection error[vscode-pytest]") + print(f"[vscode-pytest] data: {request}") + except Exception as error: + print(f"Plugin error connection error[vscode-pytest]: {error}") + print(f"[vscode-pytest] data: {request}") + retries += 1 # Increment retry counter + if retries < max_retries: + print(f"Retrying ({retries}/{max_retries}) in 2 seconds...") + time.sleep(2) # Wait for a short duration before retrying + else: + print("Maximum retry attempts reached. Cannot send post request.") diff --git a/pythonFiles/vscode_pytest/run_pytest_script.py b/pythonFiles/vscode_pytest/run_pytest_script.py index ffb4d0c55b16..0fca8208a406 100644 --- a/pythonFiles/vscode_pytest/run_pytest_script.py +++ b/pythonFiles/vscode_pytest/run_pytest_script.py @@ -53,7 +53,7 @@ buffer = b"" # Process the JSON data - print(f"Received JSON data: {test_ids_from_buffer}") + print("Received JSON data in run script") break except json.JSONDecodeError: # JSON decoding error, the complete JSON object is not yet received diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index aa9f2a541f51..5ef6695ca280 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -3,7 +3,7 @@ import { CancellationToken, TestController, TestItem, Uri, TestMessage, Location, TestRun } from 'vscode'; import * as util from 'util'; -import { DiscoveredTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; +import { DiscoveredTestPayload, EOTTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; import { TestProvider } from '../../types'; import { traceError, traceLog } from '../../../logging'; import { Testing } from '../../../common/utils/localize'; @@ -12,6 +12,7 @@ import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { splitLines } from '../../../common/stringUtils'; import { buildErrorNodeOptions, fixLogLines, populateTestTree } from './utils'; +import { Deferred } from '../../../common/utils/async'; export class PythonResultResolver implements ITestResultResolver { testController: TestController; @@ -35,16 +36,30 @@ export class PythonResultResolver implements ITestResultResolver { this.vsIdToRunId = new Map(); } - public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise { - const workspacePath = this.workspaceUri.fsPath; - traceLog('Using result resolver for discovery'); - - const rawTestData = payload; - if (!rawTestData) { + public resolveDiscovery( + payload: DiscoveredTestPayload | EOTTestPayload, + deferredTillEOT: Deferred, + token?: CancellationToken, + ): Promise { + if (!payload) { // No test data is available return Promise.resolve(); } + if ('eot' in payload) { + // the payload is an EOT payload, so resolve the deferred promise. + traceLog('ResultResolver EOT received for discovery.'); + const eotPayload = payload as EOTTestPayload; + if (eotPayload.eot === true) { + deferredTillEOT.resolve(); + return Promise.resolve(); + } + } + return this._resolveDiscovery(payload as DiscoveredTestPayload, token); + } + public _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise { + const workspacePath = this.workspaceUri.fsPath; + const rawTestData = payload as DiscoveredTestPayload; // Check if there were any errors in the discovery process. if (rawTestData.status === 'error') { const testingErrorConst = @@ -87,8 +102,25 @@ export class PythonResultResolver implements ITestResultResolver { return Promise.resolve(); } - public resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise { - const rawTestExecData = payload; + public resolveExecution( + payload: ExecutionTestPayload | EOTTestPayload, + runInstance: TestRun, + deferredTillEOT: Deferred, + ): Promise { + if (payload !== undefined && 'eot' in payload) { + // the payload is an EOT payload, so resolve the deferred promise. + traceLog('ResultResolver EOT received for execution.'); + const eotPayload = payload as EOTTestPayload; + if (eotPayload.eot === true) { + deferredTillEOT.resolve(); + return Promise.resolve(); + } + } + return this._resolveExecution(payload as ExecutionTestPayload, runInstance); + } + + public _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise { + const rawTestExecData = payload as ExecutionTestPayload; if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { // Map which holds the subtest information for each test item. diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index d0a225ae5712..f59c486f7a85 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -15,7 +15,7 @@ import { traceError, traceInfo, traceLog } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; -import { jsonRPCHeaders, jsonRPCContent, JSONRPC_UUID_HEADER, createExecutionErrorPayload } from './utils'; +import { createEOTPayload, createExecutionErrorPayload, extractJsonPayload } from './utils'; import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { @@ -35,56 +35,22 @@ export class PythonTestServer implements ITestServer, Disposable { this.server = net.createServer((socket: net.Socket) => { let buffer: Buffer = Buffer.alloc(0); // Buffer to accumulate received data socket.on('data', (data: Buffer) => { - try { - let rawData: string = data.toString(); - buffer = Buffer.concat([buffer, data]); - while (buffer.length > 0) { - const rpcHeaders = jsonRPCHeaders(buffer.toString()); - const uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); - const totalContentLength = rpcHeaders.headers.get('Content-Length'); - if (!uuid) { - traceError('On data received: Error occurred because payload UUID is undefined'); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; - } - if (!this.uuids.includes(uuid)) { - traceError('On data received: Error occurred because the payload UUID is not recognized'); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; - } - rawData = rpcHeaders.remainingRawData; - const rpcContent = jsonRPCContent(rpcHeaders.headers, rawData); - const extractedData = rpcContent.extractedJSON; - // do not send until we have the full content - if (extractedData.length === Number(totalContentLength)) { - // if the rawData includes tests then this is a discovery request - if (rawData.includes(`"tests":`)) { - this._onDiscoveryDataReceived.fire({ - uuid, - data: rpcContent.extractedJSON, - }); - // if the rawData includes result then this is a run request - } else if (rawData.includes(`"result":`)) { - this._onRunDataReceived.fire({ - uuid, - data: rpcContent.extractedJSON, - }); - } else { - traceLog( - `Error processing test server request: request is not recognized as discovery or run.`, - ); - this._onDataReceived.fire({ uuid: '', data: '' }); - return; - } - // this.uuids = this.uuids.filter((u) => u !== uuid); WHERE DOES THIS GO?? - buffer = Buffer.alloc(0); - } else { + buffer = Buffer.concat([buffer, data]); // get the new data and add it to the buffer + while (buffer.length > 0) { + try { + // try to resolve data, returned unresolved data + const remainingBuffer = this._resolveData(buffer); + if (remainingBuffer.length === buffer.length) { + // if the remaining buffer is exactly the same as the buffer before processing, + // then there is no more data to process so loop should be exited. break; } + buffer = remainingBuffer; + } catch (ex) { + traceError(`Error reading data from buffer: ${ex} observed.`); + buffer = Buffer.alloc(0); + this._onDataReceived.fire({ uuid: '', data: '' }); } - } catch (ex) { - traceError(`Error processing test server request: ${ex} observe`); - this._onDataReceived.fire({ uuid: '', data: '' }); } }); }); @@ -107,6 +73,47 @@ export class PythonTestServer implements ITestServer, Disposable { }); } + savedBuffer = ''; + + public _resolveData(buffer: Buffer): Buffer { + try { + const extractedJsonPayload = extractJsonPayload(buffer.toString(), this.uuids); + // what payload is so small it doesn't include the whole UUID think got this + if (extractedJsonPayload.uuid !== undefined && extractedJsonPayload.cleanedJsonData !== undefined) { + // if a full json was found in the buffer, fire the data received event then keep cycling with the remaining raw data. + traceInfo(`Firing data received event, ${extractedJsonPayload.cleanedJsonData}`); + this._fireDataReceived(extractedJsonPayload.uuid, extractedJsonPayload.cleanedJsonData); + } + buffer = Buffer.from(extractedJsonPayload.remainingRawData); + if (buffer.length === 0) { + // if the buffer is empty, then there is no more data to process so buffer should be cleared. + buffer = Buffer.alloc(0); + } + } catch (ex) { + traceError(`Error attempting to resolve data: ${ex}`); + this._onDataReceived.fire({ uuid: '', data: '' }); + } + return buffer; + } + + private _fireDataReceived(uuid: string, extractedJSON: string): void { + if (extractedJSON.includes(`"tests":`) || extractedJSON.includes(`"command_type": "discovery"`)) { + this._onDiscoveryDataReceived.fire({ + uuid, + data: extractedJSON, + }); + // if the rawData includes result then this is a run request + } else if (extractedJSON.includes(`"result":`) || extractedJSON.includes(`"command_type": "execution"`)) { + this._onRunDataReceived.fire({ + uuid, + data: extractedJSON, + }); + } else { + traceError(`Error processing test server request: request is not recognized as discovery or run.`); + this._onDataReceived.fire({ uuid: '', data: '' }); + } + } + public serverReady(): Promise { return this.ready; } @@ -208,10 +215,9 @@ export class PythonTestServer implements ITestServer, Disposable { traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); } const deferred = createDeferred>(); - const result = execService.execObservable(args, spawnOptions); - runInstance?.token.onCancellationRequested(() => { + traceInfo('Test run cancelled, killing unittest subprocess.'); result?.proc?.kill(); }); @@ -226,18 +232,26 @@ export class PythonTestServer implements ITestServer, Disposable { result?.proc?.on('exit', (code, signal) => { // if the child has testIds then this is a run request if (code !== 0 && testIds) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, + ); // if the child process exited with a non-zero exit code, then we need to send the error payload. this._onRunDataReceived.fire({ uuid, data: JSON.stringify(createExecutionErrorPayload(code, signal, testIds, options.cwd)), }); + // then send a EOT payload + this._onRunDataReceived.fire({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); } deferred.resolve({ stdout: '', stderr: '' }); - callback?.(); }); await deferred.promise; } } catch (ex) { + traceError(`Error while server attempting to run unittest command: ${ex}`); this.uuids = this.uuids.filter((u) => u !== uuid); this._onDataReceived.fire({ uuid, diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 610c3d76d17b..386c397b310c 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -14,6 +14,7 @@ import { } from 'vscode'; import { ITestDebugLauncher, TestDiscoveryOptions } from '../../common/types'; import { IPythonExecutionFactory } from '../../../common/process/types'; +import { Deferred } from '../../../common/utils/async'; export type TestRunInstanceOptions = TestRunOptions & { exclude?: readonly TestItem[]; @@ -191,8 +192,18 @@ export interface ITestResultResolver { runIdToVSid: Map; runIdToTestItem: Map; vsIdToRunId: Map; - resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise; - resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise; + resolveDiscovery( + payload: DiscoveredTestPayload | EOTTestPayload, + deferredTillEOT: Deferred, + token?: CancellationToken, + ): Promise; + resolveExecution( + payload: ExecutionTestPayload | EOTTestPayload, + runInstance: TestRun, + deferredTillEOT: Deferred, + ): Promise; + _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise; + _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise; } export interface ITestDiscoveryAdapter { // ** first line old method signature, second line new method signature @@ -241,6 +252,11 @@ export type DiscoveredTestPayload = { error?: string[]; }; +export type EOTTestPayload = { + commandType: 'discovery' | 'execution'; + eot: boolean; +}; + export type ExecutionTestPayload = { cwd: string; status: 'success' | 'error'; diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index dd1b51551a45..572863ecdbfe 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -9,27 +9,100 @@ import { EnableTestAdapterRewrite } from '../../../common/experiments/groups'; import { IExperimentService } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilities'; -import { DiscoveredTestItem, DiscoveredTestNode, ExecutionTestPayload, ITestResultResolver } from './types'; +import { + DiscoveredTestItem, + DiscoveredTestNode, + EOTTestPayload, + ExecutionTestPayload, + ITestResultResolver, +} from './types'; +import { Deferred, createDeferred } from '../../../common/utils/async'; export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); return `${lines.join('\r\n')}\r\n`; } -export interface IJSONRPCContent { +export interface IJSONRPCData { extractedJSON: string; remainingRawData: string; } -export interface IJSONRPCHeaders { +export interface ParsedRPCHeadersAndData { headers: Map; remainingRawData: string; } +export interface ExtractOutput { + uuid: string | undefined; + cleanedJsonData: string | undefined; + remainingRawData: string; +} + export const JSONRPC_UUID_HEADER = 'Request-uuid'; export const JSONRPC_CONTENT_LENGTH_HEADER = 'Content-Length'; export const JSONRPC_CONTENT_TYPE_HEADER = 'Content-Type'; -export function jsonRPCHeaders(rawData: string): IJSONRPCHeaders { +export function createEOTDeferred(): Deferred { + return createDeferred(); +} + +export function extractJsonPayload(rawData: string, uuids: Array): ExtractOutput { + /** + * Extracts JSON-RPC payload from the provided raw data. + * @param {string} rawData - The raw string data from which the JSON payload will be extracted. + * @param {Array} uuids - The list of UUIDs that are active. + * @returns {string} The remaining raw data after the JSON payload is extracted. + */ + + const rpcHeaders: ParsedRPCHeadersAndData = parseJsonRPCHeadersAndData(rawData); + + // verify the RPC has a UUID and that it is recognized + let uuid = rpcHeaders.headers.get(JSONRPC_UUID_HEADER); + uuid = checkUuid(uuid, uuids); + + const payloadLength = rpcHeaders.headers.get('Content-Length'); + + // separate out the data within context length of the given payload from the remaining data in the buffer + const rpcContent: IJSONRPCData = ExtractJsonRPCData(payloadLength, rpcHeaders.remainingRawData); + const cleanedJsonData = rpcContent.extractedJSON; + const { remainingRawData } = rpcContent; + + // if the given payload has the complete json, process it otherwise wait for the rest in the buffer + if (cleanedJsonData.length === Number(payloadLength)) { + // call to process this data + // remove this data from the buffer + return { uuid, cleanedJsonData, remainingRawData }; + } + // wait for the remaining + return { uuid: undefined, cleanedJsonData: undefined, remainingRawData: rawData }; +} + +export function checkUuid(uuid: string | undefined, uuids: Array): string | undefined { + if (!uuid) { + // no UUID found, this could occurred if the payload is full yet so send back without erroring + return undefined; + } + if (!uuids.includes(uuid)) { + // no UUID found, this could occurred if the payload is full yet so send back without erroring + throw new Error('On data received: Error occurred because the payload UUID is not recognized'); + } + return uuid; +} + +export function parseJsonRPCHeadersAndData(rawData: string): ParsedRPCHeadersAndData { + /** + * Parses the provided raw data to extract JSON-RPC specific headers and remaining data. + * + * This function aims to extract specific JSON-RPC headers (like UUID, content length, + * and content type) from the provided raw string data. Headers are expected to be + * delimited by newlines and the format should be "key:value". The function stops parsing + * once it encounters an empty line, and the rest of the data after this line is treated + * as the remaining raw data. + * + * @param {string} rawData - The raw string containing headers and possibly other data. + * @returns {ParsedRPCHeadersAndData} An object containing the parsed headers as a map and the + * remaining raw data after the headers. + */ const lines = rawData.split('\n'); let remainingRawData = ''; const headerMap = new Map(); @@ -51,8 +124,21 @@ export function jsonRPCHeaders(rawData: string): IJSONRPCHeaders { }; } -export function jsonRPCContent(headers: Map, rawData: string): IJSONRPCContent { - const length = parseInt(headers.get('Content-Length') ?? '0', 10); +export function ExtractJsonRPCData(payloadLength: string | undefined, rawData: string): IJSONRPCData { + /** + * Extracts JSON-RPC content based on provided headers and raw data. + * + * This function uses the `Content-Length` header from the provided headers map + * to determine how much of the rawData string represents the actual JSON content. + * After extracting the expected content, it also returns any remaining data + * that comes after the extracted content as remaining raw data. + * + * @param {string | undefined} payloadLength - The value of the `Content-Length` header. + * @param {string} rawData - The raw string data from which the JSON content will be extracted. + * + * @returns {IJSONRPCContent} An object containing the extracted JSON content and any remaining raw data. + */ + const length = parseInt(payloadLength ?? '0', 10); const data = rawData.slice(0, length); const remainingRawData = rawData.slice(length); return { @@ -212,3 +298,10 @@ export function createExecutionErrorPayload( } return etp; } + +export function createEOTPayload(executionBool: boolean): EOTTestPayload { + return { + commandType: executionBool ? 'execution' : 'discovery', + eot: true, + } as EOTTestPayload; +} diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 1550323ff8f8..0c7db5594004 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -371,6 +371,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); const dispose = token.onCancellationRequested(() => { + runInstance.appendOutput(`Run instance cancelled.\r\n`); runInstance.end(); }); diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 450e2ef1edf2..bafa91847b42 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -9,9 +9,9 @@ import { SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; +import { Deferred, createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; import { DataReceivedEvent, DiscoveredTestPayload, @@ -32,20 +32,20 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { ) {} async discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { - const settings = this.configSettings.getSettings(uri); const uuid = this.testServer.createUUID(uri.fsPath); - const { pytestArgs } = settings.testing; - traceVerbose(pytestArgs); - const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { - this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); + const deferredTillEOT: Deferred = createDeferred(); + const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived(async (e: DataReceivedEvent) => { + this.resultResolver?.resolveDiscovery(JSON.parse(e.data), deferredTillEOT); }); const disposeDataReceiver = function (testServer: ITestServer) { + traceInfo(`Disposing data receiver for ${uri.fsPath} and deleting UUID; pytest discovery.`); testServer.deleteUUID(uuid); dataReceivedDisposable.dispose(); }; try { await this.runPytestDiscovery(uri, uuid, executionFactory); } finally { + await deferredTillEOT.promise; disposeDataReceiver(this.testServer); } // this is only a placeholder to handle function overloading until rewrite is finished @@ -84,6 +84,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // delete UUID following entire discovery finishing. const deferredExec = createDeferred>(); const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + traceVerbose(`Running pytest discovery with command: ${execArgs.join(' ')}`); const result = execService?.execObservable(execArgs, spawnOptions); // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. @@ -94,7 +95,12 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { result?.proc?.stderr?.on('data', (data) => { spawnOptions.outputChannel?.append(data.toString()); }); - result?.proc?.on('exit', () => { + result?.proc?.on('exit', (code, signal) => { + if (code !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, + ); + } deferredExec.resolve({ stdout: '', stderr: '' }); deferred.resolve(); }); diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 9705374d74af..085af40375d4 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -4,8 +4,8 @@ import { TestRun, Uri } from 'vscode'; import * as path from 'path'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; -import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { Deferred, createDeferred } from '../../../common/utils/async'; +import { traceError, traceInfo } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, @@ -42,29 +42,39 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { debugLauncher?: ITestDebugLauncher, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); - traceVerbose(uri, testIds, debugBool); + const deferredTillEOT: Deferred = utils.createEOTDeferred(); const dataReceivedDisposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { - this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); + const eParsed = JSON.parse(e.data); + this.resultResolver?.resolveExecution(eParsed, runInstance, deferredTillEOT); + } else { + traceError('No run instance found, cannot resolve execution.'); } }); const disposeDataReceiver = function (testServer: ITestServer) { + traceInfo(`Disposing data receiver for ${uri.fsPath} and deleting UUID; pytest execution.`); testServer.deleteUUID(uuid); dataReceivedDisposable.dispose(); }; runInstance?.token.onCancellationRequested(() => { - disposeDataReceiver(this.testServer); + traceInfo("Test run cancelled, resolving 'till EOT' deferred."); + deferredTillEOT.resolve(); }); - await this.runTestsNew( - uri, - testIds, - uuid, - runInstance, - debugBool, - executionFactory, - debugLauncher, - disposeDataReceiver, - ); + try { + this.runTestsNew( + uri, + testIds, + uuid, + runInstance, + debugBool, + executionFactory, + debugLauncher, + deferredTillEOT, + ); + } finally { + await deferredTillEOT.promise; + disposeDataReceiver(this.testServer); + } // placeholder until after the rewrite is adopted // TODO: remove after adoption. @@ -84,19 +94,16 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { debugBool?: boolean, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, - disposeDataReceiver?: (testServer: ITestServer) => void, + deferredTillEOT?: Deferred, ): Promise { - const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); - this.configSettings.isTestExecution(); const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - const spawnOptions: SpawnOptions = { cwd, throwOnStdErr: true, @@ -149,19 +156,19 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }; traceInfo(`Running DEBUG pytest with arguments: ${testArgs.join(' ')}\r\n`); await debugLauncher!.launchDebugger(launchOptions, () => { - deferred.resolve(); - this.testServer.deleteUUID(uuid); + deferredTillEOT?.resolve(); }); } else { // combine path to run script with run args const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); const runArgs = [scriptPath, ...testArgs]; - traceInfo(`Running pytests with arguments: ${runArgs.join(' ')}\r\n`); + traceInfo(`Running pytest with arguments: ${runArgs.join(' ')}\r\n`); const deferredExec = createDeferred>(); const result = execService?.execObservable(runArgs, spawnOptions); runInstance?.token.onCancellationRequested(() => { + traceInfo('Test run cancelled, killing pytest subprocess.'); result?.proc?.kill(); }); @@ -175,17 +182,24 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }); result?.proc?.on('exit', (code, signal) => { + traceInfo('Test run finished, subprocess exited.'); // if the child has testIds then this is a run request if (code !== 0 && testIds) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, + ); // if the child process exited with a non-zero exit code, then we need to send the error payload. this.testServer.triggerRunDataReceivedEvent({ uuid, data: JSON.stringify(utils.createExecutionErrorPayload(code, signal, testIds, cwd)), }); + // then send a EOT payload + this.testServer.triggerRunDataReceivedEvent({ + uuid, + data: JSON.stringify(utils.createEOTPayload(true)), + }); } deferredExec.resolve({ stdout: '', stderr: '' }); - deferred.resolve(); - disposeDataReceiver?.(this.testServer); }); await deferredExec.promise; } diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 9820aa89626c..440df4f94dc6 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -14,6 +14,7 @@ import { TestCommandOptions, TestDiscoveryCommand, } from '../common/types'; +import { Deferred, createDeferred } from '../../../common/utils/async'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. @@ -34,7 +35,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const command = buildDiscoveryCommand(unittestArgs); const uuid = this.testServer.createUUID(uri.fsPath); - + const deferredTillEOT: Deferred = createDeferred(); const options: TestCommandOptions = { workspaceFolder: uri, command, @@ -44,7 +45,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const dataReceivedDisposable = this.testServer.onDiscoveryDataReceived((e: DataReceivedEvent) => { - this.resultResolver?.resolveDiscovery(JSON.parse(e.data)); + this.resultResolver?.resolveDiscovery(JSON.parse(e.data), deferredTillEOT); }); const disposeDataReceiver = function (testServer: ITestServer) { testServer.deleteUUID(uuid); @@ -52,8 +53,10 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; await this.callSendCommand(options, () => { - disposeDataReceiver(this.testServer); + disposeDataReceiver?.(this.testServer); }); + await deferredTillEOT.promise; + disposeDataReceiver(this.testServer); // placeholder until after the rewrite is adopted // TODO: remove after adoption. const discoveryPayload: DiscoveredTestPayload = { diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index bc5e41d19d9d..9da0872ef601 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { TestRun, Uri } from 'vscode'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; +import { Deferred, createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { DataReceivedEvent, @@ -15,7 +15,7 @@ import { TestCommandOptions, TestExecutionCommand, } from '../common/types'; -import { traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog } from '../../../logging'; import { startTestIdServer } from '../common/utils'; /** @@ -37,19 +37,30 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance?: TestRun, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); + const deferredTillEOT: Deferred = createDeferred(); const disposedDataReceived = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { - this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance); + this.resultResolver?.resolveExecution(JSON.parse(e.data), runInstance, deferredTillEOT); + } else { + traceError('No run instance found, cannot resolve execution.'); } }); const disposeDataReceiver = function (testServer: ITestServer) { + traceInfo(`Disposing data receiver for ${uri.fsPath} and deleting UUID; unittest execution.`); testServer.deleteUUID(uuid); disposedDataReceived.dispose(); }; runInstance?.token.onCancellationRequested(() => { - disposeDataReceiver(this.testServer); + traceInfo("Test run cancelled, resolving 'till EOT' deferred."); + deferredTillEOT.resolve(); }); - await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, disposeDataReceiver); + try { + await this.runTestsNew(uri, testIds, uuid, runInstance, debugBool, deferredTillEOT); + await deferredTillEOT.promise; + disposeDataReceiver(this.testServer); + } catch (error) { + traceError(`Error in running unittest tests: ${error}`); + } const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', error: '' }; return executionPayload; } @@ -60,7 +71,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uuid: string, runInstance?: TestRun, debugBool?: boolean, - disposeDataReceiver?: (testServer: ITestServer) => void, + deferredTillEOT?: Deferred, ): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -77,15 +88,12 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { testIds, outChannel: this.outputChannel, }; - - const deferred = createDeferred(); traceLog(`Running UNITTEST execution for the following test ids: ${testIds}`); const runTestIdsPort = await startTestIdServer(testIds); await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, testIds, () => { - deferred.resolve(); - disposeDataReceiver?.(this.testServer); + deferredTillEOT?.resolve(); }); // placeholder until after the rewrite is adopted // TODO: remove after adoption. diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 5af7e59f6a46..b445772d6958 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { TestRun, Uri } from 'vscode'; +import { TestController, TestRun, Uri } from 'vscode'; import * as typeMoq from 'typemoq'; import * as path from 'path'; import * as assert from 'assert'; import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; -import { ITestResultResolver, ITestServer } from '../../../client/testing/testController/common/types'; +import { ITestController, ITestResultResolver } from '../../../client/testing/testController/common/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { IPythonExecutionFactory } from '../../../client/common/process/types'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; @@ -17,16 +17,22 @@ import { traceLog } from '../../../client/logging'; import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; +import { PythonResultResolver } from '../../../client/testing/testController/common/resultResolver'; +import { TestProvider } from '../../../client/testing/types'; +import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; suite('End to End Tests: test adapters', () => { - let resultResolver: typeMoq.IMock; - let pythonTestServer: ITestServer; + let resultResolver: ITestResultResolver; + let pythonTestServer: PythonTestServer; let pythonExecFactory: IPythonExecutionFactory; let debugLauncher: ITestDebugLauncher; let configService: IConfigurationService; - let testOutputChannel: ITestOutputChannel; let serviceContainer: IServiceContainer; let workspaceUri: Uri; + let testOutputChannel: typeMoq.IMock; + let testController: TestController; + const unittestProvider: TestProvider = UNITTEST_PROVIDER; + const pytestProvider: TestProvider = PYTEST_PROVIDER; const rootPathSmallWorkspace = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', @@ -54,83 +60,97 @@ suite('End to End Tests: test adapters', () => { configService = serviceContainer.get(IConfigurationService); pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); debugLauncher = serviceContainer.get(ITestDebugLauncher); - testOutputChannel = serviceContainer.get(ITestOutputChannel); - - // create mock resultResolver object - resultResolver = typeMoq.Mock.ofType(); + testController = serviceContainer.get(ITestController); // create objects that were not injected pythonTestServer = new PythonTestServer(pythonExecFactory, debugLauncher); await pythonTestServer.serverReady(); + + testOutputChannel = typeMoq.Mock.ofType(); + testOutputChannel + .setup((x) => x.append(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('output channel - ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + testOutputChannel + .setup((x) => x.appendLine(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('output channel ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + }); + teardown(async () => { + pythonTestServer.dispose(); }); test('unittest discovery adapter small workspace', async () => { // result resolver and saved data for assertions let actualData: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + workspaceUri = Uri.parse(rootPathSmallWorkspace); + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveDiscovery ${data}`); - actualData = data; - return Promise.resolve(); - }); // set workspace to test workspace folder and set up settings - workspaceUri = Uri.parse(rootPathSmallWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; // run unittest discovery const discoveryAdapter = new UnittestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); await discoveryAdapter.discoverTests(workspaceUri).finally(() => { // verification after discovery is complete - // resultResolver.verify( - // (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - // typeMoq.Times.once(), - // ); // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); // 3. Confirm tests are found assert.ok(actualData.tests, 'Expected tests to be present'); - }); - await discoveryAdapter.discoverTests(Uri.parse(rootPathErrorWorkspace)).finally(() => { - // verification after discovery is complete - - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); - // 3. Confirm tests are found - assert.ok(actualData.tests, 'Expected tests to be present'); + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); }); }); test('unittest discovery adapter large workspace', async () => { // result resolver and saved data for assertions let actualData: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveDiscovery ${data}`); - actualData = data; - return Promise.resolve(); - }); // set settings to work for the given workspace workspaceUri = Uri.parse(rootPathLargeWorkspace); @@ -139,84 +159,89 @@ suite('End to End Tests: test adapters', () => { const discoveryAdapter = new UnittestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); await discoveryAdapter.discoverTests(workspaceUri).finally(() => { - // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); // 3. Confirm tests are found assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); }); }); test('pytest discovery adapter small workspace', async () => { // result resolver and saved data for assertions let actualData: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveDiscovery ${data}`); - actualData = data; - return Promise.resolve(); - }); // run pytest discovery const discoveryAdapter = new PytestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathSmallWorkspace); - await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(actualData.error.length, 0, "Expected no errors in 'error' field"); + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); // 3. Confirm tests are found assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); }); }); test('pytest discovery adapter large workspace', async () => { // result resolver and saved data for assertions let actualData: { - status: unknown; - error: string | any[]; - tests: unknown; + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver._resolveDiscovery = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - actualData = data; - return Promise.resolve(); - }); // run pytest discovery const discoveryAdapter = new PytestTestDiscoveryAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); // set workspace to test workspace folder @@ -224,32 +249,42 @@ suite('End to End Tests: test adapters', () => { await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveDiscovery(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(actualData.error.length, 0, "Expected no errors in 'error' field"); + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); // 3. Confirm tests are found assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); }); }); test('unittest execution adapter small workspace', async () => { // result resolver and saved data for assertions - let actualData: { - status: unknown; - error: string | any[]; - result: unknown; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - actualData = data; - return Promise.resolve(); - }); // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathSmallWorkspace); @@ -258,8 +293,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new UnittestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -273,33 +308,34 @@ suite('End to End Tests: test adapters', () => { await executionAdapter .runTests(workspaceUri, ['test_simple.SimpleClass.test_simple_unit'], false, testRun.object) .finally(() => { - // verification after execution is complete - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm tests are found - assert.ok(actualData.result, 'Expected results to be present'); + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); test('unittest execution adapter large workspace', async () => { // result resolver and saved data for assertions - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - traceLog(`resolveExecution ${data}`); - // do the following asserts for each time resolveExecution is called, should be called once per test. - // 1. Check the status, can be subtest success or failure - assert( - data.status === 'subtest-success' || data.status === 'subtest-failure', - "Expected status to be 'subtest-success' or 'subtest-failure'", + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + const validStatuses = ['subtest-success', 'subtest-failure']; + assert.ok( + validStatuses.includes(payload.status), + `Expected status to be one of ${validStatuses.join(', ')}, but instead status is ${payload.status}`, ); - // 2. Confirm tests are found - assert.ok(data.result, 'Expected results to be present'); - return Promise.resolve(); - }); + assert.ok(payload.result, 'Expected results to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathLargeWorkspace); @@ -309,8 +345,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new UnittestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -323,28 +359,35 @@ suite('End to End Tests: test adapters', () => { ); await executionAdapter .runTests(workspaceUri, ['test_parameterized_subtest.NumbersTest.test_even'], false, testRun.object) - .finally(() => { - // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.atLeastOnce(), - ); + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); test('pytest execution adapter small workspace', async () => { // result resolver and saved data for assertions - let actualData: { - status: unknown; - error: string | any[]; - result: unknown; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); }; - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - actualData = data; - return Promise.resolve(); - }); - // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathSmallWorkspace); @@ -352,8 +395,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new PytestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -372,40 +415,42 @@ suite('End to End Tests: test adapters', () => { testRun.object, pythonExecFactory, ) - .finally(() => { - // verification after discovery is complete - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.once(), - ); - // 1. Check the status is "success" - assert.strictEqual(actualData.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(actualData.error, null, "Expected no errors in 'error' field"); - // 3. Confirm tests are found - assert.ok(actualData.result, 'Expected results to be present'); + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); test('pytest execution adapter large workspace', async () => { - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - // do the following asserts for each time resolveExecution is called, should be called once per test. - // 1. Check the status is "success" - assert.strictEqual(data.status, 'success', "Expected status to be 'success'"); - // 2. Confirm no errors - assert.strictEqual(data.error, null, "Expected no errors in 'error' field"); - // 3. Confirm tests are found - assert.ok(data.result, 'Expected results to be present'); - return Promise.resolve(); - }); + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathLargeWorkspace); // generate list of test_ids const testIds: string[] = []; - for (let i = 0; i < 200; i = i + 1) { + for (let i = 0; i < 2000; i = i + 1) { const testId = `${rootPathLargeWorkspace}/test_parameterized_subtest.py::test_odd_even[${i}]`; testIds.push(testId); } @@ -414,8 +459,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new PytestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -426,35 +471,51 @@ suite('End to End Tests: test adapters', () => { onCancellationRequested: () => undefined, } as any), ); - await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).finally(() => { - // resolve execution should be called 200 times since there are 200 tests run. - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.exactly(200), - ); + await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); test('unittest execution adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + console.log(`unittest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); + } else { + assert.ok(data.error, "Expected errors in 'error' field"); + } + } else { + const indexOfTest = JSON.stringify(data.result).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + assert.ok(data.result, 'Expected results to be present'); + // make sure the testID is found in the results + const indexOfTest = JSON.stringify(data).search('test_seg_fault.TestSegmentationFault.test_segfault'); + assert.notDeepEqual(indexOfTest, -1, 'Expected testId to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + const testId = `test_seg_fault.TestSegmentationFault.test_segfault`; const testIds: string[] = [testId]; - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - // do the following asserts for each time resolveExecution is called, should be called once per test. - // 1. Check the status is "success" - assert.strictEqual(data.status, 'error', "Expected status to be 'error'"); - // 2. Confirm no errors - assert.ok(data.error, "Expected errors in 'error' field"); - // 3. Confirm tests are found - assert.ok(data.result, 'Expected results to be present'); - // 4. make sure the testID is found in the results - assert.notDeepEqual( - JSON.stringify(data).search('test_seg_fault.TestSegmentationFault.test_segfault'), - -1, - 'Expected testId to be present', - ); - return Promise.resolve(); - }); // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathErrorWorkspace); @@ -463,8 +524,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new UnittestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -476,33 +537,45 @@ suite('End to End Tests: test adapters', () => { } as any), ); await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object).finally(() => { - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.exactly(1), - ); + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); test('pytest execution adapter seg fault error handling', async () => { - const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; - const testIds: string[] = [testId]; - resultResolver - .setup((x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((data) => { - // do the following asserts for each time resolveExecution is called, should be called once per test. - // 1. Check the status is "success" - assert.strictEqual(data.status, 'error', "Expected status to be 'error'"); - // 2. Confirm no errors - assert.ok(data.error, "Expected errors in 'error' field"); - // 3. Confirm tests are found + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + console.log(`pytest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); + callCount = callCount + 1; + try { + if (data.status === 'error') { + assert.ok(data.error, "Expected errors in 'error' field"); + } else { + const indexOfTest = JSON.stringify(data.result).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } assert.ok(data.result, 'Expected results to be present'); - // 4. make sure the testID is found in the results - assert.notDeepEqual( - JSON.stringify(data).search('test_seg_fault.py::TestSegmentationFault::test_segfault'), - -1, - 'Expected testId to be present', + // make sure the testID is found in the results + const indexOfTest = JSON.stringify(data).search( + 'test_seg_fault.py::TestSegmentationFault::test_segfault', ); - return Promise.resolve(); - }); + assert.notDeepEqual(indexOfTest, -1, 'Expected testId to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + + const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; + const testIds: string[] = [testId]; // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathErrorWorkspace); @@ -511,8 +584,8 @@ suite('End to End Tests: test adapters', () => { const executionAdapter = new PytestTestExecutionAdapter( pythonTestServer, configService, - testOutputChannel, - resultResolver.object, + testOutputChannel.object, + resultResolver, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -524,10 +597,8 @@ suite('End to End Tests: test adapters', () => { } as any), ); await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).finally(() => { - resultResolver.verify( - (x) => x.resolveExecution(typeMoq.It.isAny(), typeMoq.It.isAny()), - typeMoq.Times.exactly(1), - ); + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); }); }); }); diff --git a/src/test/testing/common/testingPayloadsEot.test.ts b/src/test/testing/common/testingPayloadsEot.test.ts new file mode 100644 index 000000000000..227ad5fa1697 --- /dev/null +++ b/src/test/testing/common/testingPayloadsEot.test.ts @@ -0,0 +1,210 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { TestController, TestRun, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as path from 'path'; +import * as assert from 'assert'; +import * as net from 'net'; +import { Observable } from 'rxjs'; +import * as crypto from 'crypto'; +// import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; +import * as sinon from 'sinon'; +import { ITestController, ITestResultResolver } from '../../../client/testing/testController/common/types'; +import { PythonTestServer } from '../../../client/testing/testController/common/server'; +import { IPythonExecutionFactory, IPythonExecutionService, Output } from '../../../client/common/process/types'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { initialize } from '../../initialize'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { PythonResultResolver } from '../../../client/testing/testController/common/resultResolver'; +import { PYTEST_PROVIDER } from '../../../client/testing/common/constants'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; +import { + PAYLOAD_SINGLE_CHUNK, + PAYLOAD_MULTI_CHUNK, + PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY, + DataWithPayloadChunks, + PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY, +} from '../testController/payloadTestCases'; +import { traceLog } from '../../../client/logging'; + +const FAKE_UUID = 'fake-u-u-i-d'; +export interface TestCase { + name: string; + value: DataWithPayloadChunks; +} + +const testCases: Array = [ + { + name: 'single payload single chunk', + value: PAYLOAD_SINGLE_CHUNK(FAKE_UUID), + }, + { + name: 'multiple payloads per buffer chunk', + value: PAYLOAD_MULTI_CHUNK(FAKE_UUID), + }, + { + name: 'single payload across multiple buffer chunks', + value: PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(FAKE_UUID), + }, + { + name: 'two chunks, payload split and two payloads in a chunk', + value: PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY(FAKE_UUID), + }, +]; + +suite('EOT tests', () => { + let resultResolver: ITestResultResolver; + let pythonTestServer: PythonTestServer; + let debugLauncher: ITestDebugLauncher; + let configService: IConfigurationService; + let serviceContainer: IServiceContainer; + let workspaceUri: Uri; + let testOutputChannel: typeMoq.IMock; + let testController: TestController; + let stubExecutionFactory: typeMoq.IMock; + let client: net.Socket; + const sandbox = sinon.createSandbox(); + // const unittestProvider: TestProvider = UNITTEST_PROVIDER; + // const pytestProvider: TestProvider = PYTEST_PROVIDER; + const rootPathSmallWorkspace = path.join('src'); + suiteSetup(async () => { + serviceContainer = (await initialize()).serviceContainer; + }); + + setup(async () => { + // create objects that were injected + configService = serviceContainer.get(IConfigurationService); + debugLauncher = serviceContainer.get(ITestDebugLauncher); + testController = serviceContainer.get(ITestController); + + // create client to act as python server which sends testing result response + client = new net.Socket(); + client.on('error', (error) => { + traceLog('Socket connection error:', error); + }); + + const mockProc = new MockChildProcess('', ['']); + const output2 = new Observable>(() => { + /* no op */ + }); + + // stub out execution service and factory so mock data is returned from client. + const stubExecutionService = ({ + execObservable: () => { + client.connect(pythonTestServer.getPort()); + return { + proc: mockProc, + out: output2, + dispose: () => { + /* no-body */ + }, + }; + }, + } as unknown) as IPythonExecutionService; + + stubExecutionFactory = typeMoq.Mock.ofType(); + stubExecutionFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(stubExecutionService)); + + // stub create UUID + + const v4Stub = sandbox.stub(crypto, 'randomUUID'); + v4Stub.returns(FAKE_UUID); + + // create python test server + pythonTestServer = new PythonTestServer(stubExecutionFactory.object, debugLauncher); + await pythonTestServer.serverReady(); + // handles output from client + testOutputChannel = typeMoq.Mock.ofType(); + testOutputChannel + .setup((x) => x.append(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('out - ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + testOutputChannel + .setup((x) => x.appendLine(typeMoq.It.isAny())) + .callback((appendVal: any) => { + traceLog('outL - ', appendVal.toString()); + }) + .returns(() => { + // Whatever you need to return + }); + }); + teardown(async () => { + pythonTestServer.dispose(); + sandbox.restore(); + }); + testCases.forEach((testCase) => { + test(`Testing Payloads: ${testCase.name}`, async () => { + let actualCollectedResult = ''; + client.on('connect', async () => { + traceLog('socket connected, sending stubbed data'); + // payload is a string array, each string represents one line written to the buffer + const { payloadArray } = testCase.value; + for (let i = 0; i < payloadArray.length; i = i + 1) { + await (async (clientSub, payloadSub) => { + if (!clientSub.write(payloadSub)) { + // If write returns false, wait for the 'drain' event before proceeding + await new Promise((resolve) => clientSub.once('drain', resolve)); + } + })(client, payloadArray[i]); + } + client.end(); + }); + + resultResolver = new PythonResultResolver(testController, PYTEST_PROVIDER, workspaceUri); + resultResolver._resolveExecution = async (payload, _token?) => { + // the payloads that get to the _resolveExecution are all data and should be successful. + assert.strictEqual(payload.status, 'success', "Expected status to be 'success'"); + assert.ok(payload.result, 'Expected results to be present'); + actualCollectedResult = actualCollectedResult + JSON.stringify(payload.result); + return Promise.resolve(); + }; + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests( + workspaceUri, + [`${rootPathSmallWorkspace}/test_simple.py::test_a`], + false, + testRun.object, + stubExecutionFactory.object, + ) + .then(() => { + assert.strictEqual( + testCase.value.data, + actualCollectedResult, + "Expected collected result to match 'data'", + ); + // nervous about this not testing race conditions correctly + // client.end(); + // verify that the _resolveExecution was called once per test + // assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + }); + }); + }); +}); diff --git a/src/test/testing/testController/payloadTestCases.ts b/src/test/testing/testController/payloadTestCases.ts new file mode 100644 index 000000000000..5ddcc0edecf9 --- /dev/null +++ b/src/test/testing/testController/payloadTestCases.ts @@ -0,0 +1,149 @@ +export interface DataWithPayloadChunks { + payloadArray: string[]; + data: string; +} + +const EOT_PAYLOAD = `Content-Length: 42 +Content-Type: application/json +Request-uuid: fake-u-u-i-d + +{"command_type": "execution", "eot": true}`; + +const SINGLE_UNITTEST_SUBTEST = { + cwd: '/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace', + status: 'success', + result: { + 'test_parameterized_subtest.NumbersTest.test_even (i=0)': { + test: 'test_parameterized_subtest.NumbersTest.test_even', + outcome: 'success', + message: 'None', + traceback: null, + subtest: 'test_parameterized_subtest.NumbersTest.test_even (i=0)', + }, + }, +}; + +const SINGLE_PYTEST_PAYLOAD = { + cwd: 'path/to', + status: 'success', + result: { + 'path/to/file.py::test_funct': { + test: 'path/to/file.py::test_funct', + outcome: 'success', + message: 'None', + traceback: null, + subtest: 'path/to/file.py::test_funct', + }, + }, +}; + +const SINGLE_PYTEST_PAYLOAD_TWO = { + cwd: 'path/to/second', + status: 'success', + result: { + 'path/to/workspace/parametrize_tests.py::test_adding[3+5-8]': { + test: 'path/to/workspace/parametrize_tests.py::test_adding[3+5-8]', + outcome: 'success', + message: 'None', + traceback: null, + }, + }, +}; + +export function createPayload(uuid: string, data: unknown): string { + return `Content-Length: ${JSON.stringify(data).length} +Content-Type: application/json +Request-uuid: ${uuid} + +${JSON.stringify(data)}`; +} + +export function PAYLOAD_SINGLE_CHUNK(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + + return { + payloadArray: [payload, EOT_PAYLOAD], + data: JSON.stringify(SINGLE_UNITTEST_SUBTEST.result), + }; +} + +// more than one payload (item with header) per chunk sent +// payload has 3 SINGLE_UNITTEST_SUBTEST +export function PAYLOAD_MULTI_CHUNK(uuid: string): DataWithPayloadChunks { + let payload = ''; + let result = ''; + for (let i = 0; i < 3; i = i + 1) { + payload += createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + result += JSON.stringify(SINGLE_UNITTEST_SUBTEST.result); + } + return { + payloadArray: [payload, EOT_PAYLOAD], + data: result, + }; +} + +// single payload divided by an arbitrary character and split across payloads +export function PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD); + // payload length is know to be >200 + const splitPayload: Array = [ + payload.substring(0, 50), + payload.substring(50, 100), + payload.substring(100, 150), + payload.substring(150), + ]; + const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result); + splitPayload.push(EOT_PAYLOAD); + return { + payloadArray: splitPayload, + data: finalResult, + }; +} + +// here a payload is split across the buffer chunks and there are multiple payloads in a single buffer chunk +export function PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY(uuid: string): DataWithPayloadChunks { + // payload1 length is know to be >200 + const payload1 = createPayload(uuid, SINGLE_PYTEST_PAYLOAD); + const payload2 = createPayload(uuid, SINGLE_PYTEST_PAYLOAD_TWO); + + // chunk 1 is 50 char of payload1, chunk 2 is 50-end of payload1 and all of payload2 + const splitPayload: Array = [payload1.substring(0, 100), payload1.substring(100).concat(payload2)]; + const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result).concat( + JSON.stringify(SINGLE_PYTEST_PAYLOAD_TWO.result), + ); + + splitPayload.push(EOT_PAYLOAD); + return { + payloadArray: splitPayload, + data: finalResult, + }; +} + +export function PAYLOAD_SPLIT_MULTI_CHUNK_RAN_ORDER_ARRAY(uuid: string): Array { + return [ + `Content-Length: 411 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-success", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=0)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-success", "message": "None", "traceback": null, "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=0)"}}} + +Content-Length: 411 +Content-Type: application/json +Request-uuid: 9${uuid} + +{"cwd": "/home/runner/work/vscode-`, + `python/vscode-python/path with`, + ` spaces/src" + +Content-Length: 959 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-failure", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=1)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-failure", "message": "(, AssertionError('1 != 0'), )", "traceback": " File \"/opt/hostedtoolcache/Python/3.11.4/x64/lib/python3.11/unittest/case.py\", line 57, in testPartExecutor\n yield\n File \"/opt/hostedtoolcache/Python/3.11.4/x64/lib/python3.11/unittest/case.py\", line 538, in subTest\n yield\n File \"/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py\", line 16, in test_even\n self.assertEqual(i % 2, 0)\nAssertionError: 1 != 0\n", "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=1)"}}} +Content-Length: 411 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-success", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=2)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-success", "message": "None", "traceback": null, "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=2)"}}}`, + ]; +} diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 43b763f56e6c..9cc428ab0a4c 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -21,6 +21,7 @@ import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/co import * as util from '../../../../client/testing/testController/common/utils'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { traceInfo } from '../../../../client/logging'; suite('pytest test execution adapter', () => { let testServer: typeMoq.IMock; @@ -33,7 +34,7 @@ suite('pytest test execution adapter', () => { (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; let myTestPath: string; let mockProc: MockChildProcess; - let utilsStub: sinon.SinonStub; + let utilsStartServerStub: sinon.SinonStub; setup(() => { testServer = typeMoq.Mock.ofType(); testServer.setup((t) => t.getPort()).returns(() => 12345); @@ -51,6 +52,8 @@ suite('pytest test execution adapter', () => { isTestExecution: () => false, } as unknown) as IConfigurationService; + // mock out the result resolver + // set up exec service with child process mockProc = new MockChildProcess('', ['']); const output = new Observable>(() => { @@ -67,7 +70,7 @@ suite('pytest test execution adapter', () => { }, })); execFactory = typeMoq.Mock.ofType(); - utilsStub = sinon.stub(util, 'startTestIdServer'); + utilsStartServerStub = sinon.stub(util, 'startTestIdServer'); debugLauncher = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) @@ -79,13 +82,6 @@ suite('pytest test execution adapter', () => { deferred.resolve(); return Promise.resolve({ stdout: '{}' }); }); - debugLauncher - .setup((d) => d.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => { - deferred.resolve(); - return Promise.resolve(); - }); - execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); @@ -104,7 +100,7 @@ suite('pytest test execution adapter', () => { deferred2.resolve(); return Promise.resolve(execService.object); }); - utilsStub.callsFake(() => { + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); @@ -131,7 +127,7 @@ suite('pytest test execution adapter', () => { mockProc.trigger('close'); // assert - sinon.assert.calledWithExactly(utilsStub, testIds); + sinon.assert.calledWithExactly(utilsStartServerStub, testIds); }); test('pytest execution called with correct args', async () => { const deferred2 = createDeferred(); @@ -143,7 +139,7 @@ suite('pytest test execution adapter', () => { deferred2.resolve(); return Promise.resolve(execService.object); }); - utilsStub.callsFake(() => { + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); @@ -175,7 +171,6 @@ suite('pytest test execution adapter', () => { TEST_UUID: 'uuid123', TEST_PORT: '12345', }; - // execService.verify((x) => x.exec(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); execService.verify( (x) => x.execObservable( @@ -203,7 +198,7 @@ suite('pytest test execution adapter', () => { deferred2.resolve(); return Promise.resolve(execService.object); }); - utilsStub.callsFake(() => { + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); @@ -262,12 +257,28 @@ suite('pytest test execution adapter', () => { }); test('Debug launched correctly for pytest', async () => { const deferred3 = createDeferred(); - utilsStub.callsFake(() => { + const deferredEOT = createDeferred(); + utilsStartServerStub.callsFake(() => { deferred3.resolve(); return Promise.resolve(54321); }); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async () => { + traceInfo('stubs launch debugger'); + deferredEOT.resolve(); + }); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); + utilsCreateEOTStub.callsFake(() => deferredEOT); const testRun = typeMoq.Mock.ofType(); - testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); const uri = Uri.file(myTestPath); const uuid = 'uuid123'; testServer @@ -298,5 +309,6 @@ suite('pytest test execution adapter', () => { ), typeMoq.Times.once(), ); + testServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); }); }); diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts index 694fdacb8049..2078c72e8cf6 100644 --- a/src/test/testing/testController/resultResolver.unit.test.ts +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -14,6 +14,8 @@ import { import * as testItemUtilities from '../../../client/testing/testController/common/testItemUtilities'; import * as ResultResolver from '../../../client/testing/testController/common/resultResolver'; import * as util from '../../../client/testing/testController/common/utils'; +import { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { traceLog } from '../../../client/logging'; suite('Result Resolver tests', () => { suite('Test discovery', () => { @@ -87,7 +89,8 @@ suite('Result Resolver tests', () => { const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); // call resolve discovery - resultResolver.resolveDiscovery(payload, cancelationToken); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveDiscovery(payload, deferredTillEOT, cancelationToken); // assert the stub functions were called with the correct parameters @@ -126,7 +129,8 @@ suite('Result Resolver tests', () => { const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); // call resolve discovery - resultResolver.resolveDiscovery(payload); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveDiscovery(payload, deferredTillEOT, cancelationToken); // assert the stub functions were called with the correct parameters @@ -171,7 +175,8 @@ suite('Result Resolver tests', () => { // stub out functionality of populateTestTreeStub which is called in resolveDiscovery const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); // call resolve discovery - resultResolver.resolveDiscovery(payload, cancelationToken); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveDiscovery(payload, deferredTillEOT, cancelationToken); // assert the stub functions were called with the correct parameters @@ -286,7 +291,7 @@ suite('Result Resolver tests', () => { .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny())) .callback((id: string) => { generatedId = id; - console.log('createTestItem function called with id:', id); + traceLog('createTestItem function called with id:', id); }) .returns(() => ({ id: 'id_this', label: 'label_this', uri: workspaceUri } as TestItem)); @@ -307,7 +312,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item assert.ok(generatedId); @@ -347,7 +353,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item runInstance.verify((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); @@ -386,7 +393,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.once()); @@ -425,7 +433,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item runInstance.verify((r) => r.errored(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); @@ -464,7 +473,8 @@ suite('Result Resolver tests', () => { }; // call resolveExecution - resultResolver.resolveExecution(successPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(successPayload, runInstance.object, deferredTillEOT); // verify that the passed function was called for the single test item runInstance.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.once()); @@ -485,7 +495,8 @@ suite('Result Resolver tests', () => { error: 'error', }; - resultResolver.resolveExecution(errorPayload, runInstance.object); + const deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveExecution(errorPayload, runInstance.object, deferredTillEOT); // verify that none of these functions are called diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 53c2b72e40f7..92a9a1135f55 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -6,58 +6,66 @@ import * as assert from 'assert'; import * as net from 'net'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; -import { OutputChannel, Uri } from 'vscode'; import { Observable } from 'rxjs'; import * as typeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; import { IPythonExecutionFactory, IPythonExecutionService, + ObservableExecutionResult, Output, - SpawnOptions, } from '../../../client/common/process/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; import { Deferred, createDeferred } from '../../../client/common/utils/async'; import { MockChildProcess } from '../../mocks/mockChildProcess'; - -suite('Python Test Server', () => { - const fakeUuid = 'fake-uuid'; - - let stubExecutionFactory: IPythonExecutionFactory; - let stubExecutionService: IPythonExecutionService; +import { + PAYLOAD_MULTI_CHUNK, + PAYLOAD_SINGLE_CHUNK, + PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY, + DataWithPayloadChunks, +} from './payloadTestCases'; +import { traceLog } from '../../../client/logging'; + +const testCases = [ + { + val: () => PAYLOAD_SINGLE_CHUNK('fake-uuid'), + }, + { + val: () => PAYLOAD_MULTI_CHUNK('fake-uuid'), + }, + { + val: () => PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY('fake-uuid'), + }, +]; + +suite('Python Test Server, DataWithPayloadChunks', () => { + const FAKE_UUID = 'fake-uuid'; let server: PythonTestServer; - let sandbox: sinon.SinonSandbox; let v4Stub: sinon.SinonStub; let debugLauncher: ITestDebugLauncher; let mockProc: MockChildProcess; let execService: typeMoq.IMock; let deferred: Deferred; - let execFactory = typeMoq.Mock.ofType(); - - setup(() => { - sandbox = sinon.createSandbox(); - v4Stub = sandbox.stub(crypto, 'randomUUID'); + const sandbox = sinon.createSandbox(); - v4Stub.returns(fakeUuid); - stubExecutionService = ({ - execObservable: () => Promise.resolve({ stdout: '', stderr: '' }), - } as unknown) as IPythonExecutionService; + setup(async () => { + // set up test command options - stubExecutionFactory = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService), - } as unknown) as IPythonExecutionFactory; + v4Stub = sandbox.stub(crypto, 'randomUUID'); + v4Stub.returns(FAKE_UUID); // set up exec service with child process mockProc = new MockChildProcess('', ['']); execService = typeMoq.Mock.ofType(); - const output = new Observable>(() => { + const outputObservable = new Observable>(() => { /* no op */ }); execService .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => ({ proc: mockProc, - out: output, + out: outputObservable, dispose: () => { /* no-body */ }, @@ -70,53 +78,153 @@ suite('Python Test Server', () => { server.dispose(); }); + testCases.forEach((testCase) => { + test(`run correctly`, async () => { + const testCaseDataObj: DataWithPayloadChunks = testCase.val(); + let eventData = ''; + const client = new net.Socket(); + + deferred = createDeferred(); + mockProc = new MockChildProcess('', ['']); + const output2 = new Observable>(() => { + /* no op */ + }); + const stubExecutionService2 = ({ + execObservable: () => { + client.connect(server.getPort()); + return { + proc: mockProc, + out: output2, + dispose: () => { + /* no-body */ + }, + }; + }, + } as unknown) as IPythonExecutionService; + + const stubExecutionFactory2 = ({ + createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), + } as unknown) as IPythonExecutionFactory; + server = new PythonTestServer(stubExecutionFactory2, debugLauncher); + const uuid = server.createUUID(); + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid, + }; + + const dataWithPayloadChunks = testCaseDataObj; + + await server.serverReady(); + + server.onRunDataReceived(({ data }) => { + try { + const resultData = JSON.parse(data).result; + eventData = eventData + JSON.stringify(resultData); + } catch (e) { + assert(false, 'Error parsing data'); + } + deferred.resolve(); + }); + client.on('connect', () => { + traceLog('Socket connected, local port:', client.localPort); + // since this test is a single payload as a single chunk there should be a single line in the payload. + for (const line of dataWithPayloadChunks.payloadArray) { + client.write(line); + } + client.end(); + }); + client.on('error', (error) => { + traceLog('Socket connection error:', error); + }); + + server.sendCommand(options); + await deferred.promise; + const expectedResult = dataWithPayloadChunks.data; + assert.deepStrictEqual(eventData, expectedResult); + }); + }); +}); + +suite('Python Test Server, Send command etc', () => { + const FAKE_UUID = 'fake-uuid'; + let server: PythonTestServer; + let v4Stub: sinon.SinonStub; + let debugLauncher: ITestDebugLauncher; + let mockProc: MockChildProcess; + let execService: typeMoq.IMock; + let deferred: Deferred; + const sandbox = sinon.createSandbox(); + + setup(async () => { + // set up test command options + + v4Stub = sandbox.stub(crypto, 'randomUUID'); + v4Stub.returns(FAKE_UUID); + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + execService = typeMoq.Mock.ofType(); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + }); + + teardown(() => { + sandbox.restore(); + server.dispose(); + }); test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { - const options = { - command: { - script: 'myscript', - args: ['-foo', 'foo'], - }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - const expectedSpawnOptions = { - cwd: '/foo/bar', - outputChannel: undefined, - token: undefined, - throwOnStdErr: true, - extraVariables: { - PYTHONPATH: '/foo/bar', - RUN_TEST_IDS_PORT: '56789', - }, - } as SpawnOptions; const deferred2 = createDeferred(); - execFactory = typeMoq.Mock.ofType(); + const RUN_TEST_IDS_PORT_CONST = '5678'; + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((_args, options2) => { + try { + assert.strictEqual( + options2.extraVariables.PYTHONPATH, + '/foo/bar', + 'Expect python path to exist as extra variable and be set correctly', + ); + assert.strictEqual( + options2.extraVariables.RUN_TEST_IDS_PORT, + RUN_TEST_IDS_PORT_CONST, + 'Expect test id port to be in extra variables and set correctly', + ); + } catch (e) { + assert(false, 'Error parsing data, extra variables do not match'); + } + return typeMoq.Mock.ofType>().object; + }); + const execFactory = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) .returns(() => { deferred2.resolve(); return Promise.resolve(execService.object); }); - server = new PythonTestServer(execFactory.object, debugLauncher); await server.serverReady(); - - server.sendCommand(options, '56789'); + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: FAKE_UUID, + }; + server.sendCommand(options, RUN_TEST_IDS_PORT_CONST); // add in await and trigger await deferred2.promise; mockProc.trigger('close'); const port = server.getPort(); - const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo']; - execService.verify((x) => x.execObservable(expectedArgs, expectedSpawnOptions), typeMoq.Times.once()); + const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', FAKE_UUID, '-foo', 'foo']; + execService.verify((x) => x.execObservable(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); }); test('sendCommand should write to an output channel if it is provided as an option', async () => { - const output: string[] = []; + const output2: string[] = []; const outChannel = { appendLine: (str: string) => { - output.push(str); + output2.push(str); }, } as OutputChannel; const options = { @@ -126,11 +234,11 @@ suite('Python Test Server', () => { }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', - uuid: fakeUuid, + uuid: FAKE_UUID, outChannel, }; deferred = createDeferred(); - execFactory = typeMoq.Mock.ofType(); + const execFactory = typeMoq.Mock.ofType(); execFactory .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) .returns(() => { @@ -147,329 +255,49 @@ suite('Python Test Server', () => { mockProc.trigger('close'); const port = server.getPort(); - const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', fakeUuid, '-foo', 'foo'].join(' '); + const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', FAKE_UUID, '-foo', 'foo'].join(' '); - assert.deepStrictEqual(output, [expected]); + assert.deepStrictEqual(output2, [expected]); }); test('If script execution fails during sendCommand, an onDataReceived event should be fired with the "error" status', async () => { let eventData: { status: string; errors: string[] } | undefined; - stubExecutionService = ({ - execObservable: () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + const stubExecutionService = typeMoq.Mock.ofType(); + stubExecutionService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + stubExecutionService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred3.resolve(); throw new Error('Failed to execute'); - }, - } as unknown) as IPythonExecutionService; + }); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, workspaceFolder: Uri.file('/foo/bar'), cwd: '/foo/bar', - uuid: fakeUuid, + uuid: FAKE_UUID, }; + const stubExecutionFactory = typeMoq.Mock.ofType(); + stubExecutionFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(stubExecutionService.object); + }); - server = new PythonTestServer(stubExecutionFactory, debugLauncher); + server = new PythonTestServer(stubExecutionFactory.object, debugLauncher); await server.serverReady(); server.onDataReceived(({ data }) => { eventData = JSON.parse(data); }); - await server.sendCommand(options); - + server.sendCommand(options); + await deferred2.promise; + await deferred3.promise; assert.notEqual(eventData, undefined); assert.deepStrictEqual(eventData?.status, 'error'); assert.deepStrictEqual(eventData?.errors, ['Failed to execute']); }); - - test('If the server receives malformed data, it should display a log message, and not fire an event', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - - deferred = createDeferred(); - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('malformed data'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - // add in await and trigger - await deferred.promise; - mockProc.trigger('close'); - - assert.deepStrictEqual(eventData, ''); - }); - - test('If the server doesnt recognize the UUID it should ignore it', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('{"Request-uuid": "unknown-uuid"}'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, ''); - }); - - // required to have "tests" or "results" - // the heading length not being equal and yes being equal - // multiple payloads - test('Error if payload does not have a content length header', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - server.onDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write('{"not content length": "5"}'); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, ''); - }); - - const testData = [ - { - testName: 'fires discovery correctly on test payload', - payload: `Content-Length: 52 -Content-Type: application/json -Request-uuid: UUID_HERE - -{"cwd": "path", "status": "success", "tests": "xyz"}`, - expectedResult: '{"cwd": "path", "status": "success", "tests": "xyz"}', - }, - // Add more test data as needed - ]; - - testData.forEach(({ testName, payload, expectedResult }) => { - test(`test: ${testName}`, async () => { - // Your test logic here - let eventData: string | undefined; - const client = new net.Socket(); - - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - await server.serverReady(); - const uuid = server.createUUID(); - payload = payload.replace('UUID_HERE', uuid); - server.onDiscoveryDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write(payload); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - assert.deepStrictEqual(eventData, expectedResult); - }); - }); - - test('Calls run resolver if the result header is in the payload', async () => { - let eventData: string | undefined; - const client = new net.Socket(); - - deferred = createDeferred(); - mockProc = new MockChildProcess('', ['']); - const output = new Observable>(() => { - /* no op */ - }); - const stubExecutionService2 = ({ - execObservable: () => { - client.connect(server.getPort()); - return { - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - }; - }, - } as unknown) as IPythonExecutionService; - - const stubExecutionFactory2 = ({ - createActivatedEnvironment: () => Promise.resolve(stubExecutionService2), - } as unknown) as IPythonExecutionFactory; - server = new PythonTestServer(stubExecutionFactory2, debugLauncher); - const options = { - command: { script: 'myscript', args: ['-foo', 'foo'] }, - workspaceFolder: Uri.file('/foo/bar'), - cwd: '/foo/bar', - uuid: fakeUuid, - }; - - await server.serverReady(); - const uuid = server.createUUID(); - server.onRunDataReceived(({ data }) => { - eventData = data; - deferred.resolve(); - }); - - const payload = `Content-Length: 87 -Content-Type: application/json -Request-uuid: ${uuid} - -{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}`; - - client.on('connect', () => { - console.log('Socket connected, local port:', client.localPort); - client.write(payload); - client.end(); - }); - client.on('error', (error) => { - console.log('Socket connection error:', error); - }); - - server.sendCommand(options); - await deferred.promise; - const expectedResult = - '{"cwd": "path", "status": "success", "result": "xyz", "not_found": null, "error": null}'; - assert.deepStrictEqual(eventData, expectedResult); - }); }); diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts new file mode 100644 index 000000000000..2aaffdda41df --- /dev/null +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -0,0 +1,350 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { CancellationTokenSource, TestRun, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Observable } from 'rxjs'; +import { IPythonExecutionFactory, IPythonExecutionService, Output } from '../../../client/common/process/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; +import { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { ITestServer } from '../../../client/testing/testController/common/types'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; +import * as util from '../../../client/testing/testController/common/utils'; + +suite('Execution Flow Run Adapters', () => { + let testServer: typeMoq.IMock; + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType(); + let adapter: PytestTestExecutionAdapter; + let execService: typeMoq.IMock; + let deferred: Deferred; + let debugLauncher: typeMoq.IMock; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let mockProc: MockChildProcess; + let utilsStartServerStub: sinon.SinonStub; + + setup(() => { + testServer = typeMoq.Mock.ofType(); + testServer.setup((t) => t.getPort()).returns(() => 12345); + testServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'], unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + + // mock out the result resolver + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable>(() => { + /* no op */ + }); + execService = typeMoq.Mock.ofType(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + })); + execFactory = typeMoq.Mock.ofType(); + utilsStartServerStub = sinon.stub(util, 'startTestIdServer'); + debugLauncher = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve({ stdout: '{}' }); + }); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + }); + teardown(() => { + sinon.restore(); + }); + test('PYTEST cancelation token called mid-run resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + execServiceMock + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + cancellationToken.cancel(); + return { + proc: mockProc, + out: typeMoq.Mock.ofType>>().object, + dispose: () => { + /* no-body */ + }, + }; + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token + const deferredEOT = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); + utilsCreateEOTStub.callsFake(() => deferredEOT); + // set up test server + testServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + adapter = new PytestTestExecutionAdapter( + testServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await adapter.runTests( + Uri.file(myTestPath), + [], + false, + testRunMock.object, + execFactoryMock.object, + debugLauncher.object, + ); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + testServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); + test('PYTEST cancelation token called mid-debug resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async () => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token + const deferredEOT = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); + utilsCreateEOTStub.callsFake(() => deferredEOT); + // set up test server + testServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + adapter = new PytestTestExecutionAdapter( + testServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await adapter.runTests( + Uri.file(myTestPath), + [], + true, + testRunMock.object, + execFactoryMock.object, + debugLauncher.object, + ); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + testServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); + test('UNITTEST cancelation token called mid-run resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + + // Stub send command to then have token canceled + const stubTestServer = typeMoq.Mock.ofType(); + stubTestServer + .setup((t) => + t.sendCommand( + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + ) + .returns(() => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + + stubTestServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + stubTestServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + execServiceMock + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + cancellationToken.cancel(); + return { + proc: mockProc, + out: typeMoq.Mock.ofType>>().object, + dispose: () => { + /* no-body */ + }, + }; + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token + const deferredEOT = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); + utilsCreateEOTStub.callsFake(() => deferredEOT); + // set up test server + const unittestAdapter = new UnittestTestExecutionAdapter( + stubTestServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await unittestAdapter.runTests(Uri.file(myTestPath), [], false, testRunMock.object); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + stubTestServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); + test('UNITTEST cancelation token called mid-debug resolves correctly', async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + + // Stub send command to then have token canceled + const stubTestServer = typeMoq.Mock.ofType(); + stubTestServer + .setup((t) => + t.sendCommand( + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + ) + .returns(() => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + + stubTestServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => 'uuid123'); + stubTestServer + .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + // mock exec service and exec factory + const execServiceMock = typeMoq.Mock.ofType(); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async () => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + const execFactoryMock = typeMoq.Mock.ofType(); + execFactoryMock + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceMock.object)); + execFactoryMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceMock.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + const deferredStartServer = createDeferred(); + utilsStartServerStub.callsFake(() => { + deferredStartServer.resolve(); + return Promise.resolve(54321); + }); + // mock EOT token + const deferredEOT = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); + utilsCreateEOTStub.callsFake(() => deferredEOT); + // set up test server + const unittestAdapter = new UnittestTestExecutionAdapter( + stubTestServer.object, + configService, + typeMoq.Mock.ofType().object, + ); + await unittestAdapter.runTests(Uri.file(myTestPath), [], false, testRunMock.object); + // wait for server to start to keep test from failing + await deferredStartServer.promise; + + stubTestServer.verify((x) => x.deleteUUID(typeMoq.It.isAny()), typeMoq.Times.once()); + }); +}); diff --git a/src/test/testing/testController/utils.unit.test.ts b/src/test/testing/testController/utils.unit.test.ts index d971c7d37c9f..9168abc7041f 100644 --- a/src/test/testing/testController/utils.unit.test.ts +++ b/src/test/testing/testController/utils.unit.test.ts @@ -6,15 +6,15 @@ import { JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER, JSONRPC_UUID_HEADER, - jsonRPCContent, - jsonRPCHeaders, + ExtractJsonRPCData, + parseJsonRPCHeadersAndData, } from '../../../client/testing/testController/common/utils'; suite('Test Controller Utils: JSON RPC', () => { test('Empty raw data string', async () => { const rawDataString = ''; - const output = jsonRPCHeaders(rawDataString); + const output = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(output.headers.size, 0); assert.deepStrictEqual(output.remainingRawData, ''); }); @@ -22,20 +22,20 @@ suite('Test Controller Utils: JSON RPC', () => { test('Valid data empty JSON', async () => { const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 2\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n{}`; - const rpcHeaders = jsonRPCHeaders(rawDataString); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(rpcHeaders.headers.size, 3); assert.deepStrictEqual(rpcHeaders.remainingRawData, '{}'); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, '{}'); }); test('Valid data NO JSON', async () => { const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 0\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n`; - const rpcHeaders = jsonRPCHeaders(rawDataString); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(rpcHeaders.headers.size, 3); assert.deepStrictEqual(rpcHeaders.remainingRawData, ''); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, ''); }); @@ -45,10 +45,10 @@ suite('Test Controller Utils: JSON RPC', () => { '{"jsonrpc": "2.0", "method": "initialize", "params": {"processId": 1234, "rootPath": "/home/user/project", "rootUri": "file:///home/user/project", "capabilities": {}}, "id": 0}'; const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`; - const rpcHeaders = jsonRPCHeaders(rawDataString); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); assert.deepStrictEqual(rpcHeaders.headers.size, 3); assert.deepStrictEqual(rpcHeaders.remainingRawData, json); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, json); }); @@ -58,9 +58,9 @@ suite('Test Controller Utils: JSON RPC', () => { const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`; const rawDataString2 = rawDataString + rawDataString; - const rpcHeaders = jsonRPCHeaders(rawDataString2); + const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString2); assert.deepStrictEqual(rpcHeaders.headers.size, 3); - const rpcContent = jsonRPCContent(rpcHeaders.headers, rpcHeaders.remainingRawData); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); assert.deepStrictEqual(rpcContent.extractedJSON, json); assert.deepStrictEqual(rpcContent.remainingRawData, rawDataString); }); diff --git a/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py index 8c6a29adf495..a76856ebb929 100644 --- a/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py +++ b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py @@ -4,13 +4,13 @@ import unittest -@pytest.mark.parametrize("num", range(0, 200)) +@pytest.mark.parametrize("num", range(0, 2000)) def test_odd_even(num): assert num % 2 == 0 class NumbersTest(unittest.TestCase): def test_even(self): - for i in range(0, 200): + for i in range(0, 2000): with self.subTest(i=i): self.assertEqual(i % 2, 0) From a9d4df919d0039dc3e8006d5eb6c1b09fe1a9273 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 18 Sep 2023 01:16:31 -0700 Subject: [PATCH 0190/1136] Allow publish of pre-releases to VS Code stable this iteration (#22009) https://github.com/microsoft/vscode-python/pull/21997#discussion_r1327940466 For https://github.com/microsoft/vscode-python/issues/22005 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98f97c4a1905..8ce90a2a487e 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.83.0-20230915" + "vscode": "^1.82.0" }, "enableTelemetry": false, "keywords": [ From 7291d303078d6adbf367160781673c0f697f9487 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 18 Sep 2023 10:43:15 -0700 Subject: [PATCH 0191/1136] Remove repo labels corresponding to removing unrecognized label workflow (#22022) Workflow has been removed: https://github.com/microsoft/vscode-github-triage-actions/pull/188 --- .github/workflows/getLabels.js | 25 ------------------------- .github/workflows/issue-labels.yml | 7 ++----- .vscode/settings.json | 8 +++----- 3 files changed, 5 insertions(+), 35 deletions(-) delete mode 100644 .github/workflows/getLabels.js diff --git a/.github/workflows/getLabels.js b/.github/workflows/getLabels.js deleted file mode 100644 index 99060e7205eb..000000000000 --- a/.github/workflows/getLabels.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * To run this file: - * * npm install @octokit/rest - * * node .github/workflows/getLabels.js - * - * This script assumes the maximum number of labels to be 100. - */ - -const { Octokit } = require('@octokit/rest'); -const github = new Octokit(); -github.rest.issues - .listLabelsForRepo({ - owner: 'microsoft', - repo: 'vscode-python', - per_page: 100, - }) - .then((result) => { - const labels = result.data.map((label) => label.name); - console.log( - '\nNumber of labels found:', - labels.length, - ", verify that it's the same as number of labels listed in https://github.com/microsoft/vscode-python/labels\n", - ); - console.log(JSON.stringify(labels), '\n'); - }); diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index 7817ed62bd9b..8b084aef409f 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -5,8 +5,6 @@ on: types: [opened, reopened] env: - # To update the list of labels, see `getLabels.js`. - REPO_LABELS: '["area-api","area-data science","area-debugging","area-diagnostics","area-editor-*","area-environments","area-formatting","area-intellisense","area-internal","area-linting","area-repl","area-terminal","area-testing","author-verification-requested","bug","community ask","debt","dependencies","documentation","experimenting","feature-request","good first issue","help wanted","important","info-needed","invalid-testplan-item","investigating","iteration-candidate","iteration-plan","iteration-plan-draft","javascript","linux","macos","meta","needs community feedback","needs PR","needs proposal","needs spike","no-changelog","on-testplan","partner ask","regression","release-plan","reports-wanted","skip package*.json","skip tests","tensorboard","testplan-item","triage-needed","verification-found","verification-needed","verification-steps-needed","verified","windows"]' TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd","anthonykim1"]' permissions: @@ -15,7 +13,7 @@ permissions: jobs: # From https://github.com/marketplace/actions/github-script#apply-a-label-to-an-issue. add-classify-label: - name: "Add 'triage-needed' and remove unrecognizable labels & assignees" + name: "Add 'triage-needed' and remove assignees" runs-on: ubuntu-latest steps: - name: Checkout Actions @@ -28,9 +26,8 @@ jobs: - name: Install Actions run: npm install --production --prefix ./actions - - name: "Add 'triage-needed' and remove unrecognizable labels & assignees" + - name: "Add 'triage-needed' and remove assignees" uses: ./actions/python-issue-labels with: triagers: ${{ env.TRIAGERS }} token: ${{secrets.GITHUB_TOKEN}} - repo_labels: ${{ env.REPO_LABELS }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 06011b3d13cd..a5dbb4869fd9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -67,12 +67,10 @@ "typescript.preferences.importModuleSpecifier": "relative", "debug.javascript.usePreview": false, // Branch name suggestion. + "git.branchProtectionPrompt": "alwaysCommitToNewBranch", "git.branchRandomName.enable": true, - "git.branchProtection": [ - "main", - "release/*" - ], + "git.branchProtection": ["main", "release/*"], "git.pullBeforeCheckout": true, // Open merge editor for resolving conflicts. - "git.mergeEditor": true, + "git.mergeEditor": true } From 2df331be969635bf8c425b5e26c01858561b7ed2 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 18 Sep 2023 11:20:17 -0700 Subject: [PATCH 0192/1136] switch | to unions to be 3.8 compatible (#22025) fixes https://github.com/microsoft/vscode-python/issues/22020 --- pythonFiles/vscode_pytest/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 21165a02bf4b..f5827f87e1b4 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -609,7 +609,7 @@ class ExecutionPayloadDict(Dict): class EOTPayloadDict(TypedDict): """A dictionary that is used to send a end of transmission post request to the server.""" - command_type: Literal["discovery"] | Literal["execution"] + command_type: Union[Literal["discovery"], Literal["execution"]] eot: bool @@ -672,7 +672,7 @@ def default(self, obj): def send_post_request( - payload: ExecutionPayloadDict | DiscoveryPayloadDict | EOTPayloadDict, + payload: Union[ExecutionPayloadDict, DiscoveryPayloadDict, EOTPayloadDict], cls_encoder=None, ): """ From 05ae2660c10ca8f94cdddc3c50aea18eba853eaa Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 18 Sep 2023 12:03:04 -0700 Subject: [PATCH 0193/1136] Add python 3.8 and 3x specific runs (#22023) Closes https://github.com/microsoft/vscode-python/issues/22024 --- .github/workflows/build.yml | 46 +++++++++++++++++++--- .github/workflows/pr-check.yml | 50 +++++++++++++++++++----- pythonFiles/unittestadapter/discovery.py | 6 ++- pythonFiles/unittestadapter/execution.py | 6 ++- 4 files changed, 88 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 873b81fa66f8..3d121564d385 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -106,7 +106,45 @@ jobs: version: 1.1.308 working-directory: 'pythonFiles' - ### Non-smoke tests + python-tests: + name: Python Tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. + os: [ubuntu-latest, windows-latest] + # Run the tests on the oldest and most recent versions of Python. + python: ['3.8', '3.x'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: ${{ env.special-working-directory-relative }} + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@v1 + with: + requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' + options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Run Python unit tests + run: python pythonFiles/tests/run_all.py + tests: name: Tests if: github.repository == 'microsoft/vscode-python' @@ -122,7 +160,7 @@ jobs: # and we assume that Ubuntu is enough to cover the UNIX case. os: [ubuntu-latest, windows-latest] python: ['3.x'] - test-suite: [ts-unit, python-unit, venv, single-workspace, multi-workspace, debugger, functional] + test-suite: [ts-unit, venv, single-workspace, multi-workspace, debugger, functional] steps: - name: Checkout uses: actions/checkout@v4 @@ -268,10 +306,6 @@ jobs: run: npm run test:unittests if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, '3.') - - name: Run Python unit tests - run: python pythonFiles/tests/run_all.py - if: matrix.test-suite == 'python-unit' - # The virtual environment based tests use the `testSingleWorkspace` set of tests # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, # which is set in the "Prepare environment for venv tests" step. diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index c7335702e991..aa223782de62 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -80,7 +80,45 @@ jobs: version: 1.1.308 working-directory: 'pythonFiles' - ### Non-smoke tests + python-tests: + name: Python Tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. + os: [ubuntu-latest, windows-latest] + # Run the tests on the oldest and most recent versions of Python. + python: ['3.8', '3.x'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: ${{ env.special-working-directory-relative }} + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@v1 + with: + requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' + options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Run Python unit tests + run: python pythonFiles/tests/run_all.py + tests: name: Tests # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. @@ -96,7 +134,7 @@ jobs: os: [ubuntu-latest, windows-latest] # Run the tests on the oldest and most recent versions of Python. python: ['3.x'] - test-suite: [ts-unit, python-unit, venv, single-workspace, debugger, functional] + test-suite: [ts-unit, venv, single-workspace, debugger, functional] steps: - name: Checkout @@ -139,14 +177,12 @@ jobs: with: requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/python" --no-cache-dir --implementation py' - if: startsWith(matrix.python, 3.) - name: Install Jedi requirements uses: brettcannon/pip-secure-install@v1 with: requirements-file: '"${{ env.special-working-directory-relative }}/pythonFiles/jedilsp_requirements/requirements.txt"' options: '-t "${{ env.special-working-directory-relative }}/pythonFiles/lib/jedilsp" --no-cache-dir --implementation py' - if: startsWith(matrix.python, 3.) - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt @@ -243,12 +279,6 @@ jobs: run: npm run test:unittests if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, 3.) - # Run the Python tests in our codebase. - - name: Run Python unit tests - run: | - python pythonFiles/tests/run_all.py - if: matrix.test-suite == 'python-unit' - # The virtual environment based tests use the `testSingleWorkspace` set of tests # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, # which is set in the "Prepare environment for venv tests" step. diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index 6208d24bee9f..69c14cae34e6 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -51,7 +51,7 @@ class PayloadDict(TypedDict): class EOTPayloadDict(TypedDict): """A dictionary that is used to send a end of transmission post request to the server.""" - command_type: Literal["discovery"] | Literal["execution"] + command_type: Union[Literal["discovery"], Literal["execution"]] eot: bool @@ -113,7 +113,9 @@ def discover_tests( return payload -def post_response(payload: PayloadDict | EOTPayloadDict, port: int, uuid: str) -> None: +def post_response( + payload: Union[PayloadDict, EOTPayloadDict], port: int, uuid: str +) -> None: # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) data = json.dumps(payload) diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 7bbc97e78f31..a208056c6682 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -172,7 +172,7 @@ class PayloadDict(TypedDict): class EOTPayloadDict(TypedDict): """A dictionary that is used to send a end of transmission post request to the server.""" - command_type: Literal["discovery"] | Literal["execution"] + command_type: Union[Literal["discovery"], Literal["execution"]] eot: bool @@ -250,7 +250,9 @@ def send_run_data(raw_data, port, uuid): post_response(payload, port, uuid) -def post_response(payload: PayloadDict | EOTPayloadDict, port: int, uuid: str) -> None: +def post_response( + payload: Union[PayloadDict, EOTPayloadDict], port: int, uuid: str +) -> None: # Build the request data (it has to be a POST request or the Node side will not process it), and send it. addr = ("localhost", port) global __socket From b41fee72564e569808fb617328b58b0364d1995a Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 18 Sep 2023 15:11:01 -0700 Subject: [PATCH 0194/1136] Remove old linter and formatter prompts and commands (#21979) --- package.json | 33 -- package.nls.json | 3 - src/client/common/application/commands.ts | 3 - src/client/common/constants.ts | 3 - .../common/installer/productInstaller.ts | 162 +--------- src/client/extensionActivation.ts | 3 - .../linters/errorHandlers/errorHandler.ts | 6 +- .../linters/errorHandlers/notInstalled.ts | 29 -- src/client/linters/errorHandlers/standard.ts | 18 ++ src/client/linters/linterCommands.ts | 114 ------- src/test/common/installer.test.ts | 12 +- .../installer.invalidPath.unit.test.ts | 5 +- .../common/installer/installer.unit.test.ts | 297 +----------------- .../installer/productInstaller.unit.test.ts | 264 +--------------- .../common/installer/productPath.unit.test.ts | 7 +- src/test/common/productsToTest.ts | 25 ++ src/test/linters/lint.multilinter.test.ts | 126 -------- src/test/linters/linterCommands.unit.test.ts | 182 ----------- 18 files changed, 64 insertions(+), 1228 deletions(-) delete mode 100644 src/client/linters/errorHandlers/notInstalled.ts delete mode 100644 src/client/linters/linterCommands.ts create mode 100644 src/test/common/productsToTest.ts delete mode 100644 src/test/linters/lint.multilinter.test.ts delete mode 100644 src/test/linters/linterCommands.unit.test.ts diff --git a/package.json b/package.json index 8ce90a2a487e..9cf564122d7b 100644 --- a/package.json +++ b/package.json @@ -364,11 +364,6 @@ "command": "python.createEnvironment-button", "title": "%python.command.python.createEnvironment.title%" }, - { - "category": "Python", - "command": "python.enableLinting", - "title": "%python.command.python.enableLinting.title%" - }, { "category": "Python", "command": "python.enableSourceMapSupport", @@ -430,21 +425,11 @@ "icon": "$(run-errors)", "title": "%python.command.testing.rerunFailedTests.title%" }, - { - "category": "Python", - "command": "python.runLinting", - "title": "%python.command.python.runLinting.title%" - }, { "category": "Python", "command": "python.setInterpreter", "title": "%python.command.python.setInterpreter.title%" }, - { - "category": "Python", - "command": "python.setLinter", - "title": "%python.command.python.setLinter.title%" - }, { "category": "Python Refactor", "command": "python.sortImports", @@ -1807,12 +1792,6 @@ "title": "%python.command.python.createTerminal.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python", - "command": "python.enableLinting", - "title": "%python.command.python.enableLinting.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python", "command": "python.enableSourceMapSupport", @@ -1885,24 +1864,12 @@ "title": "%python.command.testing.rerunFailedTests.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python", - "command": "python.runLinting", - "title": "%python.command.python.runLinting.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python", "command": "python.setInterpreter", "title": "%python.command.python.setInterpreter.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python", - "command": "python.setLinter", - "title": "%python.command.python.setLinter.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python Refactor", "command": "python.sortImports", diff --git a/package.nls.json b/package.nls.json index b8e82b150e76..d260c98b0b5a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -18,9 +18,6 @@ "python.command.python.execSelectionInTerminal.title": "Run Selection/Line in Python Terminal", "python.command.python.execSelectionInDjangoShell.title": "Run Selection/Line in Django Shell", "python.command.python.reportIssue.title": "Report Issue...", - "python.command.python.setLinter.title": "Select Linter", - "python.command.python.enableLinting.title": "Enable/Disable Linting", - "python.command.python.runLinting.title": "Run Linting", "python.command.python.enableSourceMapSupport.title": "Enable Source Map Support For Extension Debugging", "python.command.python.clearCacheAndReload.title": "Clear Cache and Reload Window", "python.command.python.analysis.restartLanguageServer.title": "Restart Language Server", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 2a4404440101..d8944fe2b057 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -23,8 +23,6 @@ interface ICommandNameWithoutArgumentTypeMapping { [Commands.ClearWorkspaceInterpreter]: []; [Commands.Set_Interpreter]: []; [Commands.Set_ShebangInterpreter]: []; - [Commands.Run_Linter]: []; - [Commands.Enable_Linter]: []; ['workbench.action.showCommands']: []; ['workbench.action.debug.continue']: []; ['workbench.action.debug.stepOver']: []; @@ -35,7 +33,6 @@ interface ICommandNameWithoutArgumentTypeMapping { ['editor.action.formatDocument']: []; ['editor.action.rename']: []; [Commands.ViewOutput]: []; - [Commands.Set_Linter]: []; [Commands.Start_REPL]: []; [Commands.Enable_SourceMap_Support]: []; [Commands.Exec_Selection_In_Terminal]: []; diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index bea0ef9e235c..f18fd20bb3dc 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -40,7 +40,6 @@ export namespace Commands { export const Create_Environment_Button = 'python.createEnvironment-button'; export const Create_Terminal = 'python.createTerminal'; export const Debug_In_Terminal = 'python.debugInTerminal'; - export const Enable_Linter = 'python.enableLinting'; export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; export const Exec_In_Terminal = 'python.execInTerminal'; export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; @@ -56,9 +55,7 @@ export namespace Commands { export const PickLocalProcess = 'python.pickLocalProcess'; export const RefreshTensorBoard = 'python.refreshTensorBoard'; export const ReportIssue = 'python.reportIssue'; - export const Run_Linter = 'python.runLinting'; export const Set_Interpreter = 'python.setInterpreter'; - export const Set_Linter = 'python.setLinter'; export const Set_ShebangInterpreter = 'python.setShebangInterpreter'; export const Sort_Imports = 'python.sortImports'; export const Start_REPL = 'python.startREPL'; diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index 526369f9e9ad..fba860aaa383 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -6,12 +6,10 @@ import { CancellationToken, l10n, Uri } from 'vscode'; import '../extensions'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { LinterId } from '../../linters/types'; import { EnvironmentType, ModuleInstallerType, PythonEnvironment } from '../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../application/types'; -import { Commands } from '../constants'; +import { IApplicationShell, IWorkspaceService } from '../application/types'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../process/types'; import { IConfigurationService, @@ -22,7 +20,7 @@ import { Product, ProductType, } from '../types'; -import { Common, Linters } from '../utils/localize'; +import { Common } from '../utils/localize'; import { isResource, noop } from '../utils/misc'; import { translateProductToModule } from './moduleInstaller'; import { ProductNames } from './productNames'; @@ -225,158 +223,6 @@ abstract class BaseInstaller implements IBaseInstaller { } } -const doNotDisplayFormatterPromptStateKey = 'FORMATTER_NOT_INSTALLED_KEY'; - -export class FormatterInstaller extends BaseInstaller { - protected async promptToInstallImplementation( - product: Product, - resource?: Uri, - cancel?: CancellationToken, - _flags?: ModuleInstallFlags, - ): Promise { - const neverShowAgain = this.persistentStateFactory.createGlobalPersistentState( - doNotDisplayFormatterPromptStateKey, - false, - ); - - if (neverShowAgain.value) { - return InstallerResponse.Ignore; - } - - // Hard-coded on purpose because the UI won't necessarily work having - // another formatter. - const formatters = [Product.autopep8, Product.black, Product.yapf]; - const formatterNames = formatters.map((formatter) => ProductNames.get(formatter)!); - const productName = ProductNames.get(product)!; - formatterNames.splice(formatterNames.indexOf(productName), 1); - const useOptions = formatterNames.map((name) => l10n.t('Use {0}', name)); - const yesChoice = Common.bannerLabelYes; - - const options = [...useOptions, Common.doNotShowAgain]; - let message = l10n.t('Formatter {0} is not installed. Install?', productName); - if (this.isExecutableAModule(product, resource)) { - options.splice(0, 0, yesChoice); - } else { - const executable = this.getExecutableNameFromSettings(product, resource); - message = l10n.t('Path to the {0} formatter is invalid ({1})', productName, executable); - } - - const item = await this.appShell.showErrorMessage(message, ...options); - if (item === yesChoice) { - return this.install(product, resource, cancel); - } - - if (item === Common.doNotShowAgain) { - neverShowAgain.updateValue(true); - return InstallerResponse.Ignore; - } - - if (typeof item === 'string') { - for (const formatter of formatters) { - const formatterName = ProductNames.get(formatter)!; - - if (item.endsWith(formatterName)) { - await this.configService.updateSetting('formatting.provider', formatterName, resource); - return this.install(formatter, resource, cancel); - } - } - } - - return InstallerResponse.Ignore; - } -} - -export class LinterInstaller extends BaseInstaller { - constructor(protected serviceContainer: IServiceContainer) { - super(serviceContainer); - } - - protected async promptToInstallImplementation( - product: Product, - resource?: Uri, - cancel?: CancellationToken, - _flags?: ModuleInstallFlags, - ): Promise { - return this.oldPromptForInstallation(product, resource, cancel); - } - - /** - * For installers that want to avoid prompting the user over and over, they can make use of a - * persisted true/false value representing user responses to 'stop showing this prompt'. This method - * gets the persisted value given the installer-defined key. - * - * @param key Key to use to get a persisted response value, each installer must define this for themselves. - * @returns Boolean: The current state of the stored response key given. - */ - protected getStoredResponse(key: string): boolean { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const state = factory.createGlobalPersistentState(key, undefined); - return state.value === true; - } - - private async oldPromptForInstallation(product: Product, resource?: Uri, cancel?: CancellationToken) { - const productName = ProductNames.get(product)!; - const { install } = Common; - const { doNotShowAgain } = Common; - const disableLinterInstallPromptKey = `${productName}_DisableLinterInstallPrompt`; - const { selectLinter } = Linters; - - if (this.getStoredResponse(disableLinterInstallPromptKey) === true) { - return InstallerResponse.Ignore; - } - - const options = [selectLinter, doNotShowAgain]; - - let message = l10n.t('Linter {0} is not installed.', productName); - if (this.isExecutableAModule(product, resource)) { - options.splice(0, 0, install); - } else { - const executable = this.getExecutableNameFromSettings(product, resource); - message = l10n.t('Path to the {0} linter is invalid ({1})', productName, executable); - } - const response = await this.appShell.showErrorMessage(message, ...options); - if (response === install) { - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { - tool: productName as LinterId, - action: 'install', - }); - return this.install(product, resource, cancel); - } - if (response === doNotShowAgain) { - await this.setStoredResponse(disableLinterInstallPromptKey, true); - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { - tool: productName as LinterId, - action: 'disablePrompt', - }); - return InstallerResponse.Ignore; - } - - if (response === selectLinter) { - sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { action: 'select' }); - const commandManager = this.serviceContainer.get(ICommandManager); - await commandManager.executeCommand(Commands.Set_Linter); - } - return InstallerResponse.Ignore; - } - - /** - * For installers that want to avoid prompting the user over and over, they can make use of a - * persisted true/false value representing user responses to 'stop showing this prompt'. This - * method will set that persisted value given the installer-defined key. - * - * @param key Key to use to get a persisted response value, each installer must define this for themselves. - * @param value Boolean value to store for the user - if they choose to not be prompted again for instance. - * @returns Boolean: The current state of the stored response key given. - */ - private async setStoredResponse(key: string, value: boolean): Promise { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const state = factory.createGlobalPersistentState(key, undefined); - if (state && state.value !== value) { - await state.updateValue(value); - } - } -} - export class TestFrameworkInstaller extends BaseInstaller { protected async promptToInstallImplementation( product: Product, @@ -687,10 +533,6 @@ export class ProductInstaller implements IInstaller { private createInstaller(product: Product): IBaseInstaller { const productType = this.productService.getProductType(product); switch (productType) { - case ProductType.Formatter: - return new FormatterInstaller(this.serviceContainer); - case ProductType.Linter: - return new LinterInstaller(this.serviceContainer); case ProductType.TestFramework: return new TestFrameworkInstaller(this.serviceContainer); case ProductType.DataScience: diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 8dcea063676a..c82bddef3c20 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -28,7 +28,6 @@ import { IDebugConfigurationService, IDynamicDebugConfigurationService } from '. import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; -import { LinterCommands } from './linters/linterCommands'; import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; import { PythonFormattingEditProvider } from './providers/formatProvider'; import { ReplProvider } from './providers/replProvider'; @@ -168,8 +167,6 @@ async function activateLegacy(ext: ExtensionState): Promise { serviceManager.get(ICodeExecutionManager).registerCommands(); - disposables.push(new LinterCommands(serviceManager)); - if ( pythonSettings && pythonSettings.formatting && diff --git a/src/client/linters/errorHandlers/errorHandler.ts b/src/client/linters/errorHandlers/errorHandler.ts index dc884e97739c..af28dd61c3a4 100644 --- a/src/client/linters/errorHandlers/errorHandler.ts +++ b/src/client/linters/errorHandlers/errorHandler.ts @@ -3,17 +3,13 @@ import { ExecutionInfo, Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { IErrorHandler } from '../types'; import { BaseErrorHandler } from './baseErrorHandler'; -import { NotInstalledErrorHandler } from './notInstalled'; import { StandardErrorHandler } from './standard'; export class ErrorHandler implements IErrorHandler { private handler: BaseErrorHandler; constructor(product: Product, serviceContainer: IServiceContainer) { - // Create chain of handlers. - const standardErrorHandler = new StandardErrorHandler(product, serviceContainer); - this.handler = new NotInstalledErrorHandler(product, serviceContainer); - this.handler.setNextHandler(standardErrorHandler); + this.handler = new StandardErrorHandler(product, serviceContainer); } public handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { diff --git a/src/client/linters/errorHandlers/notInstalled.ts b/src/client/linters/errorHandlers/notInstalled.ts deleted file mode 100644 index 8c598ae5ece2..000000000000 --- a/src/client/linters/errorHandlers/notInstalled.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Uri } from 'vscode'; -import { IPythonExecutionFactory } from '../../common/process/types'; -import { ExecutionInfo } from '../../common/types'; -import { traceError, traceLog, traceWarn } from '../../logging'; -import { ILinterManager } from '../types'; -import { BaseErrorHandler } from './baseErrorHandler'; - -export class NotInstalledErrorHandler extends BaseErrorHandler { - public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - const pythonExecutionService = await this.serviceContainer - .get(IPythonExecutionFactory) - .create({ resource }); - const isModuleInstalled = await pythonExecutionService.isModuleInstalled(execInfo.moduleName!); - if (isModuleInstalled) { - return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : false; - } - - this.installer - .promptToInstall(this.product, resource) - .catch((ex) => traceError('NotInstalledErrorHandler.promptToInstall', ex)); - - const linterManager = this.serviceContainer.get(ILinterManager); - const info = linterManager.getLinterInfo(execInfo.product!); - const customError = `Linter '${info.id}' is not installed. Please install it or select another linter".`; - traceLog(`\n${customError}\n${error}`); - traceWarn(customError, error); - return true; - } -} diff --git a/src/client/linters/errorHandlers/standard.ts b/src/client/linters/errorHandlers/standard.ts index f6e04b50ff19..6367da7abe4a 100644 --- a/src/client/linters/errorHandlers/standard.ts +++ b/src/client/linters/errorHandlers/standard.ts @@ -18,6 +18,24 @@ export class StandardErrorHandler extends BaseErrorHandler { const info = linterManager.getLinterInfo(execInfo.product!); traceError(`There was an error in running the linter ${info.id}`, error); + if (info.id === LinterId.PyLint) { + traceError('Support for "pylint" is moved to ms-python.pylint extension.'); + traceError( + 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.pylint', + ); + } else if (info.id === LinterId.Flake8) { + traceError('Support for "flake8" is moved to ms-python.flake8 extension.'); + traceError( + 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.flake8', + ); + } else if (info.id === LinterId.MyPy) { + traceError('Support for "mypy" is moved to ms-python.mypy-type-checker extension.'); + traceError( + 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker', + ); + } + traceError(`If the error is due to missing ${info.id}, please install ${info.id} using pip manually.`); + traceError('Learn more here: https://aka.ms/AAlgvkb'); traceLog(`Linting with ${info.id} failed.`); traceLog(error.toString()); diff --git a/src/client/linters/linterCommands.ts b/src/client/linters/linterCommands.ts deleted file mode 100644 index cc35e80f26b1..000000000000 --- a/src/client/linters/linterCommands.ts +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DiagnosticCollection, Disposable, l10n, QuickPickOptions, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; -import { Commands } from '../common/constants'; -import { IDisposable } from '../common/types'; -import { Common } from '../common/utils/localize'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { ILinterManager, ILintingEngine, LinterId } from './types'; - -export class LinterCommands implements IDisposable { - private disposables: Disposable[] = []; - - private linterManager: ILinterManager; - - private readonly appShell: IApplicationShell; - - private readonly documentManager: IDocumentManager; - - constructor(private serviceContainer: IServiceContainer) { - this.linterManager = this.serviceContainer.get(ILinterManager); - this.appShell = this.serviceContainer.get(IApplicationShell); - this.documentManager = this.serviceContainer.get(IDocumentManager); - - const commandManager = this.serviceContainer.get(ICommandManager); - commandManager.registerCommand(Commands.Set_Linter, this.setLinterAsync.bind(this)); - commandManager.registerCommand(Commands.Enable_Linter, this.enableLintingAsync.bind(this)); - commandManager.registerCommand(Commands.Run_Linter, this.runLinting.bind(this)); - } - - public dispose(): void { - this.disposables.forEach((disposable) => disposable.dispose()); - } - - public async setLinterAsync(): Promise { - const linters = this.linterManager.getAllLinterInfos(); - const suggestions = linters.map((x) => x.id).sort(); - const linterList = ['Disable Linting', ...suggestions]; - const activeLinters = await this.linterManager.getActiveLinters(this.settingsUri); - - let current: string; - switch (activeLinters.length) { - case 0: - current = 'none'; - break; - case 1: - current = activeLinters[0].id; - break; - default: - current = 'multiple selected'; - break; - } - - const quickPickOptions: QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${current}`, - }; - - const selection = await this.appShell.showQuickPick(linterList, quickPickOptions); - if (selection !== undefined) { - if (selection === 'Disable Linting') { - await this.linterManager.enableLintingAsync(false); - sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { enabled: false }); - } else { - const index = linters.findIndex((x) => x.id === selection); - if (activeLinters.length > 1) { - const response = await this.appShell.showWarningMessage( - l10n.t("Multiple linters are enabled in settings. Replace with '{0}'?", selection), - Common.bannerLabelYes, - Common.bannerLabelNo, - ); - if (response !== Common.bannerLabelYes) { - return; - } - } - await this.linterManager.setActiveLintersAsync([linters[index].product], this.settingsUri); - sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { tool: selection as LinterId, enabled: true }); - } - } - } - - public async enableLintingAsync(): Promise { - const options = ['Enable', 'Disable']; - const current = (await this.linterManager.isLintingEnabled(this.settingsUri)) ? options[0] : options[1]; - - const quickPickOptions: QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${current}`, - }; - - const selection = await this.appShell.showQuickPick(options, quickPickOptions); - - if (selection !== undefined) { - const enable: boolean = selection === options[0]; - await this.linterManager.enableLintingAsync(enable, this.settingsUri); - } - } - - public runLinting(): Promise { - const engine = this.serviceContainer.get(ILintingEngine); - return engine.lintOpenPythonFiles('manual'); - } - - private get settingsUri(): Uri | undefined { - return this.documentManager.activeTextEditor ? this.documentManager.activeTextEditor.document.uri : undefined; - } -} diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 7ff0ee81c27f..5c1842a2c97c 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -87,7 +87,6 @@ import { ProductType, } from '../../client/common/types'; import { createDeferred } from '../../client/common/utils/async'; -import { getNamesAndValues } from '../../client/common/utils/enum'; import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; import { Random } from '../../client/common/utils/random'; import { ImportTracker } from '../../client/telemetry/importTracker'; @@ -105,6 +104,7 @@ import { } from '../../client/interpreter/configuration/types'; import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { getProductsForInstallerTests } from './productsToTest'; suite('Installer', () => { let ioc: UnitTestIocContainer; @@ -276,7 +276,8 @@ suite('Installer', () => { await installer.isInstalled(product, resource); await checkInstalledDef.promise; } - getNamesAndValues(Product).forEach((prod) => { + + getProductsForInstallerTests().forEach((prod) => { test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async function () { if ( new ProductService().getProductType(prod.value) === ProductType.DataScience || @@ -293,7 +294,7 @@ suite('Installer', () => { new MockModuleInstaller('two', true), ); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest || prod.value === Product.isort) { + if (prod.value === Product.unittest) { return undefined; } await testCheckingIfProductIsInstalled(prod.value); @@ -316,7 +317,8 @@ suite('Installer', () => { await installer.install(product); await checkInstalledDef.promise; } - getNamesAndValues(Product).forEach((prod) => { + + getProductsForInstallerTests().forEach((prod) => { test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async function () { const productType = new ProductService().getProductType(prod.value); if (productType === ProductType.DataScience || productType === ProductType.Python) { @@ -331,7 +333,7 @@ suite('Installer', () => { new MockModuleInstaller('two', true), ); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest || prod.value === Product.isort) { + if (prod.value === Product.unittest) { return undefined; } await testInstallingProduct(prod.value); diff --git a/src/test/common/installer/installer.invalidPath.unit.test.ts b/src/test/common/installer/installer.invalidPath.unit.test.ts index 7e8392204600..b6738759f0d7 100644 --- a/src/test/common/installer/installer.invalidPath.unit.test.ts +++ b/src/test/common/installer/installer.invalidPath.unit.test.ts @@ -14,10 +14,10 @@ import { ProductInstaller } from '../../../client/common/installer/productInstal import { ProductService } from '../../../client/common/installer/productService'; import { IProductPathService, IProductService } from '../../../client/common/installer/types'; import { IPersistentState, IPersistentStateFactory, Product, ProductType } from '../../../client/common/types'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { getProductsForInstallerTests } from '../productsToTest'; use(chaiAsPromised); @@ -26,7 +26,7 @@ suite('Module Installer - Invalid Paths', () => { ['moduleName', path.join('users', 'dev', 'tool', 'executable')].forEach((pathToExecutable) => { const isExecutableAModule = path.basename(pathToExecutable) === pathToExecutable; - getNamesAndValues(Product).forEach((product) => { + getProductsForInstallerTests().forEach((product) => { let installer: ProductInstaller; let serviceContainer: TypeMoq.IMock; let app: TypeMoq.IMock; @@ -78,7 +78,6 @@ suite('Module Installer - Invalid Paths', () => { }); switch (product.value) { - case Product.isort: case Product.unittest: { return; } diff --git a/src/test/common/installer/installer.unit.test.ts b/src/test/common/installer/installer.unit.test.ts index 38b9d9174472..69a5f3678f69 100644 --- a/src/test/common/installer/installer.unit.test.ts +++ b/src/test/common/installer/installer.unit.test.ts @@ -6,24 +6,11 @@ import { assert, expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Disposable, Uri, WorkspaceFolder } from 'vscode'; -import { ApplicationShell } from '../../../client/common/application/applicationShell'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../client/common/application/types'; -import { WorkspaceService } from '../../../client/common/application/workspace'; -import { ConfigurationService } from '../../../client/common/configuration/service'; -import { Commands } from '../../../client/common/constants'; -import { ExperimentService } from '../../../client/common/experiments/service'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; import '../../../client/common/extensions'; -import { - FormatterInstaller, - LinterInstaller, - ProductInstaller, -} from '../../../client/common/installer/productInstaller'; -import { ProductNames } from '../../../client/common/installer/productNames'; -import { LinterProductPathService } from '../../../client/common/installer/productPath'; +import { ProductInstaller } from '../../../client/common/installer/productInstaller'; import { ProductService } from '../../../client/common/installer/productService'; import { IInstallationChannelManager, @@ -39,9 +26,7 @@ import { IPythonExecutionService, } from '../../../client/common/process/types'; import { - IConfigurationService, IDisposableRegistry, - IExperimentService, InstallerResponse, IPersistentState, IPersistentStateFactory, @@ -49,20 +34,17 @@ import { ProductType, } from '../../../client/common/types'; import { createDeferred, Deferred } from '../../../client/common/utils/async'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { ServiceContainer } from '../../../client/ioc/container'; import { IServiceContainer } from '../../../client/ioc/types'; -import { LinterManager } from '../../../client/linters/linterManager'; -import { ILinterManager } from '../../../client/linters/types'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { sleep } from '../../common'; +import { getProductsForInstallerTests } from '../productsToTest'; use(chaiAsPromised); suite('Module Installer only', () => { [undefined, Uri.file('resource')].forEach((resource) => { - getNamesAndValues(Product) + getProductsForInstallerTests() .concat([{ name: 'Unknown product', value: 404 }]) .forEach((product) => { @@ -183,9 +165,6 @@ suite('Module Installer only', () => { }); return; } - case Product.isort: { - return; - } case Product.unittest: { test(`Ensure resource info is passed into the module installer ${product.name} (${ resource ? 'With a resource' : 'without a resource' @@ -638,273 +617,5 @@ suite('Module Installer only', () => { workspaceService.verifyAll(); }); }); - - suite('Test FormatterInstaller.promptToInstallImplementation', () => { - class FormatterInstallerTest extends FormatterInstaller { - public async promptToInstallImplementation(product: Product, uri?: Uri): Promise { - return super.promptToInstallImplementation(product, uri); - } - - // eslint-disable-next-line class-methods-use-this - protected getStoredResponse(_key: string) { - return false; - } - - // eslint-disable-next-line class-methods-use-this - protected isExecutableAModule(_product: Product, _resource?: Uri) { - return true; - } - } - let installer: FormatterInstallerTest; - let appShell: IApplicationShell; - let configService: IConfigurationService; - let workspaceService: IWorkspaceService; - let productService: IProductService; - let cmdManager: ICommandManager; - setup(() => { - const serviceContainer = mock(ServiceContainer); - appShell = mock(ApplicationShell); - configService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - productService = mock(ProductService); - cmdManager = mock(CommandManager); - - when(serviceContainer.get(IApplicationShell)).thenReturn(instance(appShell)); - when(serviceContainer.get(IConfigurationService)).thenReturn( - instance(configService), - ); - when(serviceContainer.get(IWorkspaceService)).thenReturn(instance(workspaceService)); - when(serviceContainer.get(IProductService)).thenReturn(instance(productService)); - when(serviceContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - - installer = new FormatterInstallerTest(instance(serviceContainer)); - }); - - teardown(() => { - sinon.restore(); - }); - - test('If nothing is selected, return Ignore as response', async () => { - const product = Product.autopep8; - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn((undefined as unknown) as Thenable); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Ignore); - }); - - test('If `Yes` is selected, install product', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Yes' as unknown) as Thenable); - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - assert.ok(install.calledOnceWith(product, resource, undefined)); - }); - - test('If `Use black` is selected, install black formatter', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Use black' as unknown) as Thenable); - when(configService.updateSetting('formatting.provider', 'black', resource)).thenResolve(); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - verify(configService.updateSetting('formatting.provider', 'black', resource)).once(); - assert.ok(install.calledOnceWith(Product.black, resource, undefined)); - }); - - test('If `Use yapf` is selected, install black formatter', async () => { - const product = Product.autopep8; - const install = sinon.stub(FormatterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - when( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).thenReturn(('Use yapf' as unknown) as Thenable); - when(configService.updateSetting('formatting.provider', 'yapf', resource)).thenResolve(); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage( - `Formatter autopep8 is not installed. Install?`, - 'Yes', - 'Use black', - 'Use yapf', - ), - ).once(); - expect(response).to.equal(InstallerResponse.Installed); - verify(configService.updateSetting('formatting.provider', 'yapf', resource)).once(); - assert.ok(install.calledOnceWith(Product.yapf, resource, undefined)); - }); - }); - }); -}); - -[undefined, Uri.file('resource')].forEach((resource) => { - suite(`Test LinterInstaller with resource: ${resource}`, () => { - class LinterInstallerTest extends LinterInstaller { - public isModuleExecutable = true; - - public async promptToInstallImplementation(product: Product, uri?: Uri): Promise { - return super.promptToInstallImplementation(product, uri); - } - - // eslint-disable-next-line class-methods-use-this - protected getStoredResponse(_key: string) { - return false; - } - - protected isExecutableAModule(_product: Product, _resource?: Uri) { - return this.isModuleExecutable; - } - } - - let installer: LinterInstallerTest; - let appShell: IApplicationShell; - let configService: IConfigurationService; - let workspaceService: IWorkspaceService; - let productService: IProductService; - let cmdManager: ICommandManager; - let experimentsService: IExperimentService; - let linterManager: ILinterManager; - let serviceContainer: IServiceContainer; - let productPathService: IProductPathService; - setup(() => { - serviceContainer = mock(ServiceContainer); - appShell = mock(ApplicationShell); - configService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - productService = mock(ProductService); - cmdManager = mock(CommandManager); - experimentsService = mock(ExperimentService); - linterManager = mock(LinterManager); - productPathService = mock(LinterProductPathService); - - when(serviceContainer.get(IApplicationShell)).thenReturn(instance(appShell)); - when(serviceContainer.get(IConfigurationService)).thenReturn( - instance(configService), - ); - when(serviceContainer.get(IWorkspaceService)).thenReturn(instance(workspaceService)); - when(serviceContainer.get(IProductService)).thenReturn(instance(productService)); - when(serviceContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - - const exp = instance(experimentsService); - when(serviceContainer.get(IExperimentService)).thenReturn(exp); - when(experimentsService.inExperiment(anything())).thenResolve(false); - - when(serviceContainer.get(ILinterManager)).thenReturn(instance(linterManager)); - when(serviceContainer.get(IProductPathService, ProductType.Linter)).thenReturn( - instance(productPathService), - ); - - installer = new LinterInstallerTest(instance(serviceContainer)); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Ensure 3 options for pylint', async () => { - const product = Product.pylint; - const options = ['Select Linter', "Don't show again"]; - const productName = ProductNames.get(product)!; - - await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).once(); - }); - test('Ensure select linter command is invoked', async () => { - const product = Product.pylint; - const options = ['Select Linter', "Don't show again"]; - const productName = ProductNames.get(product)!; - when( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).thenResolve(('Select Linter' as unknown) as void); - when(cmdManager.executeCommand(Commands.Set_Linter)).thenResolve(undefined); - - const response = await installer.promptToInstallImplementation(product, resource); - - verify( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).once(); - verify(cmdManager.executeCommand(Commands.Set_Linter)).once(); - expect(response).to.be.equal(InstallerResponse.Ignore); - }); - test('If install button is selected, install linter and return response', async () => { - const product = Product.pylint; - const options = ['Select Linter', "Don't show again"]; - const productName = ProductNames.get(product)!; - when( - appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1]), - ).thenResolve(('Install' as unknown) as void); - when(cmdManager.executeCommand(Commands.Set_Linter)).thenResolve(undefined); - const install = sinon.stub(LinterInstaller.prototype, 'install'); - install.resolves(InstallerResponse.Installed); - - const response = await installer.promptToInstallImplementation(product, resource); - - expect(response).to.be.equal(InstallerResponse.Installed); - assert.ok(install.calledOnceWith(product, resource, undefined)); - }); }); }); diff --git a/src/test/common/installer/productInstaller.unit.test.ts b/src/test/common/installer/productInstaller.unit.test.ts index 66e0cc005870..2934d613f88f 100644 --- a/src/test/common/installer/productInstaller.unit.test.ts +++ b/src/test/common/installer/productInstaller.unit.test.ts @@ -3,26 +3,15 @@ 'use strict'; -import * as assert from 'assert'; import { expect } from 'chai'; -import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; import { IApplicationShell } from '../../../client/common/application/types'; -import { DataScienceInstaller, FormatterInstaller } from '../../../client/common/installer/productInstaller'; -import { ProductNames } from '../../../client/common/installer/productNames'; -import { - IInstallationChannelManager, - IModuleInstaller, - InterpreterUri, - IProductPathService, - IProductService, -} from '../../../client/common/installer/types'; -import { InstallerResponse, IPersistentStateFactory, Product, ProductType } from '../../../client/common/types'; -import { Common } from '../../../client/common/utils/localize'; +import { DataScienceInstaller } from '../../../client/common/installer/productInstaller'; +import { IInstallationChannelManager, IModuleInstaller, InterpreterUri } from '../../../client/common/installer/types'; +import { InstallerResponse, Product } from '../../../client/common/types'; import { Architecture } from '../../../client/common/utils/platform'; import { IServiceContainer } from '../../../client/ioc/types'; import { EnvironmentType, ModuleInstallerType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { MockMemento } from '../../mocks/mementos'; class AlwaysInstalledDataScienceInstaller extends DataScienceInstaller { // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this @@ -89,250 +78,3 @@ suite('DataScienceInstaller install', async () => { expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); }); }); - -suite('Formatter installer', async () => { - let serviceContainer: TypeMoq.IMock; - // let outputChannel: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let persistentStateFactory: TypeMoq.IMock; - let productPathService: TypeMoq.IMock; - // let isExecutableAsModuleStub: sinon.SinonStub; - - // constructor(protected serviceContainer: IServiceContainer, protected outputChannel: OutputChannel) { - // this.appShell = serviceContainer.get(IApplicationShell); - // this.configService = serviceContainer.get(IConfigurationService); - // this.workspaceService = serviceContainer.get(IWorkspaceService); - // this.productService = serviceContainer.get(IProductService); - // this.persistentStateFactory = serviceContainer.get(IPersistentStateFactory); - // } - - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - // outputChannel = TypeMoq.Mock.ofType(); - appShell = TypeMoq.Mock.ofType(); - persistentStateFactory = TypeMoq.Mock.ofType(); - productPathService = TypeMoq.Mock.ofType(); - - const installStub = sinon.stub(FormatterInstaller.prototype, 'install'); - installStub.returns(Promise.resolve(InstallerResponse.Installed)); - - const productService = TypeMoq.Mock.ofType(); - productService.setup((p) => p.getProductType(TypeMoq.It.isAny())).returns(() => ProductType.Formatter); - - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory))) - .returns(() => persistentStateFactory.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IProductService))).returns(() => productService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductPathService), ProductType.Formatter)) - .returns(() => productPathService.object); - }); - - teardown(() => { - sinon.restore(); - }); - - // - if black not installed, offer autopep8 and yapf options - // - if autopep8 not installed, offer black and yapf options - // - if yapf not installed, offer black and autopep8 options - // - if not executable as a module, display error message - // - if never show again was set to true earlier, ignore - // if never show again is selected, ignore - - test('If black is not installed, offer autopep8 and yapf as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If autopep8 is not installed, offer black and yapf as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - - 'Use {0}'.format(ProductNames.get(Product.black)!), - 'Use {0}'.format(ProductNames.get(Product.yapf)!), - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.autopep8); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If yapf is not installed, offer autopep8 and black as options', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.black)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.yapf); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Installed); - }); - - test('If the formatter is not executable as a module, display an error message', async () => { - const messageOptions = [ - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => false) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => 'foo'); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: false, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - }); - - test('If "Do not show again" has been selected earlier, do not display the prompt', async () => { - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.bannerLabelYes)) - .verifiable(TypeMoq.Times.never()); - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value: true, - updateValue: () => Promise.resolve(), - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - assert.strictEqual(result, InstallerResponse.Ignore); - }); - - test('If "Do not show again" is selected, do not install the formatter and do not show the prompt again', async () => { - let value = false; - const messageOptions = [ - Common.bannerLabelYes, - `Use ${ProductNames.get(Product.autopep8)!}`, - `Use ${ProductNames.get(Product.yapf)!}`, - Common.doNotShowAgain, - ]; - - appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), ...messageOptions)) - .returns(() => Promise.resolve(Common.doNotShowAgain)) - .verifiable(TypeMoq.Times.once()); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - - persistentStateFactory - .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAnyString(), false)) - .returns(() => ({ - value, - updateValue: (newValue) => { - value = newValue; - return Promise.resolve(); - }, - storage: new MockMemento(), - })); - - const formatterInstaller = new FormatterInstaller(serviceContainer.object); - const result = await formatterInstaller.promptToInstall(Product.black); - const resultTwo = await formatterInstaller.promptToInstall(Product.black); - - appShell.verifyAll(); - productPathService.verifyAll(); - assert.strictEqual(result, InstallerResponse.Ignore); - assert.strictEqual(resultTwo, InstallerResponse.Ignore); - }); -}); diff --git a/src/test/common/installer/productPath.unit.test.ts b/src/test/common/installer/productPath.unit.test.ts index 1e64ca63e117..0f627289da70 100644 --- a/src/test/common/installer/productPath.unit.test.ts +++ b/src/test/common/installer/productPath.unit.test.ts @@ -26,18 +26,18 @@ import { Product, ProductType, } from '../../../client/common/types'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; import { IFormatterHelper } from '../../../client/formatters/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { ILinterInfo, ILinterManager } from '../../../client/linters/types'; import { ITestsHelper } from '../../../client/testing/common/types'; import { ITestingSettings } from '../../../client/testing/configuration/types'; +import { getProductsForInstallerTests } from '../productsToTest'; use(chaiAsPromised); suite('Product Path', () => { [undefined, Uri.file('resource')].forEach((resource) => { - getNamesAndValues(Product).forEach((product) => { + getProductsForInstallerTests().forEach((product) => { class TestBaseProductPathsService extends BaseProductPathsService { public getExecutableNameFromSettings(_: Product, _resource?: Uri): string { return ''; @@ -75,9 +75,6 @@ suite('Product Path', () => { .returns(() => new ProductService()); }); - if (product.value === Product.isort) { - return; - } suite('Method isExecutableAModule()', () => { test('Returns true if User has customized the executable name', () => { productInstaller.translateProductToModuleName = () => 'moduleName'; diff --git a/src/test/common/productsToTest.ts b/src/test/common/productsToTest.ts new file mode 100644 index 000000000000..7fc06863f67c --- /dev/null +++ b/src/test/common/productsToTest.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Product } from '../../client/common/types'; +import { getNamesAndValues } from '../../client/common/utils/enum'; + +export function getProductsForInstallerTests(): { name: string; value: Product }[] { + return getNamesAndValues(Product).filter( + (p) => + ![ + 'pylint', + 'flake8', + 'pycodestyle', + 'pylama', + 'prospector', + 'pydocstyle', + 'yapf', + 'autopep8', + 'mypy', + 'isort', + 'black', + 'bandit', + ].includes(p.name), + ); +} diff --git a/src/test/linters/lint.multilinter.test.ts b/src/test/linters/lint.multilinter.test.ts deleted file mode 100644 index dba263e78479..000000000000 --- a/src/test/linters/lint.multilinter.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as path from 'path'; -import { ConfigurationTarget, DiagnosticCollection, Uri, window, workspace } from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { ICommandManager } from '../../client/common/application/types'; -import { Product } from '../../client/common/installer/productInstaller'; -import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; -import { ExecutionResult, IPythonToolExecutionService, SpawnOptions } from '../../client/common/process/types'; -import { ExecutionInfo, IConfigurationService } from '../../client/common/types'; -import { ILinterManager } from '../../client/linters/types'; -import { deleteFile, IExtensionTestApi, PythonSettingKeys, rootWorkspaceUri } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; - -const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); -const pythonFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'linting'); - -// Mocked out python tool execution (all we need is mocked linter return values). -class MockPythonToolExecService extends PythonToolExecutionService { - // Mocked samples of linter messages from flake8 and pylint: - public flake8Msg = - '1,1,W,W391:blank line at end of file\ns:142:13), :1\n1,7,E,E999:SyntaxError: invalid syntax\n'; - - public pylintMsg = `[ - { - "type": "error", - "module": "print", - "obj": "", - "line": 1, - "column": 0, - "path": "print.py", - "symbol": "syntax-error", - "message": "Missing parentheses in call to 'print'. Did you mean print(x)? (, line 1)", - "message-id": "E0001" - } -]`; - - // Depending on moduleName being exec'd, return the appropriate sample. - public async execForLinter( - executionInfo: ExecutionInfo, - _options: SpawnOptions, - _resource: Uri, - ): Promise> { - let msg = this.flake8Msg; - if (executionInfo.moduleName === 'pylint') { - msg = this.pylintMsg; - } - return { stdout: msg }; - } -} - -suite('Linting - Multiple Linters Enabled Test', () => { - let api: IExtensionTestApi; - let configService: IConfigurationService; - let linterManager: ILinterManager; - - suiteSetup(async () => { - api = await initialize(); - configService = api.serviceContainer.get(IConfigurationService); - linterManager = api.serviceContainer.get(ILinterManager); - }); - setup(async () => { - await initializeTest(); - await resetSettings(); - - // We only want to return some valid strings from linters, we don't care if they - // are being returned by actual linters (we aren't testing linters here, only how - // our code responds to those linters). - api.serviceManager.rebind(IPythonToolExecutionService, MockPythonToolExecService); - }); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await closeActiveWindows(); - await resetSettings(); - await deleteFile(path.join(workspaceUri.fsPath, '.pylintrc')); - await deleteFile(path.join(workspaceUri.fsPath, '.pydocstyle')); - - // Restore the execution service as it was... - api.serviceManager.rebind(IPythonToolExecutionService, PythonToolExecutionService); - }); - - async function resetSettings() { - // Don't run these updates in parallel, as they are updating the same file. - const target = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - - await configService.updateSetting('linting.enabled', true, rootWorkspaceUri, target); - await configService.updateSetting('linting.lintOnSave', false, rootWorkspaceUri, target); - - linterManager.getAllLinterInfos().forEach(async (x) => { - await configService.updateSetting(makeSettingKey(x.product), false, rootWorkspaceUri, target); - }); - } - - function makeSettingKey(product: Product): PythonSettingKeys { - return `linting.${linterManager.getLinterInfo(product).enabledSettingName}` as PythonSettingKeys; - } - - test('Multiple linters', async () => { - await closeActiveWindows(); - const document = await workspace.openTextDocument(path.join(pythonFilesPath, 'print.py')); - await window.showTextDocument(document); - await configService.updateSetting( - 'languageServer', - LanguageServerType.Jedi, - undefined, - ConfigurationTarget.Workspace, - ); - await configService.updateSetting('linting.enabled', true, workspaceUri); - await configService.updateSetting('linting.pylintEnabled', true, workspaceUri); - await configService.updateSetting('linting.flake8Enabled', true, workspaceUri); - - const commands = api.serviceContainer.get(ICommandManager); - - const collection = (await commands.executeCommand('python.runLinting')) as DiagnosticCollection; - assert.notStrictEqual(collection, undefined, 'python.runLinting did not return valid diagnostics collection.'); - - const messages = collection!.get(document.uri); - assert.notStrictEqual(messages!.length, 0, 'No diagnostic messages.'); - assert.notStrictEqual(messages!.filter((x) => x.source === 'pylint').length, 0, 'No pylint messages.'); - assert.notStrictEqual(messages!.filter((x) => x.source === 'flake8').length, 0, 'No flake8 messages.'); - }); -}); diff --git a/src/test/linters/linterCommands.unit.test.ts b/src/test/linters/linterCommands.unit.test.ts deleted file mode 100644 index b3d5c4693832..000000000000 --- a/src/test/linters/linterCommands.unit.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; -import { DiagnosticCollection } from 'vscode'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { CommandManager } from '../../client/common/application/commandManager'; -import { DocumentManager } from '../../client/common/application/documentManager'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../../client/common/application/types'; -import { Commands } from '../../client/common/constants'; -import { Product } from '../../client/common/types'; -import { ServiceContainer } from '../../client/ioc/container'; -import { LinterCommands } from '../../client/linters/linterCommands'; -import { LinterManager } from '../../client/linters/linterManager'; -import { LintingEngine } from '../../client/linters/lintingEngine'; -import { ILinterInfo, ILinterManager, ILintingEngine } from '../../client/linters/types'; - -suite('Linting - Linter Commands', () => { - let linterCommands: LinterCommands; - let manager: ILinterManager; - let shell: IApplicationShell; - let docManager: IDocumentManager; - let cmdManager: ICommandManager; - let lintingEngine: ILintingEngine; - setup(() => { - const svcContainer = mock(ServiceContainer); - manager = mock(LinterManager); - shell = mock(ApplicationShell); - docManager = mock(DocumentManager); - cmdManager = mock(CommandManager); - lintingEngine = mock(LintingEngine); - when(svcContainer.get(ILinterManager)).thenReturn(instance(manager)); - when(svcContainer.get(IApplicationShell)).thenReturn(instance(shell)); - when(svcContainer.get(IDocumentManager)).thenReturn(instance(docManager)); - when(svcContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - when(svcContainer.get(ILintingEngine)).thenReturn(instance(lintingEngine)); - linterCommands = new LinterCommands(instance(svcContainer)); - }); - - test('Commands are registered', () => { - verify(cmdManager.registerCommand(Commands.Set_Linter, anything())).once(); - verify(cmdManager.registerCommand(Commands.Enable_Linter, anything())).once(); - verify(cmdManager.registerCommand(Commands.Run_Linter, anything())).once(); - }); - - test('Run Linting method will lint all open files', async () => { - when(lintingEngine.lintOpenPythonFiles('manual')).thenResolve(('Hello' as unknown) as DiagnosticCollection); - - const result = await linterCommands.runLinting(); - - expect(result).to.be.equal('Hello'); - }); - - async function testEnableLintingWithCurrentState( - currentState: boolean, - selectedState: 'Enable' | 'Disable' | undefined, - ) { - when(manager.isLintingEnabled(anything())).thenResolve(currentState); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${currentState ? 'Enable' : 'Disable'}`, - }; - when(shell.showQuickPick(anything(), anything())).thenReturn(Promise.resolve(selectedState)); - - await linterCommands.enableLintingAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - const options = capture(shell.showQuickPick).last()[0]; - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(options).to.deep.equal(['Enable', 'Disable']); - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - - if (selectedState) { - verify(manager.enableLintingAsync(selectedState === 'Enable', anything())).once(); - } else { - verify(manager.enableLintingAsync(anything(), anything())).never(); - } - } - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select nothing", async () => { - await testEnableLintingWithCurrentState(true, undefined); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select 'Enable'", async () => { - await testEnableLintingWithCurrentState(true, 'Enable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Enable' and select 'Disable'", async () => { - await testEnableLintingWithCurrentState(true, 'Disable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Disable' and select 'Enable'", async () => { - await testEnableLintingWithCurrentState(true, 'Enable'); - }); - - test("Enable linting should check if linting is enabled, and display current state of 'Disable' and select 'Disable'", async () => { - await testEnableLintingWithCurrentState(true, 'Disable'); - }); - - test('Set Linter should display a quickpick', async () => { - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(anything())).thenResolve([]); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: none', - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - - test('Set Linter should display a quickpick and currently active linter when only one is enabled', async () => { - const linterId = 'Hello World'; - const activeLinters: ILinterInfo[] = [({ id: linterId } as unknown) as ILinterInfo]; - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${linterId}`, - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - - test('Set Linter should display a quickpick and with message about multiple linters being enabled', async () => { - const activeLinters: ILinterInfo[] = ([{ id: 'linterId' }, { id: 'linterId2' }] as unknown) as ILinterInfo[]; - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: multiple selected', - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - - test('Selecting a linter should display warning message about multiple linters', async () => { - const linters: ILinterInfo[] = ([ - { id: '1' }, - { id: '2' }, - { id: '3', product: 'Three' }, - ] as unknown) as ILinterInfo[]; - const activeLinters: ILinterInfo[] = ([{ id: '1' }, { id: '3' }] as unknown) as ILinterInfo[]; - when(manager.getAllLinterInfos()).thenReturn(linters); - when(manager.getActiveLinters(anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenReturn(Promise.resolve('3')); - when(shell.showWarningMessage(anything(), 'Yes', 'No')).thenReturn(Promise.resolve('Yes')); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: multiple selected', - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - verify(shell.showWarningMessage(anything(), 'Yes', 'No')).once(); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - verify(manager.setActiveLintersAsync(deepEqual([('Three' as unknown) as Product]), anything())).once(); - }); -}); From f2600075d40eba14d30bbdfd4a1a41e53f5511d8 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 19 Sep 2023 11:00:34 -0700 Subject: [PATCH 0195/1136] Fix duplicate environments showing up on macOS (#22030) Closes https://github.com/microsoft/vscode-python/issues/22006 --- .../base/locators/lowLevel/globalVirtualEnvronmentLocator.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts index d86b2182d50c..71ac3bca8eef 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts @@ -40,7 +40,8 @@ async function getGlobalVirtualEnvDirs(): Promise { const homeDir = getUserHomeDir(); if (homeDir && (await pathExists(homeDir))) { const subDirs = ['Envs', '.direnv', '.venvs', '.virtualenvs', path.join('.local', 'share', 'virtualenvs')]; - if (getOSType() !== OSType.Windows) { + if (![OSType.Windows, OSType.OSX].includes(getOSType())) { + // Not a case insensitive platform, push both upper and lower cases. subDirs.push('envs'); } const filtered = await asyncFilter( From ae81fcbab9b2a3cd9346bfde5adc32e0d1eaf419 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 19 Sep 2023 13:30:44 -0700 Subject: [PATCH 0196/1136] handle exceptions during test discovery (#22026) closes https://github.com/microsoft/vscode-python/issues/21999 and https://github.com/microsoft/vscode-python/issues/21826 --- .../testController/common/resultResolver.ts | 5 + .../testing/testController/common/server.ts | 27 +++- .../testing/testController/common/types.ts | 1 + .../testing/testController/common/utils.ts | 15 +++ .../pytest/pytestDiscoveryAdapter.ts | 11 ++ .../testing/common/testingAdapter.test.ts | 117 +++++++++++++++++- .../test_seg_fault_discovery.py | 16 +++ 7 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 5ef6695ca280..78883bdcf8fb 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -92,7 +92,12 @@ export class PythonResultResolver implements ITestResultResolver { populateTestTree(this.testController, rawTestData.tests, undefined, this, token); } else { // Delete everything from the test controller. + const errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`); this.testController.items.replace([]); + // Add back the error node if it exists. + if (errorNode !== undefined) { + this.testController.items.add(errorNode); + } } sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index f59c486f7a85..699f7f754122 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -15,7 +15,12 @@ import { traceError, traceInfo, traceLog } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; -import { createEOTPayload, createExecutionErrorPayload, extractJsonPayload } from './utils'; +import { + createDiscoveryErrorPayload, + createEOTPayload, + createExecutionErrorPayload, + extractJsonPayload, +} from './utils'; import { createDeferred } from '../../../common/utils/async'; export class PythonTestServer implements ITestServer, Disposable { @@ -144,6 +149,10 @@ export class PythonTestServer implements ITestServer, Disposable { this._onRunDataReceived.fire(payload); } + public triggerDiscoveryDataReceivedEvent(payload: DataReceivedEvent): void { + this._onDiscoveryDataReceived.fire(payload); + } + public dispose(): void { this.server.close(); this._onDataReceived.dispose(); @@ -231,7 +240,7 @@ export class PythonTestServer implements ITestServer, Disposable { }); result?.proc?.on('exit', (code, signal) => { // if the child has testIds then this is a run request - if (code !== 0 && testIds) { + if (code !== 0 && testIds && testIds?.length !== 0) { traceError( `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, ); @@ -245,6 +254,20 @@ export class PythonTestServer implements ITestServer, Disposable { uuid, data: JSON.stringify(createEOTPayload(true)), }); + } else if (code !== 0) { + // This occurs when we are running discovery + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, + ); + this._onDiscoveryDataReceived.fire({ + uuid, + data: JSON.stringify(createDiscoveryErrorPayload(code, signal, options.cwd)), + }); + // then send a EOT payload + this._onDiscoveryDataReceived.fire({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); } deferred.resolve({ stdout: '', stderr: '' }); }); diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 386c397b310c..32e0c4ba8cc6 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -187,6 +187,7 @@ export interface ITestServer { createUUID(cwd: string): string; deleteUUID(uuid: string): void; triggerRunDataReceivedEvent(data: DataReceivedEvent): void; + triggerDiscoveryDataReceivedEvent(data: DataReceivedEvent): void; } export interface ITestResultResolver { runIdToVSid: Map; diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 572863ecdbfe..a854c2780a75 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -12,6 +12,7 @@ import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilit import { DiscoveredTestItem, DiscoveredTestNode, + DiscoveredTestPayload, EOTTestPayload, ExecutionTestPayload, ITestResultResolver, @@ -299,6 +300,20 @@ export function createExecutionErrorPayload( return etp; } +export function createDiscoveryErrorPayload( + code: number | null, + signal: NodeJS.Signals | null, + cwd: string, +): DiscoveredTestPayload { + return { + cwd, + status: 'error', + error: [ + ` \n The python test process was terminated before it could exit on its own, the process errored with: Code: ${code}, Signal: ${signal}`, + ], + }; +} + export function createEOTPayload(executionBool: boolean): EOTTestPayload { return { commandType: executionBool ? 'execution' : 'discovery', diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index bafa91847b42..c03baeae0421 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -19,6 +19,7 @@ import { ITestResultResolver, ITestServer, } from '../common/types'; +import { createDiscoveryErrorPayload, createEOTPayload } from '../common/utils'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied @@ -100,6 +101,16 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { traceError( `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, ); + // if the child process exited with a non-zero exit code, then we need to send the error payload. + this.testServer.triggerDiscoveryDataReceivedEvent({ + uuid, + data: JSON.stringify(createDiscoveryErrorPayload(code, signal, cwd)), + }); + // then send a EOT payload + this.testServer.triggerDiscoveryDataReceivedEvent({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); } deferredExec.resolve({ stdout: '', stderr: '' }); deferred.resolve(); diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index b445772d6958..4f46f1cf738c 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -51,6 +51,12 @@ suite('End to End Tests: test adapters', () => { 'testTestingRootWkspc', 'errorWorkspace', ); + const rootPathDiscoveryErrorWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'discoveryErrorWorkspace', + ); suiteSetup(async () => { serviceContainer = (await initialize()).serviceContainer; }); @@ -477,6 +483,115 @@ suite('End to End Tests: test adapters', () => { assert.strictEqual(failureOccurred, false, failureMsg); }); }); + test('unittest discovery adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveDiscovery = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`unittest discovery adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); + } else { + assert.ok(data.error, "Expected errors in 'error' field"); + } + } else { + const indexOfTest = JSON.stringify(data.tests).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); + + const discoveryAdapter = new UnittestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await discoveryAdapter.discoverTests(workspaceUri).finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + test('pytest discovery seg fault error handling', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveDiscovery = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`add one to call count, is now ${callCount}`); + traceLog(`pytest discovery adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); + } else { + assert.ok(data.error, "Expected errors in 'error' field"); + } + } else { + const indexOfTest = JSON.stringify(data.tests).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter( + pythonTestServer, + configService, + testOutputChannel.object, + resultResolver, + ); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + assert.ok( + callCount >= 1, + `Expected _resolveDiscovery to be called at least once, call count was instead ${callCount}`, + ); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); test('unittest execution adapter seg fault error handling', async () => { resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); let callCount = 0; @@ -485,7 +600,7 @@ suite('End to End Tests: test adapters', () => { resultResolver._resolveExecution = async (data, _token?) => { // do the following asserts for each time resolveExecution is called, should be called once per test. callCount = callCount + 1; - console.log(`unittest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); + traceLog(`unittest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); try { if (data.status === 'error') { if (data.error === undefined) { diff --git a/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py b/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py new file mode 100644 index 000000000000..5aac911b575a --- /dev/null +++ b/src/testTestingRootWkspc/discoveryErrorWorkspace/test_seg_fault_discovery.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +import ctypes + +ctypes.string_at(0) # Dereference a NULL pointer + + +class TestSegmentationFault(unittest.TestCase): + def test_segfault(self): + assert True + + +if __name__ == "__main__": + unittest.main() From f38ea44affa7ab62d7a17bb4d1835dba5bab71bf Mon Sep 17 00:00:00 2001 From: Kyle Gottfried Date: Wed, 20 Sep 2023 14:33:21 -0400 Subject: [PATCH 0197/1136] Update language to encourage reading "Migration to Python Tools Extensions" (#22019) There are other formatter options besides Black and Autopep8, but the language of this notice suggests otherwise. --- package.nls.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.nls.json b/package.nls.json index d260c98b0b5a..08f3a136584f 100644 --- a/package.nls.json +++ b/package.nls.json @@ -54,14 +54,14 @@ "python.formatting.blackPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", "python.formatting.blackPath.deprecationMessage": "This setting will soon be deprecated. Please use the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.provider.description": "Provider for formatting. Possible options include 'autopep8', 'black', and 'yapf'.", - "python.formatting.provider.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8) or the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.provider.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension or the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", + "python.formatting.provider.markdownDeprecationMessage": "This setting will soon be deprecated. Please use a dedicated formatter extension.
Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.provider.deprecationMessage": "This setting will soon be deprecated. Please use a dedicated formatter extension. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.yapfArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.yapfArgs.markdownDeprecationMessage": "Yapf support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.yapfArgs.deprecationMessage": "Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", + "python.formatting.yapfArgs.markdownDeprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", + "python.formatting.yapfArgs.deprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.formatting.yapfPath.description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", "python.formatting.yapfPath.markdownDeprecationMessage": "Yapf support will soon be deprecated.
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.yapfPath.deprecationMessage": "Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", + "python.formatting.yapfPath.deprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.", "python.languageServer.description": "Defines type of the language server.", "python.languageServer.defaultDescription": "Automatically select a language server: Pylance if installed and available, otherwise fallback to Jedi.", From 849be34d4772086ae7bfef6986c7aef36887f7a5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 21 Sep 2023 01:10:15 -0700 Subject: [PATCH 0198/1136] De-duplicate directories at the very end in Global virtual env locators (#22040) --- .../lowLevel/globalVirtualEnvronmentLocator.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts index 71ac3bca8eef..71f3d69e9067 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { uniq } from 'lodash'; +import { toLower, uniq, uniqBy } from 'lodash'; import * as path from 'path'; import { chain, iterable } from '../../../../common/utils/async'; import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../../common/utils/platform'; @@ -39,11 +39,14 @@ async function getGlobalVirtualEnvDirs(): Promise { const homeDir = getUserHomeDir(); if (homeDir && (await pathExists(homeDir))) { - const subDirs = ['Envs', '.direnv', '.venvs', '.virtualenvs', path.join('.local', 'share', 'virtualenvs')]; - if (![OSType.Windows, OSType.OSX].includes(getOSType())) { - // Not a case insensitive platform, push both upper and lower cases. - subDirs.push('envs'); - } + const subDirs = [ + 'envs', + 'Envs', + '.direnv', + '.venvs', + '.virtualenvs', + path.join('.local', 'share', 'virtualenvs'), + ]; const filtered = await asyncFilter( subDirs.map((d) => path.join(homeDir, d)), pathExists, @@ -51,7 +54,7 @@ async function getGlobalVirtualEnvDirs(): Promise { filtered.forEach((d) => venvDirs.push(d)); } - return uniq(venvDirs); + return [OSType.Windows, OSType.OSX].includes(getOSType()) ? uniqBy(venvDirs, toLower) : uniq(venvDirs); } /** From 42cdaf302d5c5bc5db341aba06539ae1c6ef1670 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 21 Sep 2023 13:37:00 -0700 Subject: [PATCH 0199/1136] Make sure `PATH` ends with a separator before prepending (#22046) Introduced with https://github.com/microsoft/vscode-python/pull/21906 For #20950 Fixes https://github.com/microsoft/vscode-python/issues/22047 --- .../activation/terminalEnvVarCollectionService.ts | 9 ++++++++- .../terminalEnvVarCollectionService.unit.test.ts | 5 +++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 9bc95ee6d2e3..660dbdece257 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -63,6 +63,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ */ private processEnvVars: EnvironmentVariables | undefined; + private separator: string; + constructor( @inject(IPlatformService) private readonly platform: IPlatformService, @inject(IInterpreterService) private interpreterService: IInterpreterService, @@ -75,7 +77,9 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, @inject(IPathUtils) private readonly pathUtils: IPathUtils, - ) {} + ) { + this.separator = platform.osType === OSType.Windows ? ';' : ':'; + } public async activate(resource: Resource): Promise { try { @@ -196,6 +200,9 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ applyAtProcessCreation: true, }); } else { + if (!value.endsWith(this.separator)) { + value = value.concat(this.separator); + } traceVerbose(`Prepending environment variable ${key} in collection to ${value}`); envVarCollection.prepend(key, value, { applyAtShellIntegration: true, diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index cedc2701112f..3ebcea376045 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -248,12 +248,13 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); }); - test('Prepend full PATH otherwise', async () => { + test('Prepend full PATH with separator otherwise', async () => { const processEnv = { PATH: 'hello/1/2/3' }; reset(environmentActivationService); when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( processEnv, ); + const separator = getOSType() === OSType.Windows ? ';' : ':'; const finalPath = 'hello/3/2/1'; const envVars: NodeJS.ProcessEnv = { PATH: finalPath }; when( @@ -275,7 +276,7 @@ suite('Terminal Environment Variable Collection Service', () => { await terminalEnvVarCollectionService._applyCollection(undefined, customShell); verify(collection.clear()).once(); - verify(collection.prepend('PATH', finalPath, anything())).once(); + verify(collection.prepend('PATH', `${finalPath}${separator}`, anything())).once(); verify(collection.replace('PATH', anything(), anything())).never(); assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); }); From 00b198af9cc41a674243958ee4290de71968130a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 21 Sep 2023 14:24:23 -0700 Subject: [PATCH 0200/1136] Fix bugs related to discovery blocking other features (#22041) For #21755 --- src/client/common/utils/multiStepInput.ts | 10 ++++-- src/client/interpreter/autoSelection/index.ts | 13 +++++++- .../commands/setInterpreter.ts | 11 ++++--- .../composite/envsCollectionService.ts | 4 +-- .../commands/setInterpreter.unit.test.ts | 31 +++++++++++++++---- 5 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/client/common/utils/multiStepInput.ts b/src/client/common/utils/multiStepInput.ts index e44879e8bbbb..e2b2567b5b4e 100644 --- a/src/client/common/utils/multiStepInput.ts +++ b/src/client/common/utils/multiStepInput.ts @@ -47,7 +47,7 @@ export interface IQuickPickParameters { totalSteps?: number; canGoBack?: boolean; items: T[]; - activeItem?: T | Promise; + activeItem?: T | ((quickPick: QuickPick) => Promise); placeholder: string | undefined; customButtonSetups?: QuickInputButtonSetup[]; matchOnDescription?: boolean; @@ -156,7 +156,13 @@ export class MultiStepInput implements IMultiStepInput { initialize(input); } if (activeItem) { - input.activeItems = [await activeItem]; + if (typeof activeItem === 'function') { + activeItem(input).then((item) => { + if (input.activeItems.length === 0) { + input.activeItems = [item]; + } + }); + } } else { input.activeItems = []; } diff --git a/src/client/interpreter/autoSelection/index.ts b/src/client/interpreter/autoSelection/index.ts index a57577c8c918..7714c487ed30 100644 --- a/src/client/interpreter/autoSelection/index.ts +++ b/src/client/interpreter/autoSelection/index.ts @@ -181,6 +181,11 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio return this.stateFactory.createWorkspacePersistentState(key, undefined); } + private getAutoSelectionQueriedOnceState(): IPersistentState { + const key = `autoSelectionInterpretersQueriedOnce`; + return this.stateFactory.createWorkspacePersistentState(key, undefined); + } + /** * Auto-selection logic: * 1. If there are cached interpreters (not the first session in this workspace) @@ -200,7 +205,12 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio }); } - await this.interpreterService.refreshPromise; + const globalQueriedState = this.getAutoSelectionQueriedOnceState(); + if (!globalQueriedState.value) { + // Global interpreters are loaded the first time an extension loads, after which we don't need to + // wait on global interpreter promise refresh. + await this.interpreterService.refreshPromise; + } const interpreters = this.interpreterService.getInterpreters(resource); const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); @@ -215,6 +225,7 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio } queriedState.updateValue(true); + globalQueriedState.updateValue(true); this.didAutoSelectedInterpreterEmitter.fire(); } diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index c0876ff518dd..9b8ecec74f9f 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -50,7 +50,7 @@ import { BaseInterpreterSelectorCommand } from './base'; const untildify = require('untildify'); export type InterpreterStateArgs = { path?: string; workspace: Resource }; -type QuickPickType = IInterpreterQuickPickItem | ISpecialQuickPickItem | QuickPickItem; +export type QuickPickType = IInterpreterQuickPickItem | ISpecialQuickPickItem | QuickPickItem; function isInterpreterQuickPickItem(item: QuickPickType): item is IInterpreterQuickPickItem { return 'interpreter' in item; @@ -177,7 +177,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem items: suggestions, sortByLabel: !preserveOrderWhenFiltering, keepScrollPosition: true, - activeItem: this.getActiveItem(state.workspace, suggestions), // Use a promise here to ensure quickpick is initialized synchronously. + activeItem: (quickPick) => this.getActiveItem(state.workspace, quickPick), // Use a promise here to ensure quickpick is initialized synchronously. matchOnDetail: true, matchOnDescription: true, title, @@ -277,8 +277,9 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem return getGroupedQuickPickItems(items, recommended, workspaceFolder?.uri.fsPath); } - private async getActiveItem(resource: Resource, suggestions: QuickPickType[]) { + private async getActiveItem(resource: Resource, quickPick: QuickPick) { const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const suggestions = quickPick.items; const activeInterpreterItem = suggestions.find( (i) => isInterpreterQuickPickItem(i) && i.interpreter.id === interpreter?.id, ); @@ -339,7 +340,9 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem return false; }) : undefined; - quickPick.activeItems = activeItem ? [activeItem] : []; + if (activeItem) { + quickPick.activeItems = [activeItem]; + } } /** diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts index 2567168c6325..fb1a791d07ed 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts @@ -115,9 +115,9 @@ export class EnvsCollectionService extends PythonEnvsWatcher this.sendTelemetry(query, stopWatch)); } - return refreshPromise.then(() => this.sendTelemetry(query, stopWatch)); + return refreshPromise; } private startRefresh(query: PythonLocatorQuery | undefined): Promise { diff --git a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts index 7059fb7ab26f..f177db5c2a32 100644 --- a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts +++ b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -31,6 +31,7 @@ import { import { EnvGroups, InterpreterStateArgs, + QuickPickType, SetInterpreterCommand, } from '../../../../client/interpreter/configuration/interpreterSelector/commands/setInterpreter'; import { @@ -265,8 +266,14 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, recommended); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); @@ -308,8 +315,14 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, noPythonInstalled); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, noPythonInstalled); + } else { + assert(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); @@ -666,8 +679,14 @@ suite('Set Interpreter Command', () => { delete actualParameters!.initialize; delete actualParameters!.customButtonSetups; delete actualParameters!.onChangeItem; - const activeItem = await actualParameters!.activeItem; - assert.deepStrictEqual(activeItem, recommended); + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert(false, 'Not a function'); + } delete actualParameters!.activeItem; assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); From 242a333787db32066fc31579fc1bb2b1475e597a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 21 Sep 2023 18:58:55 -0700 Subject: [PATCH 0201/1136] Respect `VIRTUAL_ENV_DISABLE_PROMPT` when activating virtual envs (#22053) --- .../terminalEnvVarCollectionService.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 660dbdece257..489dd1b7c6eb 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -37,6 +37,7 @@ import { EnvironmentVariables } from '../../common/variables/types'; import { TerminalShellType } from '../../common/terminal/types'; import { OSType } from '../../common/utils/platform'; import { normCase } from '../../common/platform/fs-paths'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { @@ -264,8 +265,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (this.platform.osType !== OSType.Windows) { // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. const interpreter = await this.interpreterService.getActiveInterpreter(resource); - const shouldPS1BeSet = interpreter?.type !== undefined; - if (shouldPS1BeSet && !env.PS1) { + const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); + if (shouldSetPS1 && !env.PS1) { // PS1 should be set but no PS1 was set. return; } @@ -291,8 +292,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (this.platform.osType !== OSType.Windows) { // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. const interpreter = await this.interpreterService.getActiveInterpreter(resource); - const shouldPS1BeSet = interpreter?.type !== undefined; - if (shouldPS1BeSet && !env.PS1) { + const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); + if (shouldSetPS1 && !env.PS1) { // PS1 should be set but no PS1 was set. return getPromptForEnv(interpreter); } @@ -367,6 +368,15 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } +function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariables): boolean { + if (type === PythonEnvType.Virtual) { + const promptDisabledVar = env.VIRTUAL_ENV_DISABLE_PROMPT; + const isPromptDisabled = promptDisabledVar && promptDisabledVar !== undefined; + return !isPromptDisabled; + } + return type !== undefined; +} + function shouldSkip(env: string) { return ['_', 'SHLVL'].includes(env); } From 7693fcb3aeb08d5e3d6c6d66afda70dc1c99188a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 22 Sep 2023 00:02:43 -0700 Subject: [PATCH 0202/1136] Respect conda changeps1 config when setting PS1 (#22054) For https://github.com/microsoft/vscode-python/issues/22048 --- .../terminalEnvVarCollectionService.ts | 7 ++ ...rminalEnvVarCollectionService.unit.test.ts | 85 ++++++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 489dd1b7c6eb..50a15d25f3de 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -374,6 +374,13 @@ function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariabl const isPromptDisabled = promptDisabledVar && promptDisabledVar !== undefined; return !isPromptDisabled; } + if (type === PythonEnvType.Conda) { + // Instead of checking config value using `conda config --get changeps1`, simply check + // `CONDA_PROMPT_MODIFER` to avoid the cost of launching the conda binary. + const promptEnabledVar = env.CONDA_PROMPT_MODIFIER; + const isPromptEnabled = promptEnabledVar && promptEnabledVar !== ''; + return !!isPromptEnabled; + } return type !== undefined; } diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 3ebcea376045..091986b7e18b 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -216,6 +216,85 @@ suite('Terminal Environment Variable Collection Service', () => { assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); + test('Respect VIRTUAL_ENV_DISABLE_PROMPT when setting PS1 for venv', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + VIRTUAL_BIN: 'prefix/to/conda', + ...process.env, + VIRTUAL_ENV_DISABLE_PROMPT: '1', + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + when(collection.prepend('PS1', anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.prepend('PS1', anything(), anything())).never(); + }); + + test('Otherwise set PS1 for venv even if PS1 is not returned', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + VIRTUAL_BIN: 'prefix/to/conda', + ...process.env, + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + when(collection.prepend('PS1', '(envName) ', anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.prepend('PS1', '(envName) ', anything())).once(); + }); + + test('Respect CONDA_PROMPT_MODIFIER when setting PS1 for conda', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + CONDA_PROMPT_MODIFIER: '(envName)', + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PS1', '(envName) ', anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); + }); + test('Prepend only "prepend portion of PATH" where applicable', async () => { const processEnv = { PATH: 'hello/1/2/3' }; reset(environmentActivationService); @@ -416,7 +495,11 @@ suite('Terminal Environment Variable Collection Service', () => { test('Correct track that prompt was not set for non-Windows where PS1 is not set but env name is base', async () => { when(platform.osType).thenReturn(OSType.Linux); - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + CONDA_PROMPT_MODIFIER: '(base)', + }; const ps1Shell = 'zsh'; const resource = Uri.file('a'); const workspaceFolder: WorkspaceFolder = { From 4ed3fa06b6a7e45b29c736c82c81821c2e1df330 Mon Sep 17 00:00:00 2001 From: Anna Burlyaeva Date: Fri, 22 Sep 2023 12:43:25 -0700 Subject: [PATCH 0203/1136] =?UTF-8?q?Changed=20order=20of=20options=20in?= =?UTF-8?q?=20Create=20Environment=20flow=20when=20.venv=20exists=E2=80=A6?= =?UTF-8?q?=20(#22055)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes https://github.com/microsoft/vscode-python/issues/22038 --- src/client/pythonEnvironments/creation/provider/venvUtils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/src/client/pythonEnvironments/creation/provider/venvUtils.ts index 723337b2a7fa..f0828be8d1f7 100644 --- a/src/client/pythonEnvironments/creation/provider/venvUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -292,11 +292,14 @@ export async function pickExistingVenvAction( if (workspaceFolder) { if (await hasVenv(workspaceFolder)) { const items: QuickPickItem[] = [ - { label: CreateEnv.Venv.recreate, description: CreateEnv.Venv.recreateDescription }, { label: CreateEnv.Venv.useExisting, description: CreateEnv.Venv.useExistingDescription, }, + { + label: CreateEnv.Venv.recreate, + description: CreateEnv.Venv.recreateDescription, + }, ]; const selection = (await showQuickPickWithBack( From dfc939b87db1466aae7334f6d21797fecaed78b7 Mon Sep 17 00:00:00 2001 From: Ludi Zhan Date: Fri, 22 Sep 2023 13:16:20 -0700 Subject: [PATCH 0204/1136] Remove sort imports from command palette and context menu (#22058) Fixes https://github.com/microsoft/vscode-python/issues/20233 --- package.json | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/package.json b/package.json index 9cf564122d7b..57a2b34f7c03 100644 --- a/package.json +++ b/package.json @@ -430,11 +430,6 @@ "command": "python.setInterpreter", "title": "%python.command.python.setInterpreter.title%" }, - { - "category": "Python Refactor", - "command": "python.sortImports", - "title": "%python.command.python.sortImports.title%" - }, { "category": "Python", "command": "python.startREPL", @@ -1870,12 +1865,6 @@ "title": "%python.command.python.setInterpreter.title%", "when": "!virtualWorkspace && shellExecutionSupported" }, - { - "category": "Python Refactor", - "command": "python.sortImports", - "title": "%python.command.python.sortImports.title%", - "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" - }, { "category": "Python", "command": "python.startREPL", @@ -1914,12 +1903,6 @@ "group": "Python", "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && isWorkspaceTrusted" }, - { - "command": "python.sortImports", - "group": "Refactor", - "title": "%python.command.python.sortImports.title%", - "when": "editorLangId == python && !notebookEditorFocused && !virtualWorkspace && shellExecutionSupported" - }, { "submenu": "python.runFileInteractive", "group": "Jupyter2", From 3b6c47b4816eff2de44f5a99892b3a8bdf7f2535 Mon Sep 17 00:00:00 2001 From: Himani Madan Date: Fri, 22 Sep 2023 18:55:39 -0400 Subject: [PATCH 0205/1136] Pytest to pytest (#22062) --- src/client/common/utils/localize.ts | 2 +- src/client/testing/testController/common/utils.ts | 2 +- src/client/testing/testController/pytest/pytestController.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 05b525bdf5bf..a785ab4bc75a 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -415,7 +415,7 @@ export namespace Testing { export const cancelUnittestExecution = l10n.t('Canceled unittest test execution'); export const errorUnittestExecution = l10n.t('Unittest test execution error'); export const cancelPytestExecution = l10n.t('Canceled pytest test execution'); - export const errorPytestExecution = l10n.t('Pytest test execution error'); + export const errorPytestExecution = l10n.t('pytest test execution error'); } export namespace OutdatedDebugger { diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index a854c2780a75..dc254a9900a1 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -211,7 +211,7 @@ export async function startTestIdServer(testIds: string[]): Promise { } export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { - const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error'; + const labelText = testType === 'pytest' ? 'pytest Discovery Error' : 'Unittest Discovery Error'; return { id: `DiscoveryError:${uri.fsPath}`, label: `${labelText} [${path.basename(uri.fsPath)}]`, diff --git a/src/client/testing/testController/pytest/pytestController.ts b/src/client/testing/testController/pytest/pytestController.ts index 997e3e29b7ec..d23cac842cda 100644 --- a/src/client/testing/testController/pytest/pytestController.ts +++ b/src/client/testing/testController/pytest/pytestController.ts @@ -235,7 +235,7 @@ export class PytestController implements ITestFrameworkController { testController.items.add( createErrorTestItem(testController, { id: `DiscoveryError:${workspace.uri.fsPath}`, - label: `Pytest Discovery Error [${path.basename(workspace.uri.fsPath)}]`, + label: `pytest Discovery Error [${path.basename(workspace.uri.fsPath)}]`, error: util.format( `${cancel} discovering pytest tests (see Output > Python):\r\n`, message.length > 0 ? message : ex, From 337b8626c80b0d066e9865e07b9e520771637945 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 25 Sep 2023 11:11:54 -0700 Subject: [PATCH 0206/1136] Show a prompt asking users if they want to create environment (#22071) Criteria for showing prompts: 1. It has to be a workspace or multiroot workspace. 2. The workspace or workspace folder should not have ".venv" or ".conda" environments. 3. The selected python should be a global python, i.e., there is no workspace specific environment selected. 4. The workspace should **not** have any `pipfile`, `poetry.lock` etc. 5. The workspace should have files that match `*requirements*.txt` or `requirements/*.txt` pattern. There is a setting to enable this behavior: `python.createEnvironment.trigger` and default is `off` closes https://github.com/microsoft/vscode-python/issues/21965 --- package.json | 13 + package.nls.json | 1 + src/client/common/constants.ts | 1 + src/client/common/persistentState.ts | 45 ++- src/client/common/utils/localize.ts | 9 + .../configuration/resolvers/launch.ts | 8 + .../debugger/extension/debugCommands.ts | 6 + src/client/extensionActivation.ts | 4 + .../creation/common/createEnvTriggerUtils.ts | 113 +++++++ .../creation/createEnvironmentTrigger.ts | 160 ++++++++++ .../creation/provider/venvUtils.ts | 2 +- src/client/telemetry/constants.ts | 3 + src/client/telemetry/index.ts | 28 ++ .../codeExecution/codeExecutionManager.ts | 12 + .../resolvers/launch.unit.test.ts | 7 + .../extension/debugCommands.unit.test.ts | 7 + .../createEnvironmentTrigger.unit.test.ts | 285 ++++++++++++++++++ .../codeExecutionManager.unit.test.ts | 9 + 18 files changed, 711 insertions(+), 2 deletions(-) create mode 100644 src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts create mode 100644 src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts create mode 100644 src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts diff --git a/package.json b/package.json index 57a2b34f7c03..ef9d0aa6c033 100644 --- a/package.json +++ b/package.json @@ -484,6 +484,19 @@ "experimental" ] }, + "python.createEnvironment.trigger": { + "default": "off", + "markdownDescription": "%python.createEnvironment.trigger.description%", + "scope": "machine-overridable", + "type": "string", + "enum": [ + "off", + "prompt" + ], + "tags": [ + "experimental" + ] + }, "python.condaPath": { "default": "", "description": "%python.condaPath.description%", diff --git a/package.nls.json b/package.nls.json index 08f3a136584f..7d14baf40d64 100644 --- a/package.nls.json +++ b/package.nls.json @@ -24,6 +24,7 @@ "python.command.python.launchTensorBoard.title": "Launch TensorBoard", "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", "python.createEnvironment.contentButton.description": "Show or hide Create Environment button in the editor for `requirements.txt` or other dependency files.", + "python.createEnvironment.trigger.description": "Detect if environment creation is required for the current project", "python.menu.createNewFile.title": "Python File", "python.editor.context.submenu.runPython": "Run Python", "python.editor.context.submenu.runPythonInteractive": "Run in Interactive window", diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index f18fd20bb3dc..c3705a3c6504 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -38,6 +38,7 @@ export namespace Commands { export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter'; export const Create_Environment = 'python.createEnvironment'; export const Create_Environment_Button = 'python.createEnvironment-button'; + export const Create_Environment_Check = 'python.createEnvironmentCheck'; export const Create_Terminal = 'python.createTerminal'; export const Debug_In_Terminal = 'python.debugInTerminal'; export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts index 76f6d2112fe0..2959a2dc8216 100644 --- a/src/client/common/persistentState.ts +++ b/src/client/common/persistentState.ts @@ -20,6 +20,46 @@ import { import { cache } from './utils/decorators'; import { noop } from './utils/misc'; +let _workspaceState: Memento | undefined; +const _workspaceKeys: string[] = []; +export function initializePersistentStateForTriggers(context: IExtensionContext) { + _workspaceState = context.workspaceState; +} + +export function getWorkspaceStateValue(key: string, defaultValue?: T): T | undefined { + if (!_workspaceState) { + throw new Error('Workspace state not initialized'); + } + if (defaultValue === undefined) { + return _workspaceState.get(key); + } + return _workspaceState.get(key, defaultValue); +} + +export async function updateWorkspaceStateValue(key: string, value: T): Promise { + if (!_workspaceState) { + throw new Error('Workspace state not initialized'); + } + try { + _workspaceKeys.push(key); + await _workspaceState.update(key, value); + const after = getWorkspaceStateValue(key); + if (JSON.stringify(after) !== JSON.stringify(value)) { + await _workspaceState.update(key, undefined); + await _workspaceState.update(key, value); + traceError('Error while updating workspace state for key:', key); + } + } catch (ex) { + traceError(`Error while updating workspace state for key [${key}]:`, ex); + } +} + +async function clearWorkspaceState(): Promise { + if (_workspaceState !== undefined) { + await Promise.all(_workspaceKeys.map((key) => updateWorkspaceStateValue(key, undefined))); + } +} + export class PersistentState implements IPersistentState { constructor( public readonly storage: Memento, @@ -93,7 +133,10 @@ export class PersistentStateFactory implements IPersistentStateFactory, IExtensi ) {} public async activate(): Promise { - this.cmdManager?.registerCommand(Commands.ClearStorage, this.cleanAllPersistentStates.bind(this)); + this.cmdManager?.registerCommand(Commands.ClearStorage, async () => { + await clearWorkspaceState(); + await this.cleanAllPersistentStates(); + }); const globalKeysStorageDeprecated = this.createGlobalPersistentState(GLOBAL_PERSISTENT_KEYS_DEPRECATED, []); const workspaceKeysStorageDeprecated = this.createWorkspacePersistentState( WORKSPACE_PERSISTENT_KEYS_DEPRECATED, diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index a785ab4bc75a..bc32c1078cad 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -499,6 +499,15 @@ export namespace CreateEnv { export const deletingEnvironmentProgress = l10n.t('Deleting existing ".conda" environment...'); export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".conda" environment.'); } + + export namespace Trigger { + export const workspaceTriggerMessage = l10n.t( + 'A virtual environment is not currently selected for your Python interpreter. Would you like to create a virtual environment?', + ); + export const createEnvironment = l10n.t('Create'); + export const disableCheck = l10n.t('Disable'); + export const disableCheckWorkspace = l10n.t('Disable (Workspace)'); + } } export namespace ToolsExtensions { diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts index c4ae6a204d71..274758797eb9 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -16,6 +16,12 @@ import { DebuggerTypeName } from '../../../constants'; import { DebugOptions, DebugPurpose, LaunchRequestArguments } from '../../../types'; import { BaseConfigurationResolver } from './base'; import { getProgram, IDebugEnvironmentVariablesService } from './helper'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../../../pythonEnvironments/creation/createEnvironmentTrigger'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; @injectable() export class LaunchConfigurationResolver extends BaseConfigurationResolver { @@ -84,6 +90,8 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver debugConfiguration.debugOptions!.indexOf(item) === pos, ); } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.Workspace, workspaceFolder); return debugConfiguration; } diff --git a/src/client/debugger/extension/debugCommands.ts b/src/client/debugger/extension/debugCommands.ts index 14a108d27793..b3322e8e7dd1 100644 --- a/src/client/debugger/extension/debugCommands.ts +++ b/src/client/debugger/extension/debugCommands.ts @@ -14,6 +14,10 @@ import { DebugPurpose, LaunchRequestArguments } from '../types'; import { IInterpreterService } from '../../interpreter/contracts'; import { noop } from '../../common/utils/misc'; import { getConfigurationsByUri } from './configuration/launch.json/launchJsonReader'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; @injectable() export class DebugCommands implements IExtensionSingleActivationService { @@ -35,6 +39,8 @@ export class DebugCommands implements IExtensionSingleActivationService { this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug-in-terminal' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); const config = await DebugCommands.getDebugConfiguration(file); this.debugService.startDebugging(undefined, config); }), diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index c82bddef3c20..807698f3ec29 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -53,6 +53,8 @@ import { DynamicPythonDebugConfigurationService } from './debugger/extension/con import { IInterpreterQuickPick } from './interpreter/configuration/types'; import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt'; import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; +import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger'; +import { initializePersistentStateForTriggers } from './common/persistentState'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -199,6 +201,8 @@ async function activateLegacy(ext: ExtensionState): Promise { ); registerInstallFormatterPrompt(serviceContainer); + registerCreateEnvironmentTriggers(disposables); + initializePersistentStateForTriggers(ext.context); } } diff --git a/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts b/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts new file mode 100644 index 000000000000..0c1c2b38eab2 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as fsapi from 'fs-extra'; +import { ConfigurationTarget, Uri, WorkspaceFolder } from 'vscode'; +import { getPipRequirementsFiles } from '../provider/venvUtils'; +import { getExtension } from '../../../common/vscodeApis/extensionsApi'; +import { PVSC_EXTENSION_ID } from '../../../common/constants'; +import { PythonExtension } from '../../../api/types'; +import { traceVerbose } from '../../../logging'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; +import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../../../common/persistentState'; + +export const CREATE_ENV_TRIGGER_SETTING_PART = 'createEnvironment.trigger'; +export const CREATE_ENV_TRIGGER_SETTING = `python.${CREATE_ENV_TRIGGER_SETTING_PART}`; + +export async function fileContainsInlineDependencies(_uri: Uri): Promise { + // This is a placeholder for the real implementation of inline dependencies support + // For now we don't detect anything. Once PEP-722/PEP-723 are accepted we can implement + // this properly. + return false; +} + +export async function hasRequirementFiles(workspace: WorkspaceFolder): Promise { + const files = await getPipRequirementsFiles(workspace); + const found = (files?.length ?? 0) > 0; + if (found) { + traceVerbose(`Found requirement files: ${workspace.uri.fsPath}`); + } + return found; +} + +export async function hasKnownFiles(workspace: WorkspaceFolder): Promise { + const filePaths: string[] = [ + 'poetry.lock', + 'conda.yaml', + 'environment.yaml', + 'conda.yml', + 'environment.yml', + 'Pipfile', + 'Pipfile.lock', + ].map((fileName) => path.join(workspace.uri.fsPath, fileName)); + const result = await Promise.all(filePaths.map((f) => fsapi.pathExists(f))); + const found = result.some((r) => r); + if (found) { + traceVerbose(`Found known files: ${workspace.uri.fsPath}`); + } + return found; +} + +export async function isGlobalPythonSelected(workspace: WorkspaceFolder): Promise { + const extension = getExtension(PVSC_EXTENSION_ID); + if (!extension) { + return false; + } + const extensionApi: PythonExtension = extension.exports as PythonExtension; + const interpreter = extensionApi.environments.getActiveEnvironmentPath(workspace.uri); + const details = await extensionApi.environments.resolveEnvironment(interpreter); + const isGlobal = details?.environment === undefined; + if (isGlobal) { + traceVerbose(`Selected python for [${workspace.uri.fsPath}] is [global] type: ${interpreter.path}`); + } + return isGlobal; +} + +/** + * Checks the setting `python.createEnvironment.trigger` to see if we should perform the checks + * to prompt to create an environment. + * @export + * @returns : True if we should prompt to create an environment. + */ +export function shouldPromptToCreateEnv(): boolean { + const config = getConfiguration('python'); + if (config) { + const value = config.get(CREATE_ENV_TRIGGER_SETTING_PART, 'off'); + return value !== 'off'; + } + + return getWorkspaceStateValue(CREATE_ENV_TRIGGER_SETTING, 'off') !== 'off'; +} + +/** + * Sets `python.createEnvironment.trigger` to 'off' in the user settings. + */ +export function disableCreateEnvironmentTrigger(): void { + const config = getConfiguration('python'); + if (config) { + config.update('createEnvironment.trigger', 'off', ConfigurationTarget.Global); + } +} + +/** + * Sets trigger to 'off' in workspace persistent state. This disables trigger check + * for the current workspace only. In multi root case, it is disabled for all folders + * in the multi root workspace. + */ +export async function disableWorkspaceCreateEnvironmentTrigger(): Promise { + await updateWorkspaceStateValue(CREATE_ENV_TRIGGER_SETTING, 'off'); +} + +let _alreadyCreateEnvCriteriaCheck = false; +/** + * Run-once wrapper function for the workspace check to prompt to create an environment. + * @returns : True if we should prompt to c environment. + */ +export function isCreateEnvWorkspaceCheckNotRun(): boolean { + if (_alreadyCreateEnvCriteriaCheck) { + return false; + } + _alreadyCreateEnvCriteriaCheck = true; + return true; +} diff --git a/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts b/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts new file mode 100644 index 000000000000..1737d351ca7b --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable, Uri, WorkspaceFolder } from 'vscode'; +import { + fileContainsInlineDependencies, + hasKnownFiles, + hasRequirementFiles, + isGlobalPythonSelected, + shouldPromptToCreateEnv, + isCreateEnvWorkspaceCheckNotRun, + disableCreateEnvironmentTrigger, + disableWorkspaceCreateEnvironmentTrigger, +} from './common/createEnvTriggerUtils'; +import { getWorkspaceFolder } from '../../common/vscodeApis/workspaceApis'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; +import { hasPrefixCondaEnv, hasVenv } from './common/commonUtils'; +import { showInformationMessage } from '../../common/vscodeApis/windowApis'; +import { CreateEnv } from '../../common/utils/localize'; +import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; +import { Commands } from '../../common/constants'; +import { Resource } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +export enum CreateEnvironmentCheckKind { + /** + * Checks if environment creation is needed based on file location and content. + */ + File = 'file', + + /** + * Checks if environment creation is needed based on workspace contents. + */ + Workspace = 'workspace', +} + +export interface CreateEnvironmentTriggerOptions { + force?: boolean; +} + +async function createEnvironmentCheckForWorkspace(uri: Uri): Promise { + const workspace = getWorkspaceFolder(uri); + if (!workspace) { + traceInfo(`CreateEnv Trigger - Workspace not found for ${uri.fsPath}`); + return; + } + + const missingRequirements = async (workspaceFolder: WorkspaceFolder) => + !(await hasRequirementFiles(workspaceFolder)); + + const isNonGlobalPythonSelected = async (workspaceFolder: WorkspaceFolder) => + !(await isGlobalPythonSelected(workspaceFolder)); + + // Skip showing the Create Environment prompt if one of the following is True: + // 1. The workspace already has a ".venv" or ".conda" env + // 2. The workspace does NOT have "requirements.txt" or "requirements/*.txt" files + // 3. The workspace has known files for other environment types like environment.yml, conda.yml, poetry.lock, etc. + // 4. The selected python is NOT classified as a global python interpreter + const skipPrompt: boolean = ( + await Promise.all([ + hasVenv(workspace), + hasPrefixCondaEnv(workspace), + missingRequirements(workspace), + hasKnownFiles(workspace), + isNonGlobalPythonSelected(workspace), + ]) + ).some((r) => r); + + if (skipPrompt) { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'criteria-not-met' }); + traceInfo(`CreateEnv Trigger - Skipping for ${uri.fsPath}`); + return; + } + + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'criteria-met' }); + const selection = await showInformationMessage( + CreateEnv.Trigger.workspaceTriggerMessage, + CreateEnv.Trigger.createEnvironment, + CreateEnv.Trigger.disableCheckWorkspace, + CreateEnv.Trigger.disableCheck, + ); + + if (selection === CreateEnv.Trigger.createEnvironment) { + try { + await executeCommand(Commands.Create_Environment); + } catch (error) { + traceError('CreateEnv Trigger - Error while creating environment: ', error); + } + } else if (selection === CreateEnv.Trigger.disableCheck) { + disableCreateEnvironmentTrigger(); + } else if (selection === CreateEnv.Trigger.disableCheckWorkspace) { + disableWorkspaceCreateEnvironmentTrigger(); + } +} + +function runOnceWorkspaceCheck(uri: Uri, options: CreateEnvironmentTriggerOptions = {}): Promise { + if (isCreateEnvWorkspaceCheckNotRun() || options?.force) { + return createEnvironmentCheckForWorkspace(uri); + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'already-ran' }); + traceVerbose('CreateEnv Trigger - skipping this because it was already run'); + return Promise.resolve(); +} + +async function createEnvironmentCheckForFile(uri: Uri, options?: CreateEnvironmentTriggerOptions): Promise { + if (await fileContainsInlineDependencies(uri)) { + // TODO: Handle create environment for each file here. + // pending acceptance of PEP-722/PEP-723 + + // For now we do the same thing as for workspace. + await runOnceWorkspaceCheck(uri, options); + } + + // If the file does not have any inline dependencies, then we do the same thing + // as for workspace. + await runOnceWorkspaceCheck(uri, options); +} + +export async function triggerCreateEnvironmentCheck( + kind: CreateEnvironmentCheckKind, + uri: Resource, + options?: CreateEnvironmentTriggerOptions, +): Promise { + if (!uri) { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'no-uri' }); + traceVerbose('CreateEnv Trigger - Skipping No URI provided'); + return; + } + + if (shouldPromptToCreateEnv()) { + if (kind === CreateEnvironmentCheckKind.File) { + await createEnvironmentCheckForFile(uri, options); + } else { + await runOnceWorkspaceCheck(uri, options); + } + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'turned-off' }); + traceVerbose('CreateEnv Trigger - turned off in settings'); + } +} + +export function triggerCreateEnvironmentCheckNonBlocking( + kind: CreateEnvironmentCheckKind, + uri: Resource, + options?: CreateEnvironmentTriggerOptions, +): void { + // The Event loop for Node.js runs functions with setTimeout() with lower priority than setImmediate. + // This is done to intentionally avoid blocking anything that the user wants to do. + setTimeout(() => triggerCreateEnvironmentCheck(kind, uri, options).ignoreErrors(), 0); +} + +export function registerCreateEnvironmentTriggers(disposables: Disposable[]): void { + disposables.push( + registerCommand(Commands.Create_Environment_Check, (file: Resource) => { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'as-command' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file, { force: true }); + }), + ); +} diff --git a/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/src/client/pythonEnvironments/creation/provider/venvUtils.ts index f0828be8d1f7..cc506a11a88f 100644 --- a/src/client/pythonEnvironments/creation/provider/venvUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -35,7 +35,7 @@ export const OPEN_REQUIREMENTS_BUTTON = { tooltip: CreateEnv.Venv.openRequirementsFile, }; const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**'; -async function getPipRequirementsFiles( +export async function getPipRequirementsFiles( workspaceFolder: WorkspaceFolder, token?: CancellationToken, ): Promise { diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index a729b3d491e8..c680b91094cb 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -115,6 +115,9 @@ export enum EventName { ENVIRONMENT_DELETE = 'ENVIRONMENT.DELETE', ENVIRONMENT_REUSE = 'ENVIRONMENT.REUSE', + ENVIRONMENT_CHECK_TRIGGER = 'ENVIRONMENT.CHECK.TRIGGER', + ENVIRONMENT_CHECK_RESULT = 'ENVIRONMENT.CHECK.RESULT', + TOOLS_EXTENSIONS_ALREADY_INSTALLED = 'TOOLS_EXTENSIONS.ALREADY_INSTALLED', TOOLS_EXTENSIONS_PROMPT_SHOWN = 'TOOLS_EXTENSIONS.PROMPT_SHOWN', TOOLS_EXTENSIONS_INSTALL_SELECTED = 'TOOLS_EXTENSIONS.INSTALL_SELECTED', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index f4947cd73f05..600f9a2d48ff 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -2144,6 +2144,34 @@ export interface IEventNamePropertyMapping { [EventName.ENVIRONMENT_REUSE]: { environmentType: 'venv' | 'conda'; }; + /** + * Telemetry event sent when a check for environment creation conditions is triggered. + */ + /* __GDPR__ + "environemt.check.trigger" : { + "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_CHECK_TRIGGER]: { + trigger: + | 'run-in-terminal' + | 'debug-in-terminal' + | 'run-selection' + | 'on-workspace-load' + | 'as-command' + | 'debug'; + }; + /** + * Telemetry event sent when a check for environment creation condition is computed. + */ + /* __GDPR__ + "environemt.check.result" : { + "result" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_CHECK_RESULT]: { + result: 'criteria-met' | 'criteria-not-met' | 'already-ran' | 'turned-off' | 'no-uri'; + }; /** * Telemetry event sent when a linter or formatter extension is already installed. */ diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index 9f1ba6e90d90..2dd619a1816a 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -18,6 +18,10 @@ import { traceError } from '../../logging'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } from '../../terminals/types'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; @injectable() export class CodeExecutionManager implements ICodeExecutionManager { @@ -48,6 +52,10 @@ export class CodeExecutionManager implements ICodeExecutionManager { .then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { + trigger: 'run-in-terminal', + }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; await this.executeFileInTerminal(file, trigger, { newTerminalPerFile: cmd === Commands.Exec_In_Separate_Terminal, @@ -69,6 +77,8 @@ export class CodeExecutionManager implements ICodeExecutionManager { this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'run-selection' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); await this.executeSelectionInTerminal().then(() => { if (this.shouldTerminalFocusOnStart(file)) this.commandManager.executeCommand('workbench.action.terminal.focus'); @@ -85,6 +95,8 @@ export class CodeExecutionManager implements ICodeExecutionManager { this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); return; } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'run-selection' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); await this.executeSelectionInDjangoShell().then(() => { if (this.shouldTerminalFocusOnStart(file)) this.commandManager.executeCommand('workbench.action.terminal.focus'); diff --git a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts index 2aec3dcfd041..59f61f81cd85 100644 --- a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts @@ -22,6 +22,7 @@ import * as platform from '../../../../../client/common/utils/platform'; import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; import { IEnvironmentActivationService } from '../../../../../client/interpreter/activation/types'; +import * as triggerApis from '../../../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; getInfoPerOS().forEach(([osName, osType, path]) => { if (osType === platform.OSType.Unknown) { @@ -42,12 +43,18 @@ getInfoPerOS().forEach(([osName, osType, path]) => { let getActiveTextEditorStub: sinon.SinonStub; let getOSTypeStub: sinon.SinonStub; let getWorkspaceFolderStub: sinon.SinonStub; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; setup(() => { getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); getOSTypeStub = sinon.stub(platform, 'getOSType'); getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); getOSTypeStub.returns(osType); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { diff --git a/src/test/debugger/extension/debugCommands.unit.test.ts b/src/test/debugger/extension/debugCommands.unit.test.ts index 3c023f3f1450..7d2463072f06 100644 --- a/src/test/debugger/extension/debugCommands.unit.test.ts +++ b/src/test/debugger/extension/debugCommands.unit.test.ts @@ -14,6 +14,7 @@ import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; import * as telemetry from '../../../client/telemetry'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as triggerApis from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; suite('Debugging - commands', () => { let commandManager: typemoq.IMock; @@ -21,6 +22,7 @@ suite('Debugging - commands', () => { let disposables: typemoq.IMock; let interpreterService: typemoq.IMock; let debugCommands: IExtensionSingleActivationService; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; setup(() => { commandManager = typemoq.Mock.ofType(); @@ -36,6 +38,11 @@ suite('Debugging - commands', () => { sinon.stub(telemetry, 'sendTelemetryEvent').callsFake(() => { /** noop */ }); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { sinon.restore(); diff --git a/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts new file mode 100644 index 000000000000..f751d270219e --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as triggerUtils from '../../../client/pythonEnvironments/creation/common/createEnvTriggerUtils'; +import * as commonUtils from '../../../client/pythonEnvironments/creation/common/commonUtils'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheck, +} from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as commandApis from '../../../client/common/vscodeApis/commandApis'; +import { Commands } from '../../../client/common/constants'; +import { CreateEnv } from '../../../client/common/utils/localize'; + +suite('Create Environment Trigger', () => { + let shouldPromptToCreateEnvStub: sinon.SinonStub; + let hasVenvStub: sinon.SinonStub; + let hasPrefixCondaEnvStub: sinon.SinonStub; + let hasRequirementFilesStub: sinon.SinonStub; + let hasKnownFilesStub: sinon.SinonStub; + let isGlobalPythonSelectedStub: sinon.SinonStub; + let showInformationMessageStub: sinon.SinonStub; + let isCreateEnvWorkspaceCheckNotRunStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let executeCommandStub: sinon.SinonStub; + let disableCreateEnvironmentTriggerStub: sinon.SinonStub; + let disableWorkspaceCreateEnvironmentTriggerStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + shouldPromptToCreateEnvStub = sinon.stub(triggerUtils, 'shouldPromptToCreateEnv'); + hasVenvStub = sinon.stub(commonUtils, 'hasVenv'); + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + hasRequirementFilesStub = sinon.stub(triggerUtils, 'hasRequirementFiles'); + hasKnownFilesStub = sinon.stub(triggerUtils, 'hasKnownFiles'); + isGlobalPythonSelectedStub = sinon.stub(triggerUtils, 'isGlobalPythonSelected'); + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + + isCreateEnvWorkspaceCheckNotRunStub = sinon.stub(triggerUtils, 'isCreateEnvWorkspaceCheckNotRun'); + isCreateEnvWorkspaceCheckNotRunStub.returns(true); + + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getWorkspaceFolderStub.returns(workspace1); + + executeCommandStub = sinon.stub(commandApis, 'executeCommand'); + disableCreateEnvironmentTriggerStub = sinon.stub(triggerUtils, 'disableCreateEnvironmentTrigger'); + disableWorkspaceCreateEnvironmentTriggerStub = sinon.stub( + triggerUtils, + 'disableWorkspaceCreateEnvironmentTrigger', + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No Uri', async () => { + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, undefined); + sinon.assert.notCalled(shouldPromptToCreateEnvStub); + }); + + test('Should not perform checks if user set trigger to "off"', async () => { + shouldPromptToCreateEnvStub.returns(false); + + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.notCalled(hasVenvStub); + sinon.assert.notCalled(hasPrefixCondaEnvStub); + sinon.assert.notCalled(hasRequirementFilesStub); + sinon.assert.notCalled(hasKnownFilesStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not perform checks even if force is true, if user set trigger to "off"', async () => { + shouldPromptToCreateEnvStub.returns(false); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri, { + force: true, + }); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.notCalled(hasVenvStub); + sinon.assert.notCalled(hasPrefixCondaEnvStub); + sinon.assert.notCalled(hasRequirementFilesStub); + sinon.assert.notCalled(hasKnownFilesStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there is a ".venv"', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(true); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there is a ".conda"', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(true); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there are no requirements', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(false); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there are known files', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(false); + hasKnownFilesStub.resolves(true); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if selected python is not global', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(false); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should show prompt if all conditions met: User closes prompt', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + showInformationMessageStub.resolves(undefined); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + sinon.assert.notCalled(disableWorkspaceCreateEnvironmentTriggerStub); + }); + + test('Should show prompt if all conditions met: User clicks create', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(CreateEnv.Trigger.createEnvironment); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.calledOnceWithExactly(executeCommandStub, Commands.Create_Environment); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + sinon.assert.notCalled(disableWorkspaceCreateEnvironmentTriggerStub); + }); + + test('Should show prompt if all conditions met: User clicks disable global', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(CreateEnv.Trigger.disableCheck); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.calledOnce(disableCreateEnvironmentTriggerStub); + sinon.assert.notCalled(disableWorkspaceCreateEnvironmentTriggerStub); + }); + + test('Should show prompt if all conditions met: User clicks disable workspace', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(CreateEnv.Trigger.disableCheckWorkspace); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + sinon.assert.calledOnce(disableWorkspaceCreateEnvironmentTriggerStub); + }); +}); diff --git a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts index 30f95c94d217..29c310f6c724 100644 --- a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +++ b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; import { Disposable, TextDocument, TextEditor, Uri } from 'vscode'; import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; @@ -13,6 +14,7 @@ import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } fr import { IConfigurationService } from '../../../client/common/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as triggerApis from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; suite('Terminal - Code Execution Manager', () => { let executionManager: ICodeExecutionManager; @@ -24,6 +26,7 @@ suite('Terminal - Code Execution Manager', () => { let configService: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; setup(() => { fileSystem = TypeMoq.Mock.ofType(); fileSystem.setup((f) => f.readFile(TypeMoq.It.isAny())).returns(() => Promise.resolve('')); @@ -52,8 +55,14 @@ suite('Terminal - Code Execution Manager', () => { configService.object, serviceContainer.object, ); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { + sinon.restore(); disposables.forEach((disposable) => { if (disposable) { disposable.dispose(); From 998a0a54a8107b2033a25df36569d090a64244d4 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 25 Sep 2023 15:01:50 -0700 Subject: [PATCH 0207/1136] Add await for stdout (#22049) Add await so all output is read before ending the run instance. --- .../testing/testController/common/server.ts | 29 ++++++++-- .../testing/testController/common/utils.ts | 2 +- .../testing/testController/controller.ts | 1 - .../pytest/pytestDiscoveryAdapter.ts | 21 ++++--- .../pytest/pytestExecutionAdapter.ts | 55 +++++++++++++------ src/test/mocks/mockChildProcess.ts | 6 +- .../testing/common/testingPayloadsEot.test.ts | 11 ++-- .../pytestExecutionAdapter.unit.test.ts | 2 +- .../testCancellationRunAdapters.unit.test.ts | 53 ++++++++++++++---- 9 files changed, 126 insertions(+), 54 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 699f7f754122..d47587386069 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -5,6 +5,7 @@ import * as net from 'net'; import * as crypto from 'crypto'; import { Disposable, Event, EventEmitter, TestRun } from 'vscode'; import * as path from 'path'; +import { ChildProcess } from 'child_process'; import { ExecutionFactoryCreateWithEnvironmentOptions, ExecutionResult, @@ -86,7 +87,7 @@ export class PythonTestServer implements ITestServer, Disposable { // what payload is so small it doesn't include the whole UUID think got this if (extractedJsonPayload.uuid !== undefined && extractedJsonPayload.cleanedJsonData !== undefined) { // if a full json was found in the buffer, fire the data received event then keep cycling with the remaining raw data. - traceInfo(`Firing data received event, ${extractedJsonPayload.cleanedJsonData}`); + traceLog(`Firing data received event, ${extractedJsonPayload.cleanedJsonData}`); this._fireDataReceived(extractedJsonPayload.uuid, extractedJsonPayload.cleanedJsonData); } buffer = Buffer.from(extractedJsonPayload.remainingRawData); @@ -223,13 +224,23 @@ export class PythonTestServer implements ITestServer, Disposable { // This means it is running discovery traceLog(`Discovering unittest tests with arguments: ${args}\r\n`); } - const deferred = createDeferred>(); - const result = execService.execObservable(args, spawnOptions); + const deferredTillExecClose = createDeferred>(); + + let resultProc: ChildProcess | undefined; + runInstance?.token.onCancellationRequested(() => { traceInfo('Test run cancelled, killing unittest subprocess.'); - result?.proc?.kill(); + // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. + if (resultProc) { + resultProc?.kill(); + } else { + deferredTillExecClose?.resolve(); + } }); + const result = execService?.execObservable(args, spawnOptions); + resultProc = result?.proc; + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. result?.proc?.stdout?.on('data', (data) => { @@ -238,6 +249,12 @@ export class PythonTestServer implements ITestServer, Disposable { result?.proc?.stderr?.on('data', (data) => { spawnOptions?.outputChannel?.append(data.toString()); }); + result?.proc?.on('exit', (code, signal) => { + if (code !== 0) { + traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}`); + } + }); + result?.proc?.on('exit', (code, signal) => { // if the child has testIds then this is a run request if (code !== 0 && testIds && testIds?.length !== 0) { @@ -269,9 +286,9 @@ export class PythonTestServer implements ITestServer, Disposable { data: JSON.stringify(createEOTPayload(true)), }); } - deferred.resolve({ stdout: '', stderr: '' }); + deferredTillExecClose.resolve({ stdout: '', stderr: '' }); }); - await deferred.promise; + await deferredTillExecClose.promise; } } catch (ex) { traceError(`Error while server attempting to run unittest command: ${ex}`); diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index dc254a9900a1..1272ff37fb5d 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -43,7 +43,7 @@ export const JSONRPC_UUID_HEADER = 'Request-uuid'; export const JSONRPC_CONTENT_LENGTH_HEADER = 'Content-Length'; export const JSONRPC_CONTENT_TYPE_HEADER = 'Content-Type'; -export function createEOTDeferred(): Deferred { +export function createTestingDeferred(): Deferred { return createDeferred(); } diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 0c7db5594004..af77ab2b2525 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -462,7 +462,6 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); } } - if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { unconfiguredWorkspaces.push(workspace); } diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index c03baeae0421..c0e1a310ee4a 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -4,7 +4,6 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -19,7 +18,7 @@ import { ITestResultResolver, ITestServer, } from '../common/types'; -import { createDiscoveryErrorPayload, createEOTPayload } from '../common/utils'; +import { createDiscoveryErrorPayload, createEOTPayload, createTestingDeferred } from '../common/utils'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied @@ -47,6 +46,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { await this.runPytestDiscovery(uri, uuid, executionFactory); } finally { await deferredTillEOT.promise; + traceVerbose('deferredTill EOT resolved'); disposeDataReceiver(this.testServer); } // this is only a placeholder to handle function overloading until rewrite is finished @@ -55,7 +55,6 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } async runPytestDiscovery(uri: Uri, uuid: string, executionFactory?: IPythonExecutionFactory): Promise { - const deferred = createDeferred(); const relativePathToPytest = 'pythonFiles'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); const settings = this.configSettings.getSettings(uri); @@ -83,9 +82,10 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); // delete UUID following entire discovery finishing. - const deferredExec = createDeferred>(); const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); traceVerbose(`Running pytest discovery with command: ${execArgs.join(' ')}`); + + const deferredTillExecClose: Deferred = createTestingDeferred(); const result = execService?.execObservable(execArgs, spawnOptions); // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. @@ -97,6 +97,11 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { spawnOptions.outputChannel?.append(data.toString()); }); result?.proc?.on('exit', (code, signal) => { + if (code !== 0) { + traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}.`); + } + }); + result?.proc?.on('close', (code, signal) => { if (code !== 0) { traceError( `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, @@ -112,10 +117,10 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { data: JSON.stringify(createEOTPayload(true)), }); } - deferredExec.resolve({ stdout: '', stderr: '' }); - deferred.resolve(); + // deferredTillEOT is resolved when all data sent on stdout and stderr is received, close event is only called when this occurs + // due to the sync reading of the output. + deferredTillExecClose?.resolve(); }); - - await deferredExec.promise; + await deferredTillExecClose.promise; } } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 085af40375d4..8020be17cf90 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -3,9 +3,10 @@ import { TestRun, Uri } from 'vscode'; import * as path from 'path'; +import { ChildProcess } from 'child_process'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { Deferred, createDeferred } from '../../../common/utils/async'; -import { traceError, traceInfo } from '../../../logging'; +import { Deferred } from '../../../common/utils/async'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, @@ -15,7 +16,6 @@ import { } from '../common/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; @@ -42,7 +42,9 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { debugLauncher?: ITestDebugLauncher, ): Promise { const uuid = this.testServer.createUUID(uri.fsPath); - const deferredTillEOT: Deferred = utils.createEOTDeferred(); + // deferredTillEOT is resolved when all data sent over payload is received + const deferredTillEOT: Deferred = utils.createTestingDeferred(); + const dataReceivedDisposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { if (runInstance) { const eParsed = JSON.parse(e.data); @@ -60,8 +62,9 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo("Test run cancelled, resolving 'till EOT' deferred."); deferredTillEOT.resolve(); }); + try { - this.runTestsNew( + await this.runTestsNew( uri, testIds, uuid, @@ -73,6 +76,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { ); } finally { await deferredTillEOT.promise; + traceVerbose('deferredTill EOT resolved'); disposeDataReceiver(this.testServer); } @@ -123,7 +127,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }; // need to check what will happen in the exec service is NOT defined and is null const execService = await executionFactory?.createActivatedEnvironment(creationOptions); - try { // Remove positional test folders and files, we will add as needed per node const testArgs = removePositionalFoldersAndFiles(pytestArgs); @@ -159,19 +162,28 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { deferredTillEOT?.resolve(); }); } else { + // deferredTillExecClose is resolved when all stdout and stderr is read + const deferredTillExecClose: Deferred = utils.createTestingDeferred(); // combine path to run script with run args const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); const runArgs = [scriptPath, ...testArgs]; traceInfo(`Running pytest with arguments: ${runArgs.join(' ')}\r\n`); - const deferredExec = createDeferred>(); - const result = execService?.execObservable(runArgs, spawnOptions); + let resultProc: ChildProcess | undefined; runInstance?.token.onCancellationRequested(() => { traceInfo('Test run cancelled, killing pytest subprocess.'); - result?.proc?.kill(); + // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. + if (resultProc) { + resultProc?.kill(); + } else { + deferredTillExecClose?.resolve(); + } }); + const result = execService?.execObservable(runArgs, spawnOptions); + resultProc = result?.proc; + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. result?.proc?.stdout?.on('data', (data) => { @@ -180,15 +192,20 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { result?.proc?.stderr?.on('data', (data) => { this.outputChannel?.append(data.toString()); }); - result?.proc?.on('exit', (code, signal) => { - traceInfo('Test run finished, subprocess exited.'); + if (code !== 0 && testIds) { + traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}.`); + } + }); + + result?.proc?.on('close', (code, signal) => { + traceVerbose('Test run finished, subprocess closed.'); // if the child has testIds then this is a run request + // if the child process exited with a non-zero exit code, then we need to send the error payload. if (code !== 0 && testIds) { traceError( - `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, + `Subprocess closed unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, ); - // if the child process exited with a non-zero exit code, then we need to send the error payload. this.testServer.triggerRunDataReceivedEvent({ uuid, data: JSON.stringify(utils.createExecutionErrorPayload(code, signal, testIds, cwd)), @@ -199,16 +216,22 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { data: JSON.stringify(utils.createEOTPayload(true)), }); } - deferredExec.resolve({ stdout: '', stderr: '' }); + // deferredTillEOT is resolved when all data sent on stdout and stderr is received, close event is only called when this occurs + // due to the sync reading of the output. + deferredTillExecClose?.resolve(); }); - await deferredExec.promise; + await deferredTillExecClose?.promise; } } catch (ex) { traceError(`Error while running tests: ${testIds}\r\n${ex}\r\n\r\n`); return Promise.reject(ex); } - const executionPayload: ExecutionTestPayload = { cwd, status: 'success', error: '' }; + const executionPayload: ExecutionTestPayload = { + cwd, + status: 'success', + error: '', + }; return executionPayload; } } diff --git a/src/test/mocks/mockChildProcess.ts b/src/test/mocks/mockChildProcess.ts index a46d66d79ca0..c0a24b1c955f 100644 --- a/src/test/mocks/mockChildProcess.ts +++ b/src/test/mocks/mockChildProcess.ts @@ -133,9 +133,9 @@ export class MockChildProcess extends EventEmitter { emit(event: string | symbol, ...args: unknown[]): boolean { if (this.eventMap.has(event.toString())) { - this.eventMap.get(event.toString()).forEach((listener: (arg0: unknown) => void) => { - const argsArray = Array.isArray(args) ? args : [args]; - listener(argsArray); + this.eventMap.get(event.toString()).forEach((listener: (...arg0: unknown[]) => void) => { + const argsArray: unknown[] = Array.isArray(args) ? args : [args]; + listener(...argsArray); }); } return true; diff --git a/src/test/testing/common/testingPayloadsEot.test.ts b/src/test/testing/common/testingPayloadsEot.test.ts index 227ad5fa1697..9025248c66ff 100644 --- a/src/test/testing/common/testingPayloadsEot.test.ts +++ b/src/test/testing/common/testingPayloadsEot.test.ts @@ -66,6 +66,7 @@ suite('EOT tests', () => { let testController: TestController; let stubExecutionFactory: typeMoq.IMock; let client: net.Socket; + let mockProc: MockChildProcess; const sandbox = sinon.createSandbox(); // const unittestProvider: TestProvider = UNITTEST_PROVIDER; // const pytestProvider: TestProvider = PYTEST_PROVIDER; @@ -86,7 +87,7 @@ suite('EOT tests', () => { traceLog('Socket connection error:', error); }); - const mockProc = new MockChildProcess('', ['']); + mockProc = new MockChildProcess('', ['']); const output2 = new Observable>(() => { /* no op */ }); @@ -156,15 +157,17 @@ suite('EOT tests', () => { } })(client, payloadArray[i]); } + mockProc.emit('close', 0, null); client.end(); }); resultResolver = new PythonResultResolver(testController, PYTEST_PROVIDER, workspaceUri); resultResolver._resolveExecution = async (payload, _token?) => { // the payloads that get to the _resolveExecution are all data and should be successful. + actualCollectedResult = actualCollectedResult + JSON.stringify(payload.result); assert.strictEqual(payload.status, 'success', "Expected status to be 'success'"); assert.ok(payload.result, 'Expected results to be present'); - actualCollectedResult = actualCollectedResult + JSON.stringify(payload.result); + return Promise.resolve(); }; // set workspace to test workspace folder @@ -200,10 +203,6 @@ suite('EOT tests', () => { actualCollectedResult, "Expected collected result to match 'data'", ); - // nervous about this not testing race conditions correctly - // client.end(); - // verify that the _resolveExecution was called once per test - // assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); }); }); }); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 9cc428ab0a4c..a2e5c810dc86 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -268,7 +268,7 @@ suite('pytest test execution adapter', () => { traceInfo('stubs launch debugger'); deferredEOT.resolve(); }); - const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); utilsCreateEOTStub.callsFake(() => deferredEOT); const testRun = typeMoq.Mock.ofType(); testRun diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts index 2aaffdda41df..e85cd2b62834 100644 --- a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -117,10 +117,16 @@ suite('Execution Flow Run Adapters', () => { deferredStartServer.resolve(); return Promise.resolve(54321); }); - // mock EOT token + // mock EOT token & ExecClose token const deferredEOT = createDeferred(); - const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); - utilsCreateEOTStub.callsFake(() => deferredEOT); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); // set up test server testServer .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) @@ -158,6 +164,11 @@ suite('Execution Flow Run Adapters', () => { const execServiceMock = typeMoq.Mock.ofType(); debugLauncher .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .callback((_options, callback) => { + if (callback) { + callback(); + } + }) .returns(async () => { cancellationToken.cancel(); return Promise.resolve(); @@ -174,10 +185,16 @@ suite('Execution Flow Run Adapters', () => { deferredStartServer.resolve(); return Promise.resolve(54321); }); - // mock EOT token + // mock EOT token & ExecClose token const deferredEOT = createDeferred(); - const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); - utilsCreateEOTStub.callsFake(() => deferredEOT); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); // set up test server testServer .setup((t) => t.onRunDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) @@ -263,10 +280,16 @@ suite('Execution Flow Run Adapters', () => { deferredStartServer.resolve(); return Promise.resolve(54321); }); - // mock EOT token + // mock EOT token & ExecClose token const deferredEOT = createDeferred(); - const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); - utilsCreateEOTStub.callsFake(() => deferredEOT); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); // set up test server const unittestAdapter = new UnittestTestExecutionAdapter( stubTestServer.object, @@ -331,10 +354,16 @@ suite('Execution Flow Run Adapters', () => { deferredStartServer.resolve(); return Promise.resolve(54321); }); - // mock EOT token + // mock EOT token & ExecClose token const deferredEOT = createDeferred(); - const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createEOTDeferred'); - utilsCreateEOTStub.callsFake(() => deferredEOT); + const deferredExecClose = createDeferred(); + const utilsCreateEOTStub: sinon.SinonStub = sinon.stub(util, 'createTestingDeferred'); + utilsCreateEOTStub.callsFake(() => { + if (utilsCreateEOTStub.callCount === 1) { + return deferredEOT; + } + return deferredExecClose; + }); // set up test server const unittestAdapter = new UnittestTestExecutionAdapter( stubTestServer.object, From b3c5698cee0abe48e4fe471ebbc47ba34c5a7eb5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 25 Sep 2023 21:40:35 -0700 Subject: [PATCH 0208/1136] Explicitly continue execution after timeout on launching conda binary is reached (#22072) Closes https://github.com/microsoft/vscode-python/issues/22050 --- .../common/environmentManagers/conda.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/src/client/pythonEnvironments/common/environmentManagers/conda.ts index 88178d02d58a..8f048ddd0676 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -23,6 +23,7 @@ import { traceError, traceVerbose } from '../../../logging'; import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; import { splitLines } from '../../../common/stringUtils'; import { SpawnOptions } from '../../../common/process/types'; +import { sleep } from '../../../common/utils/async'; export const AnacondaCompanyName = 'Anaconda, Inc.'; export const CONDAPATH_SETTING_KEY = 'condaPath'; @@ -238,7 +239,7 @@ export function getCondaInterpreterPath(condaEnvironmentPath: string): string { // Minimum version number of conda required to be able to use 'conda run' with '--no-capture-output' flag. export const CONDA_RUN_VERSION = '4.9.0'; export const CONDA_ACTIVATION_TIMEOUT = 45000; -const CONDA_GENERAL_TIMEOUT = 50000; +const CONDA_GENERAL_TIMEOUT = 45000; /** Wraps the "conda" utility, and exposes its functionality. */ @@ -439,9 +440,19 @@ export class Conda { if (shellPath) { options.shell = shellPath; } - const result = await exec(command, ['info', '--json'], options); - traceVerbose(`${command} info --json: ${result.stdout}`); - return JSON.parse(result.stdout); + const resultPromise = exec(command, ['info', '--json'], options); + // It has been observed that specifying a timeout is still not reliable to terminate the Conda process, see #27915. + // Hence explicitly continue execution after timeout has been reached. + const success = await Promise.race([ + resultPromise.then(() => true), + sleep(CONDA_GENERAL_TIMEOUT + 3000).then(() => false), + ]); + if (success) { + const result = await resultPromise; + traceVerbose(`${command} info --json: ${result.stdout}`); + return JSON.parse(result.stdout); + } + throw new Error(`Launching '${command} info --json' timed out`); } /** From 4f44fa917a1fe2c451490b1d252a195dbc9e08ee Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 25 Sep 2023 21:51:49 -0700 Subject: [PATCH 0209/1136] Calculate PS1 instead of using PS1 returned by shell (#22078) Closes https://github.com/microsoft/vscode-python/issues/22056 `PS1` returned by shell is not predictable, it can be `(.venv) ` or already have the context of the terminal: ``` (venv) [\u@\h \W]\[\e[91m\]$(parse_git_branch)\[\e[00m\]$ ``` Calculate it to be safe and not double prepend it. --- .../terminalEnvVarCollectionService.ts | 23 +++++++++++------ ...rminalEnvVarCollectionService.unit.test.ts | 25 ++++++++++++------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 50a15d25f3de..a4a5137953a6 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -282,22 +282,25 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } private async getPS1(shell: string, resource: Resource, env: EnvironmentVariables) { - if (env.PS1) { - return env.PS1; - } const customShellType = identifyShellFromShellPath(shell); if (this.noPromptVariableShells.includes(customShellType)) { - return undefined; + return env.PS1; } if (this.platform.osType !== OSType.Windows) { // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. const interpreter = await this.interpreterService.getActiveInterpreter(resource); const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); - if (shouldSetPS1 && !env.PS1) { - // PS1 should be set but no PS1 was set. - return getPromptForEnv(interpreter); + if (shouldSetPS1) { + const prompt = getPromptForEnv(interpreter); + if (prompt) { + return prompt; + } } } + if (env.PS1) { + // Prefer PS1 set by env vars, as env.PS1 may or may not contain the full PS1: #22056. + return env.PS1; + } return undefined; } @@ -369,6 +372,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariables): boolean { + if (env.PS1) { + // Activated variables contain PS1, meaning it was supposed to be set. + return true; + } if (type === PythonEnvType.Virtual) { const promptDisabledVar = env.VIRTUAL_ENV_DISABLE_PROMPT; const isPromptDisabled = promptDisabledVar && promptDisabledVar !== undefined; @@ -381,7 +388,7 @@ function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariabl const isPromptEnabled = promptEnabledVar && promptEnabledVar !== ''; return !!isPromptEnabled; } - return type !== undefined; + return false; } function shouldSkip(env: string) { diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 091986b7e18b..e41d6ce4d53c 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -191,21 +191,28 @@ suite('Terminal Environment Variable Collection Service', () => { verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); }); - test('If activated variables contain PS1, prefix it using shell integration', async () => { - const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env, PS1: '(prompt)' }; + // eslint-disable-next-line consistent-return + test('If activated variables contain PS1, prefix it using shell integration', async function () { + if (getOSType() === OSType.Windows) { + return this.skip(); + } + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + PS1: '(envName) extra prompt', // Should not use this + }; when( - environmentActivationService.getActivatedEnvironmentVariables( - anything(), - undefined, - undefined, - customShell, - ), + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + envName: 'envName', + } as unknown) as PythonEnvironment); + when(collection.replace(anything(), anything(), anything())).thenResolve(); when(collection.delete(anything())).thenResolve(); let opts: EnvironmentVariableMutatorOptions | undefined; - when(collection.prepend('PS1', '(prompt)', anything())).thenCall((_, _v, o) => { + when(collection.prepend('PS1', '(envName) ', anything())).thenCall((_, _v, o) => { opts = o; }); From 8aad45710a2b1df45978aeac3b45c321b50e6897 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 26 Sep 2023 02:18:54 -0700 Subject: [PATCH 0210/1136] Fix progress indicator when reactivating terminals (#22082) --- .../activation/terminalEnvVarCollectionService.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index a4a5137953a6..8d99ffd48445 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -102,21 +102,17 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (!this.registeredOnce) { this.interpreterService.onDidChangeInterpreter( async (r) => { - this.showProgress(); await this._applyCollection(r).ignoreErrors(); - this.hideProgress(); }, this, this.disposables, ); this.applicationEnvironment.onDidChangeShell( async (shell: string) => { - this.showProgress(); this.processEnvVars = undefined; // Pass in the shell where known instead of relying on the application environment, because of bug // on VSCode: https://github.com/microsoft/vscode/issues/160694 await this._applyCollection(undefined, shell).ignoreErrors(); - this.hideProgress(); }, this, this.disposables, @@ -130,6 +126,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { + this.showProgress(); const workspaceFolder = this.getWorkspaceFolder(resource); const settings = this.configurationService.getSettings(resource); const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); @@ -224,6 +221,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); envVarCollection.description = description; + this.hideProgress(); await this.trackTerminalPrompt(shell, resource, env); } From bd3590d3b6e78aa47de978cc4103ead9accb560e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 26 Sep 2023 13:45:08 -0700 Subject: [PATCH 0211/1136] Fix "reactivating terminals..." for global interpreters (#22096) Closes https://github.com/microsoft/vscode-python/issues/22085 closes https://github.com/microsoft/vscode-python/issues/22087 Will add tests in a follow up PR --- .../activation/terminalEnvVarCollectionService.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index 8d99ffd48445..c11ec221d4d7 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -125,8 +125,13 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } - public async _applyCollection(resource: Resource, shell = this.applicationEnvironment.shell): Promise { + public async _applyCollection(resource: Resource, shell?: string): Promise { this.showProgress(); + await this._applyCollectionImpl(resource, shell); + this.hideProgress(); + } + + private async _applyCollectionImpl(resource: Resource, shell = this.applicationEnvironment.shell): Promise { const workspaceFolder = this.getWorkspaceFolder(resource); const settings = this.configurationService.getSettings(resource); const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); @@ -221,7 +226,6 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); envVarCollection.description = description; - this.hideProgress(); await this.trackTerminalPrompt(shell, resource, env); } From 2d3ce9839f35b7aeb26655956b2dc21fb52793c7 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Wed, 27 Sep 2023 00:42:16 +0100 Subject: [PATCH 0212/1136] Bump jedi-language-server and jedi (#22069) This picks up the latest versions of each of these, removing pydantic as a dependency and adding support for Python 3.11. Fixes https://github.com/microsoft/vscode-python/issues/22011 Note: this doesn't yet include Jedi support for Python 3.12 so it's likely we'll want to bump Jedi again once that support is released. --- .../jedilsp_requirements/requirements.txt | 92 ++++++------------- 1 file changed, 26 insertions(+), 66 deletions(-) diff --git a/pythonFiles/jedilsp_requirements/requirements.txt b/pythonFiles/jedilsp_requirements/requirements.txt index 64b1bb958f67..3759193344a6 100644 --- a/pythonFiles/jedilsp_requirements/requirements.txt +++ b/pythonFiles/jedilsp_requirements/requirements.txt @@ -13,33 +13,32 @@ attrs==23.1.0 \ cattrs==23.1.2 \ --hash=sha256:b2bb14311ac17bed0d58785e5a60f022e5431aca3932e3fc5cc8ed8639de50a4 \ --hash=sha256:db1c821b8c537382b2c7c66678c3790091ca0275ac486c76f3c8f3920e83c657 - # via lsprotocol + # via + # jedi-language-server + # lsprotocol docstring-to-markdown==0.12 \ --hash=sha256:40004224b412bd6f64c0f3b85bb357a41341afd66c4b4896709efa56827fb2bb \ --hash=sha256:7df6311a887dccf9e770f51242ec002b19f0591994c4783be49d24cdc1df3737 # via jedi-language-server -exceptiongroup==1.1.2 \ - --hash=sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5 \ - --hash=sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f +exceptiongroup==1.1.3 \ + --hash=sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9 \ + --hash=sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3 # via cattrs -importlib-metadata==3.10.1 \ - --hash=sha256:2ec0faae539743ae6aaa84b49a169670a465f7f5d64e6add98388cc29fd1f2f6 \ - --hash=sha256:c9356b657de65c53744046fa8f7358afe0714a1af7d570c00c3835c2d724a7c1 - # via - # attrs - # jedi-language-server - # typeguard -jedi==0.18.2 \ - --hash=sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e \ - --hash=sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612 +importlib-metadata==6.8.0 \ + --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ + --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743 + # via typeguard +jedi==0.19.0 \ + --hash=sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4 \ + --hash=sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e # via jedi-language-server -jedi-language-server==0.40.0 \ - --hash=sha256:53e590400b5cd2f6e363e77a4d824b1883798994b731cb0b4370d103748d30e2 \ - --hash=sha256:bacbae2930b6a8a0f1f284c211672fceec94b4808b0415d1c3352fa4b1ac5ad6 +jedi-language-server==0.41.1 \ + --hash=sha256:3f15ca5cc28e728564f7d63583e171b418025582447ce023512e3f2b2d71ebae \ + --hash=sha256:ca9b3e7f48b70f0988d85ffde4f01dd1ab94c8e0f69e8c6424e6657117b44f91 # via -r pythonFiles\jedilsp_requirements\requirements.in -lsprotocol==2023.0.0a2 \ - --hash=sha256:80aae7e39171b49025876a524937c10be2eb986f4be700ca22ee7d186b8488aa \ - --hash=sha256:c4f2f77712b50d065b17f9b50d2b88c480dc2ce4bbaa56eea8269dbf54bc9701 +lsprotocol==2023.0.0b1 \ + --hash=sha256:ade2cd0fa0ede7965698cb59cd05d3adbd19178fd73e83f72ef57a032fbb9d62 \ + --hash=sha256:f7a2d4655cbd5639f373ddd1789807450c543341fa0a32b064ad30dbb9f510d4 # via # jedi-language-server # pygls @@ -47,44 +46,6 @@ parso==0.8.3 \ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 # via jedi -pydantic==1.10.12 \ - --hash=sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303 \ - --hash=sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe \ - --hash=sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47 \ - --hash=sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494 \ - --hash=sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33 \ - --hash=sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86 \ - --hash=sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d \ - --hash=sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c \ - --hash=sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a \ - --hash=sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565 \ - --hash=sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb \ - --hash=sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62 \ - --hash=sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62 \ - --hash=sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0 \ - --hash=sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523 \ - --hash=sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d \ - --hash=sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405 \ - --hash=sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f \ - --hash=sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b \ - --hash=sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718 \ - --hash=sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed \ - --hash=sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb \ - --hash=sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5 \ - --hash=sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc \ - --hash=sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942 \ - --hash=sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe \ - --hash=sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246 \ - --hash=sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350 \ - --hash=sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303 \ - --hash=sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09 \ - --hash=sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33 \ - --hash=sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8 \ - --hash=sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a \ - --hash=sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1 \ - --hash=sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6 \ - --hash=sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d - # via jedi-language-server pygls==1.0.2 \ --hash=sha256:6d278d29fa6559b0f7a448263c85cb64ec6e9369548b02f1a7944060848b21f9 \ --hash=sha256:888ed63d1f650b4fc64d603d73d37545386ec533c0caac921aed80f80ea946a4 @@ -95,15 +56,14 @@ typeguard==3.0.2 \ --hash=sha256:bbe993854385284ab42fd5bd3bee6f6556577ce8b50696d6cb956d704f286c8e \ --hash=sha256:fee5297fdb28f8e9efcb8142b5ee219e02375509cd77ea9d270b5af826358d5a # via pygls -typing-extensions==4.7.1 \ - --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ - --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 +typing-extensions==4.8.0 \ + --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ + --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef # via # cattrs - # importlib-metadata - # pydantic + # jedi-language-server # typeguard -zipp==3.15.0 \ - --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ - --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 +zipp==3.17.0 \ + --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \ + --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0 # via importlib-metadata From 6d74f8d0c5a26a7b51429e44d9f19439d4aaddb2 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 27 Sep 2023 10:51:53 -0700 Subject: [PATCH 0213/1136] Ensure we don't show version selection when user selects useExisting (#22099) Closes https://github.com/microsoft/vscode-python/issues/22084 --- .../provider/condaCreationProvider.ts | 33 ++++++++++++------- .../condaCreationProvider.unit.test.ts | 23 +++++++++++++ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 86e0b56801cd..9dff50c5586d 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -200,21 +200,30 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise { - try { - version = await pickPythonVersion(); - } catch (ex) { - if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { - return ex; + async (context) => { + if ( + existingCondaAction === ExistingCondaAction.Recreate || + existingCondaAction === ExistingCondaAction.Create + ) { + try { + version = await pickPythonVersion(); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + if (version === undefined) { + traceError('Python version was not selected for creating conda environment.'); + return MultiStepAction.Cancel; + } + traceInfo(`Selected Python version ${version} for creating conda environment.`); + } else if (existingCondaAction === ExistingCondaAction.UseExisting) { + if (context === MultiStepAction.Back) { + return MultiStepAction.Back; } - throw ex; } - if (version === undefined) { - traceError('Python version was not selected for creating conda environment.'); - return MultiStepAction.Cancel; - } - traceInfo(`Selected Python version ${version} for creating conda environment.`); return MultiStepAction.Continue; }, undefined, diff --git a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts index 3195d1f88ea9..e1344dc5f3ad 100644 --- a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -36,6 +36,7 @@ suite('Conda Creation provider tests', () => { let withProgressStub: sinon.SinonStub; let showErrorMessageWithLogsStub: sinon.SinonStub; let pickExistingCondaActionStub: sinon.SinonStub; + let getPrefixCondaEnvPathStub: sinon.SinonStub; setup(() => { pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); @@ -50,6 +51,8 @@ suite('Conda Creation provider tests', () => { pickExistingCondaActionStub = sinon.stub(condaUtils, 'pickExistingCondaAction'); pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.Create); + getPrefixCondaEnvPathStub = sinon.stub(commonUtils, 'getPrefixCondaEnvPath'); + progressMock = typemoq.Mock.ofType(); condaProvider = condaCreationProvider(); }); @@ -254,4 +257,24 @@ suite('Conda Creation provider tests', () => { assert.isTrue(showErrorMessageWithLogsStub.calledOnce); assert.isTrue(pickExistingCondaActionStub.calledOnce); }); + + test('Use existing conda environment', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + pickWorkspaceFolderStub.resolves(workspace1); + pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.UseExisting); + getPrefixCondaEnvPathStub.returns('existing_environment'); + + const result = await condaProvider.createEnvironment(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(pickPythonVersionStub.notCalled); + assert.isTrue(execObservableStub.notCalled); + assert.isTrue(withProgressStub.notCalled); + + assert.deepStrictEqual(result, { path: 'existing_environment', workspaceFolder: workspace1 }); + }); }); From cc2a5678047a396d6d36092f8ff21182ca18e6b7 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 28 Sep 2023 09:47:41 -0700 Subject: [PATCH 0214/1136] fix regex split for subtest names (#22107) fixes https://github.com/microsoft/vscode-python/issues/21733. Handles both cases of subtest naming as described here by ChatGPT: When you use self.subTest(i=i), you're explicitly naming the argument i. This causes subTest to use the key=value format for the sub-test's description. Therefore, the sub-test name becomes: `test_subtests.NumbersTest2.test_even2 (i='h i')` However, when you use self.subTest(i), you're passing a positional argument. In this case, subTest doesn't have a key for the argument, so it simply uses the value in square brackets: `test_subtests.NumbersTest2.test_even2 [h i]` --- .../testController/common/resultResolver.ts | 19 +++---- .../testing/testController/common/utils.ts | 17 ++++++ .../testing/testController/utils.unit.test.ts | 56 +++++++++++++++++++ 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 78883bdcf8fb..79cee6452a8c 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -11,7 +11,7 @@ import { clearAllChildren, createErrorTestItem, getTestCaseNodes } from './testI import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { splitLines } from '../../../common/stringUtils'; -import { buildErrorNodeOptions, fixLogLines, populateTestTree } from './utils'; +import { buildErrorNodeOptions, fixLogLines, populateTestTree, splitTestNameWithRegex } from './utils'; import { Deferred } from '../../../common/utils/async'; export class PythonResultResolver implements ITestResultResolver { @@ -216,9 +216,8 @@ export class PythonResultResolver implements ITestResultResolver { }); } } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { - // split on " " since the subtest ID has the parent test ID in the first part of the ID. - const parentTestCaseId = keyTemp.split(' ')[0]; - const subtestId = keyTemp.split(' ')[1]; + // split on [] or () based on how the subtest is setup. + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); const data = rawTestExecData.result[keyTemp]; // find the subtest's parent test item @@ -227,7 +226,10 @@ export class PythonResultResolver implements ITestResultResolver { if (subtestStats) { subtestStats.failed += 1; } else { - this.subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 }); + this.subTestStats.set(parentTestCaseId, { + failed: 1, + passed: 0, + }); runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); @@ -253,11 +255,8 @@ export class PythonResultResolver implements ITestResultResolver { throw new Error('Parent test item not found'); } } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { - // split only on first " [" since the subtest ID has the parent test ID in the first part of the ID. - const index = keyTemp.indexOf(' ['); - const parentTestCaseId = keyTemp.substring(0, index); - // add one to index to remove the space from the start of the subtest ID - const subtestId = keyTemp.substring(index + 1, keyTemp.length); + // split on [] or () based on how the subtest is setup. + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); // find the subtest's parent test item diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 1272ff37fb5d..c58850050590 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -320,3 +320,20 @@ export function createEOTPayload(executionBool: boolean): EOTTestPayload { eot: true, } as EOTTestPayload; } + +/** + * Splits a test name into its parent test name and subtest unique section. + * + * @param testName The full test name string. + * @returns A tuple where the first item is the parent test name and the second item is the subtest section or `testName` if no subtest section exists. + */ +export function splitTestNameWithRegex(testName: string): [string, string] { + // If a match is found, return the parent test name and the subtest (whichever was captured between parenthesis or square brackets). + // Otherwise, return the entire testName for the parent and entire testName for the subtest. + const regex = /^(.*?) ([\[(].*[\])])$/; + const match = testName.match(regex); + if (match) { + return [match[1].trim(), match[2] || match[3] || testName]; + } + return [testName, testName]; +} diff --git a/src/test/testing/testController/utils.unit.test.ts b/src/test/testing/testController/utils.unit.test.ts index 9168abc7041f..12100252d1a9 100644 --- a/src/test/testing/testController/utils.unit.test.ts +++ b/src/test/testing/testController/utils.unit.test.ts @@ -8,6 +8,7 @@ import { JSONRPC_UUID_HEADER, ExtractJsonRPCData, parseJsonRPCHeadersAndData, + splitTestNameWithRegex, } from '../../../client/testing/testController/common/utils'; suite('Test Controller Utils: JSON RPC', () => { @@ -65,3 +66,58 @@ suite('Test Controller Utils: JSON RPC', () => { assert.deepStrictEqual(rpcContent.remainingRawData, rawDataString); }); }); + +suite('Test Controller Utils: Other', () => { + interface TestCase { + name: string; + input: string; + expectedParent: string; + expectedSubtest: string; + } + + const testCases: Array = [ + { + name: 'Single parameter, named', + input: 'test_package.ClassName.test_method (param=value)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param=value)', + }, + { + name: 'Single parameter, unnamed', + input: 'test_package.ClassName.test_method [value]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '[value]', + }, + { + name: 'Multiple parameters, named', + input: 'test_package.ClassName.test_method (param1=value1, param2=value2)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param1=value1, param2=value2)', + }, + { + name: 'Multiple parameters, unnamed', + input: 'test_package.ClassName.test_method [value1, value2]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '[value1, value2]', + }, + { + name: 'Names with special characters', + input: 'test_package.ClassName.test_method (param1=value/1, param2=value+2)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param1=value/1, param2=value+2)', + }, + { + name: 'Names with spaces', + input: 'test_package.ClassName.test_method ["a b c d"]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '["a b c d"]', + }, + ]; + + testCases.forEach((testCase) => { + test(`splitTestNameWithRegex: ${testCase.name}`, () => { + const splitResult = splitTestNameWithRegex(testCase.input); + assert.deepStrictEqual(splitResult, [testCase.expectedParent, testCase.expectedSubtest]); + }); + }); +}); From 0fe920f067ce67632a820d22687ad182c1550610 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 28 Sep 2023 11:10:21 -0700 Subject: [PATCH 0215/1136] ignore payload key-value if value is empty (#22105) fixes https://github.com/microsoft/vscode-python/issues/22104 --- .../testing/testController/common/utils.ts | 6 ++++-- .../testing/common/testingPayloadsEot.test.ts | 5 +++++ .../testController/payloadTestCases.ts | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index c58850050590..f34d0172abd6 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -114,8 +114,10 @@ export function parseJsonRPCHeadersAndData(rawData: string): ParsedRPCHeadersAnd break; } const [key, value] = line.split(':'); - if ([JSONRPC_UUID_HEADER, JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER].includes(key)) { - headerMap.set(key.trim(), value.trim()); + if (value.trim()) { + if ([JSONRPC_UUID_HEADER, JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER].includes(key)) { + headerMap.set(key.trim(), value.trim()); + } } } diff --git a/src/test/testing/common/testingPayloadsEot.test.ts b/src/test/testing/common/testingPayloadsEot.test.ts index 9025248c66ff..a30b1efe288c 100644 --- a/src/test/testing/common/testingPayloadsEot.test.ts +++ b/src/test/testing/common/testingPayloadsEot.test.ts @@ -27,6 +27,7 @@ import { PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY, DataWithPayloadChunks, PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY, + PAYLOAD_ONLY_HEADER_MULTI_CHUNK, } from '../testController/payloadTestCases'; import { traceLog } from '../../../client/logging'; @@ -37,6 +38,10 @@ export interface TestCase { } const testCases: Array = [ + { + name: 'header in single chunk edge case', + value: PAYLOAD_ONLY_HEADER_MULTI_CHUNK(FAKE_UUID), + }, { name: 'single payload single chunk', value: PAYLOAD_SINGLE_CHUNK(FAKE_UUID), diff --git a/src/test/testing/testController/payloadTestCases.ts b/src/test/testing/testController/payloadTestCases.ts index 5ddcc0edecf9..40b2113904dd 100644 --- a/src/test/testing/testController/payloadTestCases.ts +++ b/src/test/testing/testController/payloadTestCases.ts @@ -82,6 +82,25 @@ export function PAYLOAD_MULTI_CHUNK(uuid: string): DataWithPayloadChunks { }; } +// more than one payload, split so the first one is only 'Content-Length' to confirm headers +// with null values are ignored +export function PAYLOAD_ONLY_HEADER_MULTI_CHUNK(uuid: string): DataWithPayloadChunks { + const payloadArray: string[] = []; + const result = JSON.stringify(SINGLE_UNITTEST_SUBTEST.result); + + const val = createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + const firstSpaceIndex = val.indexOf(' '); + const payload1 = val.substring(0, firstSpaceIndex); + const payload2 = val.substring(firstSpaceIndex); + payloadArray.push(payload1); + payloadArray.push(payload2); + payloadArray.push(EOT_PAYLOAD); + return { + payloadArray, + data: result, + }; +} + // single payload divided by an arbitrary character and split across payloads export function PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(uuid: string): DataWithPayloadChunks { const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD); From c3214c0344fdb96dde38ff424f0abfe56af13a51 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 28 Sep 2023 11:22:19 -0700 Subject: [PATCH 0216/1136] switch to verbose for raw data logs (#22110) fixes https://github.com/microsoft/vscode-python/issues/22095 --- src/client/testing/testController/common/server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index d47587386069..1f9c0223d3fd 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -12,7 +12,7 @@ import { IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { traceError, traceInfo, traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; @@ -87,7 +87,7 @@ export class PythonTestServer implements ITestServer, Disposable { // what payload is so small it doesn't include the whole UUID think got this if (extractedJsonPayload.uuid !== undefined && extractedJsonPayload.cleanedJsonData !== undefined) { // if a full json was found in the buffer, fire the data received event then keep cycling with the remaining raw data. - traceLog(`Firing data received event, ${extractedJsonPayload.cleanedJsonData}`); + traceVerbose(`Firing data received event, ${extractedJsonPayload.cleanedJsonData}`); this._fireDataReceived(extractedJsonPayload.uuid, extractedJsonPayload.cleanedJsonData); } buffer = Buffer.from(extractedJsonPayload.remainingRawData); From 2579b15ca68461f578371e132f29747ad26130d6 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 28 Sep 2023 13:03:33 -0700 Subject: [PATCH 0217/1136] Fire active environment change event if selected environment is deleted (#22113) Closes https://github.com/microsoft/vscode-python/issues/22066 --- src/client/interpreter/interpreterService.ts | 29 +++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index b595fc2365a8..c97a35c4a973 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -85,6 +85,11 @@ export class InterpreterService implements Disposable, IInterpreterService { private readonly didChangeInterpreterInformation = new EventEmitter(); + private readonly activeInterpreterPaths = new Map< + string, + { path: string; workspaceFolder: WorkspaceFolder | undefined } + >(); + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, @@ -100,10 +105,12 @@ export class InterpreterService implements Disposable, IInterpreterService { const workspaceFolder = this.serviceContainer .get(IWorkspaceService) .getWorkspaceFolder(resource); - this.ensureEnvironmentContainsPython( - this.configService.getSettings(resource).pythonPath, - workspaceFolder, - ).ignoreErrors(); + const path = this.configService.getSettings(resource).pythonPath; + const workspaceKey = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path, workspaceFolder }); + this.ensureEnvironmentContainsPython(path, workspaceFolder).ignoreErrors(); } public initialize(): void { @@ -155,6 +162,16 @@ export class InterpreterService implements Disposable, IInterpreterService { const interpreter = e.old ?? e.new; if (interpreter) { this.didChangeInterpreterInformation.fire(interpreter); + for (const { path, workspaceFolder } of this.activeInterpreterPaths.values()) { + if (path === interpreter.path && !e.new) { + // If the active environment got deleted, notify it. + this.didChangeInterpreterEmitter.fire(workspaceFolder?.uri); + reportActiveInterpreterChanged({ + path, + resource: workspaceFolder, + }); + } + } } }), ); @@ -246,6 +263,10 @@ export class InterpreterService implements Disposable, IInterpreterService { path: pySettings.pythonPath, resource: workspaceFolder, }); + const workspaceKey = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path: pySettings.pythonPath, workspaceFolder }); const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); interpreterDisplay.refresh().catch((ex) => traceError('Python Extension: display.refresh', ex)); await this.ensureEnvironmentContainsPython(this._pythonPathSetting, workspaceFolder); From f577ce6d15af5a68a4c06494ea1500921bc81af3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:51:23 -0700 Subject: [PATCH 0218/1136] Align env type capitalization with tool recommendation (#22103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maybe it should also be Poetry? 🤷 Fixes #22094 --- src/client/pythonEnvironments/info/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index ee2ff9d7cc22..17e8958f6310 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -98,7 +98,7 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string return 'conda'; } case EnvironmentType.Pipenv: { - return 'pipenv'; + return 'Pipenv'; } case EnvironmentType.Pyenv: { return 'pyenv'; @@ -110,16 +110,16 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string return 'virtualenv'; } case EnvironmentType.MicrosoftStore: { - return 'microsoft store'; + return 'Microsoft Store'; } case EnvironmentType.Poetry: { - return 'poetry'; + return 'Poetry'; } case EnvironmentType.VirtualEnvWrapper: { return 'virtualenvwrapper'; } case EnvironmentType.ActiveState: { - return 'activestate'; + return 'ActiveState'; } default: { return ''; From e87a83cc34aa72ed6e29f29255c29ff4eb28978a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 28 Sep 2023 14:52:14 -0700 Subject: [PATCH 0219/1136] Correct display name for env kinds (#22115) Closes https://github.com/microsoft/vscode-python/issues/22094 --- src/client/pythonEnvironments/base/info/envKind.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/envKind.ts b/src/client/pythonEnvironments/base/info/envKind.ts index 8828003c5ce7..ff53a57d2f45 100644 --- a/src/client/pythonEnvironments/base/info/envKind.ts +++ b/src/client/pythonEnvironments/base/info/envKind.ts @@ -12,15 +12,15 @@ export function getKindDisplayName(kind: PythonEnvKind): string { for (const [candidate, value] of [ // Note that Unknown is excluded here. [PythonEnvKind.System, 'system'], - [PythonEnvKind.MicrosoftStore, 'microsoft store'], + [PythonEnvKind.MicrosoftStore, 'Microsoft Store'], [PythonEnvKind.Pyenv, 'pyenv'], - [PythonEnvKind.Poetry, 'poetry'], + [PythonEnvKind.Poetry, 'Poetry'], [PythonEnvKind.Custom, 'custom'], // For now we treat OtherGlobal like Unknown. [PythonEnvKind.Venv, 'venv'], [PythonEnvKind.VirtualEnv, 'virtualenv'], [PythonEnvKind.VirtualEnvWrapper, 'virtualenv'], - [PythonEnvKind.Pipenv, 'pipenv'], + [PythonEnvKind.Pipenv, 'Pipenv'], [PythonEnvKind.Conda, 'conda'], [PythonEnvKind.ActiveState, 'ActiveState'], // For now we treat OtherVirtual like Unknown. From 66c7db6e369ce02a410deca0913730a87f74df1b Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 29 Sep 2023 15:05:02 -0700 Subject: [PATCH 0220/1136] check existence of value in header before trim process json prc (#22116) Made extra tests to validate that `parseJsonRPCHeadersAndData` works as expected and uncovered a bug. Added check to see if the value is null before trim. --- .../testing/testController/common/utils.ts | 2 +- .../testing/testController/utils.unit.test.ts | 142 +++++++++++------- 2 files changed, 91 insertions(+), 53 deletions(-) diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index f34d0172abd6..f5f416529c42 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -114,7 +114,7 @@ export function parseJsonRPCHeadersAndData(rawData: string): ParsedRPCHeadersAnd break; } const [key, value] = line.split(':'); - if (value.trim()) { + if (value && value.trim()) { if ([JSONRPC_UUID_HEADER, JSONRPC_CONTENT_LENGTH_HEADER, JSONRPC_CONTENT_TYPE_HEADER].includes(key)) { headerMap.set(key.trim(), value.trim()); } diff --git a/src/test/testing/testController/utils.unit.test.ts b/src/test/testing/testController/utils.unit.test.ts index 12100252d1a9..014261a40232 100644 --- a/src/test/testing/testController/utils.unit.test.ts +++ b/src/test/testing/testController/utils.unit.test.ts @@ -65,59 +65,97 @@ suite('Test Controller Utils: JSON RPC', () => { assert.deepStrictEqual(rpcContent.extractedJSON, json); assert.deepStrictEqual(rpcContent.remainingRawData, rawDataString); }); -}); -suite('Test Controller Utils: Other', () => { - interface TestCase { - name: string; - input: string; - expectedParent: string; - expectedSubtest: string; - } - - const testCases: Array = [ - { - name: 'Single parameter, named', - input: 'test_package.ClassName.test_method (param=value)', - expectedParent: 'test_package.ClassName.test_method', - expectedSubtest: '(param=value)', - }, - { - name: 'Single parameter, unnamed', - input: 'test_package.ClassName.test_method [value]', - expectedParent: 'test_package.ClassName.test_method', - expectedSubtest: '[value]', - }, - { - name: 'Multiple parameters, named', - input: 'test_package.ClassName.test_method (param1=value1, param2=value2)', - expectedParent: 'test_package.ClassName.test_method', - expectedSubtest: '(param1=value1, param2=value2)', - }, - { - name: 'Multiple parameters, unnamed', - input: 'test_package.ClassName.test_method [value1, value2]', - expectedParent: 'test_package.ClassName.test_method', - expectedSubtest: '[value1, value2]', - }, - { - name: 'Names with special characters', - input: 'test_package.ClassName.test_method (param1=value/1, param2=value+2)', - expectedParent: 'test_package.ClassName.test_method', - expectedSubtest: '(param1=value/1, param2=value+2)', - }, - { - name: 'Names with spaces', - input: 'test_package.ClassName.test_method ["a b c d"]', - expectedParent: 'test_package.ClassName.test_method', - expectedSubtest: '["a b c d"]', - }, - ]; - - testCases.forEach((testCase) => { - test(`splitTestNameWithRegex: ${testCase.name}`, () => { - const splitResult = splitTestNameWithRegex(testCase.input); - assert.deepStrictEqual(splitResult, [testCase.expectedParent, testCase.expectedSubtest]); + test('Valid constant', async () => { + const data = `{"cwd": "/Users/eleanorboyd/testingFiles/inc_dec_example", "status": "success", "result": {"test_dup_class.test_a.TestSomething.test_a": {"test": "test_dup_class.test_a.TestSomething.test_a", "outcome": "success", "message": "None", "traceback": null, "subtest": null}}}`; + const secondPayload = `Content-Length: 270 +Content-Type: application/json +Request-uuid: 496c86b1-608f-4886-9436-ec00538e144c + +${data}`; + const payload = `Content-Length: 270 +Content-Type: application/json +Request-uuid: 496c86b1-608f-4886-9436-ec00538e144c + +${data}${secondPayload}`; + + const rpcHeaders = parseJsonRPCHeadersAndData(payload); + assert.deepStrictEqual(rpcHeaders.headers.size, 3); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, data); + assert.deepStrictEqual(rpcContent.remainingRawData, secondPayload); + }); + test('Valid content length as only header with carriage return', async () => { + const payload = `Content-Length: 7 + `; + + const rpcHeaders = parseJsonRPCHeadersAndData(payload); + assert.deepStrictEqual(rpcHeaders.headers.size, 1); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, ''); + assert.deepStrictEqual(rpcContent.remainingRawData, ''); + }); + test('Valid content length header with no value', async () => { + const payload = `Content-Length:`; + + const rpcHeaders = parseJsonRPCHeadersAndData(payload); + const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); + assert.deepStrictEqual(rpcContent.extractedJSON, ''); + assert.deepStrictEqual(rpcContent.remainingRawData, ''); + }); + + suite('Test Controller Utils: Other', () => { + interface TestCase { + name: string; + input: string; + expectedParent: string; + expectedSubtest: string; + } + + const testCases: Array = [ + { + name: 'Single parameter, named', + input: 'test_package.ClassName.test_method (param=value)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param=value)', + }, + { + name: 'Single parameter, unnamed', + input: 'test_package.ClassName.test_method [value]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '[value]', + }, + { + name: 'Multiple parameters, named', + input: 'test_package.ClassName.test_method (param1=value1, param2=value2)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param1=value1, param2=value2)', + }, + { + name: 'Multiple parameters, unnamed', + input: 'test_package.ClassName.test_method [value1, value2]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '[value1, value2]', + }, + { + name: 'Names with special characters', + input: 'test_package.ClassName.test_method (param1=value/1, param2=value+2)', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '(param1=value/1, param2=value+2)', + }, + { + name: 'Names with spaces', + input: 'test_package.ClassName.test_method ["a b c d"]', + expectedParent: 'test_package.ClassName.test_method', + expectedSubtest: '["a b c d"]', + }, + ]; + + testCases.forEach((testCase) => { + test(`splitTestNameWithRegex: ${testCase.name}`, () => { + const splitResult = splitTestNameWithRegex(testCase.input); + assert.deepStrictEqual(splitResult, [testCase.expectedParent, testCase.expectedSubtest]); + }); }); }); }); From aeee067b7aef2c9f52d81031e0e7da408a47aa3b Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 2 Oct 2023 11:30:40 -0700 Subject: [PATCH 0221/1136] Update version and packages for release candidate (#22127) --- package-lock.json | 93 +++++++++++++++++++++------------- package.json | 2 +- pythonFiles/install_debugpy.py | 2 +- 3 files changed, 59 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70205a90f00e..8885d4532477 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.17.0-dev", + "version": "2023.18.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.17.0-dev", + "version": "2023.18.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", @@ -128,7 +128,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.82.0-20230830" + "vscode": "^1.82.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -5795,13 +5795,20 @@ } }, "node_modules/eslint/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/eslint/node_modules/doctrine": { @@ -7002,9 +7009,9 @@ "dev": true }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" @@ -8735,13 +8742,20 @@ } }, "node_modules/istanbul-lib-source-maps/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/istanbul-reports": { @@ -10193,13 +10207,20 @@ } }, "node_modules/nock/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/nock/node_modules/semver": { @@ -19727,12 +19748,12 @@ } }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "doctrine": { @@ -20941,9 +20962,9 @@ "dev": true }, "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true }, "get-intrinsic": { @@ -22251,12 +22272,12 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } } } @@ -23408,12 +23429,12 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "semver": { diff --git a/package.json b/package.json index ef9d0aa6c033..5d7120a74fa2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", - "version": "2023.17.0-dev", + "version": "2023.18.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, diff --git a/pythonFiles/install_debugpy.py b/pythonFiles/install_debugpy.py index cabb620ea1f2..9377d00237d7 100644 --- a/pythonFiles/install_debugpy.py +++ b/pythonFiles/install_debugpy.py @@ -13,7 +13,7 @@ DEBUGGER_DEST = os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "python") DEBUGGER_PACKAGE = "debugpy" DEBUGGER_PYTHON_ABI_VERSIONS = ("cp310",) -DEBUGGER_VERSION = "1.6.7" # can also be "latest" +DEBUGGER_VERSION = "1.8.0" # can also be "latest" def _contains(s, parts=()): From 4a3f855c292384fc14c157961b46be8b35db5b7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 11:39:14 -0700 Subject: [PATCH 0222/1136] Bump packaging from 23.1 to 23.2 (#22124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [packaging](https://github.com/pypa/packaging) from 23.1 to 23.2.
Release notes

Sourced from packaging's releases.

23.2

What's Changed

New Contributors

Full Changelog: https://github.com/pypa/packaging/compare/23.1...23.2

Changelog

Sourced from packaging's changelog.

23.2 - 2023-10-01


* Document calendar-based versioning scheme (:issue:`716`)
* Enforce that the entire marker string is parsed (:issue:`687`)
* Requirement parsing no longer automatically validates the URL
(:issue:`120`)
* Canonicalize names for requirements comparison (:issue:`644`)
* Introduce ``metadata.Metadata`` (along with
``metadata.ExceptionGroup`` and ``metadata.InvalidMetadata``;
:issue:`570`)
* Introduce the ``validate`` keyword parameter to
``utils.validate_name()`` (:issue:`570`)
* Introduce ``utils.is_normalized_name()`` (:issue:`570`)
* Make ``utils.parse_sdist_filename()`` and
``utils.parse_wheel_filename()``
raise ``InvalidSdistFilename`` and ``InvalidWheelFilename``,
respectively,
  when the version component of the name is invalid
Commits
  • b3a5d7d Bump for release
  • d7ce40d Fix code blocks in CHANGELOG.md (#724)
  • 524b701 parse_{sdist,wheel}_filename: don't raise InvalidVersion (#721)
  • b509bef Typing annotations fixed (#723)
  • 0206c39 Bump pip version to avoid known vulnerabilities (#720)
  • 7023537 fix: Update copyright date for docs (#713)
  • 39786bb Document use of calendar-based versioning scheme (#717)
  • c1346df fix: Detect when a platform is 32-bit more accurately (#711)
  • 7e68d82 Correct rST syntax in CHANGELOG.rst (#709)
  • 61e6efb Support enriched metadata in packaging.metadata (#686)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=packaging&package-manager=pip&previous-version=23.1&new-version=23.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8ea311d28cb1..205b9fc4804c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,9 +12,9 @@ microvenv==2023.2.0 \ --hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \ --hash=sha256:a07e88a8fb5ee90219b86dd90095cb5646462d45d30285ea3b1a3c7cf33616d3 # via -r requirements.in -packaging==23.1 \ - --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \ - --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f +packaging==23.2 \ + --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ + --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 # via -r requirements.in tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ @@ -23,9 +23,7 @@ tomli==2.0.1 \ typing-extensions==4.7.1 \ --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 - # via - # -r requirements.in - # importlib-metadata + # via -r requirements.in zipp==3.15.0 \ --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 From 4f82418173be3b989e07fe2cbb076b006ad8fc83 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 2 Oct 2023 11:51:07 -0700 Subject: [PATCH 0223/1136] Update version for pre-release (#22129) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8885d4532477..2352bcf96c31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2023.18.0-rc", + "version": "2023.19.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2023.18.0-rc", + "version": "2023.19.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 5d7120a74fa2..c70a3023c267 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "IntelliSense (Pylance), Linting, Debugging (multi-threaded, remote), code formatting, refactoring, unit tests, and more.", - "version": "2023.18.0-rc", + "version": "2023.19.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 590c12a1a5150490d32fbe0b468a11a7df1daec7 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 2 Oct 2023 13:12:41 -0700 Subject: [PATCH 0224/1136] switch end to end tests to randomized substring (#22114) add in tests which are randomized to provide more testing for the issue that created `https://github.com/microsoft/vscode-python/issues/22104` --- .../testController/payloadTestCases.ts | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/test/testing/testController/payloadTestCases.ts b/src/test/testing/testController/payloadTestCases.ts index 40b2113904dd..f7f94a926f5f 100644 --- a/src/test/testing/testController/payloadTestCases.ts +++ b/src/test/testing/testController/payloadTestCases.ts @@ -50,6 +50,21 @@ const SINGLE_PYTEST_PAYLOAD_TWO = { }, }; +function splitIntoRandomSubstrings(payload: string): string[] { + // split payload at random + const splitPayload = []; + const n = payload.length; + let remaining = n; + while (remaining > 0) { + // Randomly split what remains of the string + const randomSize = Math.floor(Math.random() * remaining) + 1; + splitPayload.push(payload.slice(n - remaining, n - remaining + randomSize)); + + remaining -= randomSize; + } + return splitPayload; +} + export function createPayload(uuid: string, data: unknown): string { return `Content-Length: ${JSON.stringify(data).length} Content-Type: application/json @@ -104,13 +119,7 @@ export function PAYLOAD_ONLY_HEADER_MULTI_CHUNK(uuid: string): DataWithPayloadCh // single payload divided by an arbitrary character and split across payloads export function PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(uuid: string): DataWithPayloadChunks { const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD); - // payload length is know to be >200 - const splitPayload: Array = [ - payload.substring(0, 50), - payload.substring(50, 100), - payload.substring(100, 150), - payload.substring(150), - ]; + const splitPayload = splitIntoRandomSubstrings(payload); const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result); splitPayload.push(EOT_PAYLOAD); return { @@ -121,12 +130,8 @@ export function PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(uuid: string): DataWithPayload // here a payload is split across the buffer chunks and there are multiple payloads in a single buffer chunk export function PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY(uuid: string): DataWithPayloadChunks { - // payload1 length is know to be >200 - const payload1 = createPayload(uuid, SINGLE_PYTEST_PAYLOAD); - const payload2 = createPayload(uuid, SINGLE_PYTEST_PAYLOAD_TWO); - - // chunk 1 is 50 char of payload1, chunk 2 is 50-end of payload1 and all of payload2 - const splitPayload: Array = [payload1.substring(0, 100), payload1.substring(100).concat(payload2)]; + const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD).concat(createPayload(uuid, SINGLE_PYTEST_PAYLOAD_TWO)); + const splitPayload = splitIntoRandomSubstrings(payload); const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result).concat( JSON.stringify(SINGLE_PYTEST_PAYLOAD_TWO.result), ); From add82a0a773d5f38e294852d15a3a2eb9f90c1cc Mon Sep 17 00:00:00 2001 From: Peter Law Date: Mon, 2 Oct 2023 21:54:34 +0100 Subject: [PATCH 0225/1136] Bump Jedi to 0.19.1 for Python 3.12 support (#22132) Follows from https://github.com/microsoft/vscode-python/issues/22011#issuecomment-1742682966 --- pythonFiles/jedilsp_requirements/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pythonFiles/jedilsp_requirements/requirements.txt b/pythonFiles/jedilsp_requirements/requirements.txt index 3759193344a6..889f021670bf 100644 --- a/pythonFiles/jedilsp_requirements/requirements.txt +++ b/pythonFiles/jedilsp_requirements/requirements.txt @@ -28,9 +28,9 @@ importlib-metadata==6.8.0 \ --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743 # via typeguard -jedi==0.19.0 \ - --hash=sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4 \ - --hash=sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e +jedi==0.19.1 \ + --hash=sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd \ + --hash=sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0 # via jedi-language-server jedi-language-server==0.41.1 \ --hash=sha256:3f15ca5cc28e728564f7d63583e171b418025582447ce023512e3f2b2d71ebae \ From fc62bd8d9a2431eb6199decf58c88653de3f9a37 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 2 Oct 2023 15:21:15 -0700 Subject: [PATCH 0226/1136] Migrate extension to node 18 (#22135) --- .devcontainer/Dockerfile | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/pr-check.yml | 2 +- .nvmrc | 2 +- build/azure-pipeline.pre-release.yml | 2 +- build/azure-pipeline.stable.yml | 2 +- build/azure-pipelines/pipeline.yml | 6 +- gulpfile.js | 21 +- package-lock.json | 352 +++++------------- package.json | 5 +- pythonExtensionApi/package-lock.json | 103 ++++- pythonExtensionApi/package.json | 5 +- .../environmentManagers/conda.unit.test.ts | 1 + src/test/standardTest.ts | 7 +- 14 files changed, 219 insertions(+), 293 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 5fbf068de65f..3e7e9e9cf091 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/typescript-node:16-bookworm +FROM mcr.microsoft.com/devcontainers/typescript-node:18-bookworm RUN apt-get install -y wget bzip2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d121564d385..d1509a7b433e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ on: - 'release-*' env: - NODE_VERSION: 16.17.1 + NODE_VERSION: 18.17.1 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 # Force a path with spaces and to test extension works in these scenarios # Unicode characters are causing 2.7 failures so skip that for now. diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index aa223782de62..b7d2ed0c0545 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -8,7 +8,7 @@ on: - release* env: - NODE_VERSION: 16.17.1 + NODE_VERSION: 18.17.1 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). Also enables a reporter which exits the process running the tests if it haven't already. ARTIFACT_NAME_VSIX: ms-python-insiders-vsix diff --git a/.nvmrc b/.nvmrc index e0325e5adb60..860cc5000ae6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16.17.1 +v18.17.1 diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index eed32b70c35d..bb52f983d02e 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -33,7 +33,7 @@ extends: buildSteps: - task: NodeTool@0 inputs: - versionSpec: '16.17.1' + versionSpec: '18.17.1' displayName: Select Node version - task: UsePythonVersion@0 diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index c147f8b55164..02f8bd38cf81 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -28,7 +28,7 @@ extends: buildSteps: - task: NodeTool@0 inputs: - versionSpec: '16.17.1' + versionSpec: '18.17.1' displayName: Select Node version - task: UsePythonVersion@0 diff --git a/build/azure-pipelines/pipeline.yml b/build/azure-pipelines/pipeline.yml index 85b41c16efc0..adb2fa5d1c30 100644 --- a/build/azure-pipelines/pipeline.yml +++ b/build/azure-pipelines/pipeline.yml @@ -37,13 +37,13 @@ extends: testPlatforms: - name: Linux nodeVersions: - - 16.17.1 + - 18.17.1 - name: MacOS nodeVersions: - - 16.17.1 + - 18.17.1 - name: Windows nodeVersions: - - 16.17.1 + - 18.17.1 testSteps: - template: /build/azure-pipelines/templates/test-steps.yml@self parameters: diff --git a/gulpfile.js b/gulpfile.js index 66f96bf48ec0..4dcc03252d16 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -39,18 +39,19 @@ gulp.task('compileCore', (done) => { .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); }); -const apiTsProject = ts.createProject('./pythonExtensionApi/tsconfig.json', { typescript }); - gulp.task('compileApi', (done) => { - let failed = false; - apiTsProject - .src() - .pipe(apiTsProject()) - .on('error', () => { - failed = true; + spawnAsync('npm', ['run', 'compileApi'], undefined, true) + .then((stdout) => { + if (stdout.includes('error')) { + done(new Error(stdout)); + } else { + done(); + } }) - .js.pipe(gulp.dest('./pythonExtensionApi/out')) - .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); + .catch((ex) => { + console.log(ex); + done(new Error('TypeScript compilation errors', ex)); + }); }); gulp.task('compile', gulp.series('compileCore', 'compileApi')); diff --git a/package-lock.json b/package-lock.json index 2352bcf96c31..5d1ee32bc08b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,7 @@ "@types/md5": "^2.1.32", "@types/mocha": "^9.1.0", "@types/nock": "^10.0.3", - "@types/node": "^16.17.0", + "@types/node": "^18.17.1", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^10.0.11", @@ -73,7 +73,7 @@ "@types/xml2js": "^0.4.2", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", - "@vscode/test-electron": "^2.1.3", + "@vscode/test-electron": "^2.3.4", "@vscode/vsce": "^2.18.0", "bent": "^7.3.12", "chai": "^4.1.2", @@ -1553,9 +1553,9 @@ } }, "node_modules/@types/node": { - "version": "16.18.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.25.tgz", - "integrity": "sha512-rUDO6s9Q/El1R1I21HG4qw/LstTHCPO/oQNAwI/4b2f9EWvMnqt4d3HJwPMawfZ3UvodB8516Yg+VAq54YM+eA==", + "version": "18.17.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.14.tgz", + "integrity": "sha512-ZE/5aB73CyGqgQULkLG87N9GnyGe5TcQjv34pwS8tfBs1IkCh0ASM69mydb2znqd6v0eX+9Ytvk6oQRqu8T1Vw==", "dev": true }, "node_modules/@types/semver": { @@ -1854,18 +1854,18 @@ } }, "node_modules/@vscode/test-electron": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.1.3.tgz", - "integrity": "sha512-ps/yJ/9ToUZtR1dHfWi1mDXtep1VoyyrmGKC3UnIbScToRQvbUjyy1VMqnMEW3EpMmC3g7+pyThIPtPyCLHyow==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.4.tgz", + "integrity": "sha512-eWzIqXMhvlcoXfEFNWrVu/yYT5w6De+WZXR/bafUQhAp8+8GkQo95Oe14phwiRUPv8L+geAKl/QM2+PoT3YW3g==", "dev": true, "dependencies": { "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" + "jszip": "^3.10.1", + "semver": "^7.5.2" }, "engines": { - "node": ">=8.9.3" + "node": ">=16" } }, "node_modules/@vscode/vsce": { @@ -3023,15 +3023,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/big-integer": { - "version": "1.6.49", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.49.tgz", - "integrity": "sha512-KJ7VhqH+f/BOt9a3yMwJNmcZjG53ijWMTjSAGMveQWyLwqIiwkjNP5PFgDob3Snnx86SjDj6I89fIbv0dkQeNw==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -3041,19 +3032,6 @@ "node": "*" } }, - "node_modules/binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - }, - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3220,12 +3198,6 @@ "pako": "~1.0.5" } }, - "node_modules/browserify-zlib/node_modules/pako": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", - "dev": true - }, "node_modules/browserslist": { "version": "4.21.9", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", @@ -3319,30 +3291,12 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, - "node_modules/buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, "node_modules/buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, - "node_modules/buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true, - "engines": { - "node": ">=0.2.0" - } - }, "node_modules/builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -3501,18 +3455,6 @@ "chai": ">= 2.1.2 < 5" } }, - "node_modules/chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "dependencies": { - "traverse": ">=0.3.0 <0.4" - }, - "engines": { - "node": "*" - } - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5010,15 +4952,6 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.2" - } - }, "node_modules/duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -6955,33 +6888,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/fstream/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -7902,6 +7808,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -8926,6 +8838,18 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/just-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", @@ -9054,6 +8978,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -9082,12 +9015,6 @@ "uc.micro": "^1.0.1" } }, - "node_modules/listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true - }, "node_modules/load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -11144,6 +11071,12 @@ "node": ">=8" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -13685,15 +13618,6 @@ "node": ">=6" } }, - "node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", @@ -14342,30 +14266,6 @@ "node": ">=8" } }, - "node_modules/unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, - "node_modules/unzipper/node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - }, "node_modules/upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", @@ -16647,9 +16547,9 @@ } }, "@types/node": { - "version": "16.18.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.25.tgz", - "integrity": "sha512-rUDO6s9Q/El1R1I21HG4qw/LstTHCPO/oQNAwI/4b2f9EWvMnqt4d3HJwPMawfZ3UvodB8516Yg+VAq54YM+eA==", + "version": "18.17.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.14.tgz", + "integrity": "sha512-ZE/5aB73CyGqgQULkLG87N9GnyGe5TcQjv34pwS8tfBs1IkCh0ASM69mydb2znqd6v0eX+9Ytvk6oQRqu8T1Vw==", "dev": true }, "@types/semver": { @@ -16863,15 +16763,15 @@ } }, "@vscode/test-electron": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.1.3.tgz", - "integrity": "sha512-ps/yJ/9ToUZtR1dHfWi1mDXtep1VoyyrmGKC3UnIbScToRQvbUjyy1VMqnMEW3EpMmC3g7+pyThIPtPyCLHyow==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.4.tgz", + "integrity": "sha512-eWzIqXMhvlcoXfEFNWrVu/yYT5w6De+WZXR/bafUQhAp8+8GkQo95Oe14phwiRUPv8L+geAKl/QM2+PoT3YW3g==", "dev": true, "requires": { "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" + "jszip": "^3.10.1", + "semver": "^7.5.2" } }, "@vscode/vsce": { @@ -17786,28 +17686,12 @@ } } }, - "big-integer": { - "version": "1.6.49", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.49.tgz", - "integrity": "sha512-KJ7VhqH+f/BOt9a3yMwJNmcZjG53ijWMTjSAGMveQWyLwqIiwkjNP5PFgDob3Snnx86SjDj6I89fIbv0dkQeNw==", - "dev": true - }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -17965,14 +17849,6 @@ "dev": true, "requires": { "pako": "~1.0.5" - }, - "dependencies": { - "pako": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", - "dev": true - } } }, "browserslist": { @@ -18031,24 +17907,12 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, - "buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true - }, "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true - }, "builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -18171,15 +18035,6 @@ "check-error": "^1.0.2" } }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "requires": { - "traverse": ">=0.3.0 <0.4" - } - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -19389,15 +19244,6 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -20915,29 +20761,6 @@ "dev": true, "optional": true }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -21650,6 +21473,12 @@ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -22411,6 +22240,18 @@ "object.assign": "^4.1.2" } }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "just-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", @@ -22517,6 +22358,15 @@ "type-check": "~0.4.0" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "requires": { + "immediate": "~3.0.5" + } + }, "liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -22542,12 +22392,6 @@ "uc.micro": "^1.0.1" } }, - "listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true - }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -24156,6 +24000,12 @@ "release-zalgo": "^1.0.0" } }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -26137,12 +25987,6 @@ "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", "dev": true }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true - }, "trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", @@ -26636,32 +26480,6 @@ "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==" }, - "unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dev": true, - "requires": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - }, - "dependencies": { - "bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - } - } - }, "upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", diff --git a/package.json b/package.json index c70a3023c267..47faec663015 100644 --- a/package.json +++ b/package.json @@ -2032,6 +2032,7 @@ "package": "gulp clean && gulp prePublishBundle && vsce package -o ms-python-insiders.vsix", "prePublish": "gulp clean && gulp prePublishNonBundle", "compile": "tsc -watch -p ./", + "compileApi": "node ./node_modules/typescript/lib/tsc.js -b ./pythonExtensionApi/tsconfig.json", "compiled": "deemon npm run compile", "kill-compiled": "deemon --kill npm run compile", "checkDependencies": "gulp checkDependencies", @@ -2116,7 +2117,7 @@ "@types/md5": "^2.1.32", "@types/mocha": "^9.1.0", "@types/nock": "^10.0.3", - "@types/node": "^16.17.0", + "@types/node": "^18.17.1", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^10.0.11", @@ -2129,7 +2130,7 @@ "@types/xml2js": "^0.4.2", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", - "@vscode/test-electron": "^2.1.3", + "@vscode/test-electron": "^2.3.4", "@vscode/vsce": "^2.18.0", "bent": "^7.3.12", "chai": "^4.1.2", diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index 1f4098e1b0de..ef6914e0e786 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -10,10 +10,11 @@ "license": "MIT", "devDependencies": { "@types/vscode": "^1.78.0", + "source-map": "^0.8.0-beta.0", "typescript": "5.0.4" }, "engines": { - "node": ">=16.17.1", + "node": ">=18.17.1", "vscode": "^1.78.0" } }, @@ -23,6 +24,42 @@ "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==", "dev": true }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/typescript": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", @@ -35,6 +72,23 @@ "engines": { "node": ">=12.20" } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } } }, "dependencies": { @@ -44,11 +98,58 @@ "integrity": "sha512-qK/CmOdS2o7ry3k6YqU4zD3R2AYlJfbwBoSbKpBoP+GpXNE+0NEgJOli4n0bm0diK5kfBnchgCEj4igQz/44Hg==", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "requires": { + "whatwg-url": "^7.0.0" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, "typescript": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } } } } diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index baabf85d6549..9e58f1a2400c 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -13,7 +13,7 @@ "main": "./out/main.js", "types": "./out/main.d.ts", "engines": { - "node": ">=16.17.1", + "node": ">=18.17.1", "vscode": "^1.78.0" }, "license": "MIT", @@ -27,7 +27,8 @@ }, "devDependencies": { "typescript": "5.0.4", - "@types/vscode": "^1.78.0" + "@types/vscode": "^1.78.0", + "source-map": "^0.8.0-beta.0" }, "scripts": { "prepublishOnly": "echo \"⛔ Can only publish from a secure pipeline ⛔\" && node ../build/fail", diff --git a/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts index ca0e24d5f3d3..1e9de68ad77a 100644 --- a/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts @@ -156,6 +156,7 @@ suite('Conda and its environments are located correctly', () => { const isFile = typeof dir[name] === 'string'; return { name, + path: dir.name?.toString() ?? '', isFile: () => isFile, isDirectory: () => !isFile, isBlockDevice: () => false, diff --git a/src/test/standardTest.ts b/src/test/standardTest.ts index 0562d1adf431..0fe53437cf3d 100644 --- a/src/test/standardTest.ts +++ b/src/test/standardTest.ts @@ -6,6 +6,7 @@ import { downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath, runTest import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../client/common/constants'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; import { getChannel } from './utils/vscode'; +import { TestOptions } from '@vscode/test-electron/out/runTest'; // If running smoke tests, we don't have access to this. if (process.env.TEST_FILES_SUFFIX !== 'smoke.test') { @@ -85,18 +86,20 @@ async function start() { : ['--disable-extensions']; await installJupyterExtension(vscodeExecutablePath); await installPylanceExtension(vscodeExecutablePath); + console.log('VS Code executable', vscodeExecutablePath); const launchArgs = baseLaunchArgs .concat([workspacePath]) .concat(channel === 'insiders' ? ['--enable-proposed-api'] : []) .concat(['--timeout', '5000']); console.log(`Starting vscode ${channel} with args ${launchArgs.join(' ')}`); - await runTests({ + const options: TestOptions = { extensionDevelopmentPath: extensionDevelopmentPath, extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test'), launchArgs, version: channel, extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, - }); + }; + await runTests(options); } start().catch((ex) => { console.error('End Standard tests (with errors)', ex); From a3633810b5647008c1b89ea3c6f2d466139909ba Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 3 Oct 2023 12:55:07 -0700 Subject: [PATCH 0227/1136] switch to using envvars for port and uuid in unittest (#22131) closes https://github.com/microsoft/vscode-python/issues/22130 --- .../tests/unittestadapter/test_discovery.py | 30 +------------ .../tests/unittestadapter/test_execution.py | 40 +---------------- pythonFiles/unittestadapter/discovery.py | 36 ++++----------- pythonFiles/unittestadapter/execution.py | 45 +++++-------------- pythonFiles/vscode_pytest/__init__.py | 4 +- .../testing/testController/common/server.ts | 13 +++--- .../testController/server.unit.test.ts | 18 ++++++-- 7 files changed, 45 insertions(+), 141 deletions(-) diff --git a/pythonFiles/tests/unittestadapter/test_discovery.py b/pythonFiles/tests/unittestadapter/test_discovery.py index c4778aa85852..67e52f43b70c 100644 --- a/pythonFiles/tests/unittestadapter/test_discovery.py +++ b/pythonFiles/tests/unittestadapter/test_discovery.py @@ -6,39 +6,13 @@ from typing import List import pytest -from unittestadapter.discovery import ( - DEFAULT_PORT, - discover_tests, - parse_discovery_cli_args, -) +from unittestadapter.discovery import discover_tests from unittestadapter.utils import TestNodeTypeEnum, parse_unittest_args + from . import expected_discovery_test_output from .helpers import TEST_DATA_PATH, is_same_tree -@pytest.mark.parametrize( - "args, expected", - [ - (["--port", "6767", "--uuid", "some-uuid"], (6767, "some-uuid")), - (["--foo", "something", "--bar", "another"], (int(DEFAULT_PORT), None)), - (["--port", "4444", "--foo", "something", "--port", "9999"], (9999, None)), - ( - ["--uuid", "first-uuid", "--bar", "other", "--uuid", "second-uuid"], - (int(DEFAULT_PORT), "second-uuid"), - ), - ], -) -def test_parse_cli_args(args: List[str], expected: List[str]) -> None: - """The parse_cli_args function should parse and return the port and uuid passed as command-line options. - - If there were no --port or --uuid command-line option, it should return default values). - If there are multiple options, the last one wins. - """ - actual = parse_discovery_cli_args(args) - - assert expected == actual - - @pytest.mark.parametrize( "args, expected", [ diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py index 057f64d7396a..f7306e37662e 100644 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -4,55 +4,17 @@ import os import pathlib import sys -from typing import List import pytest script_dir = pathlib.Path(__file__).parent.parent sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from unittestadapter.execution import parse_execution_cli_args, run_tests +from unittestadapter.execution import run_tests TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" -@pytest.mark.parametrize( - "args, expected", - [ - ( - [ - "--port", - "111", - "--uuid", - "fake-uuid", - ], - (111, "fake-uuid"), - ), - ( - ["--port", "111", "--uuid", "fake-uuid"], - (111, "fake-uuid"), - ), - ( - [ - "--port", - "111", - "--uuid", - "fake-uuid", - "-v", - "-s", - ], - (111, "fake-uuid"), - ), - ], -) -def test_parse_execution_cli_args(args: List[str], expected: List[str]) -> None: - """The parse_execution_cli_args function should return values for the port, uuid, and testids arguments - when passed as command-line options, and ignore unrecognized arguments. - """ - actual = parse_execution_cli_args(args) - assert actual == expected - - def test_no_ids_run() -> None: """This test runs on an empty array of test_ids, therefore it should return an empty dict for the result. diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index 69c14cae34e6..7e07e45d1202 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -1,46 +1,27 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import argparse import json import os import pathlib import sys import traceback import unittest -from typing import List, Optional, Tuple, Union +from typing import List, Optional, Union script_dir = pathlib.Path(__file__).parent.parent sys.path.append(os.fspath(script_dir)) sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) from testing_tools import socket_manager +from typing_extensions import Literal, NotRequired, TypedDict # If I use from utils then there will be an import error in test_discovery.py. from unittestadapter.utils import TestNode, build_test_tree, parse_unittest_args -from typing_extensions import NotRequired, TypedDict, Literal - DEFAULT_PORT = "45454" -def parse_discovery_cli_args(args: List[str]) -> Tuple[int, Union[str, None]]: - """Parse command-line arguments that should be processed by the script. - - So far this includes the port number that it needs to connect to, and the uuid passed by the TS side. - The port is passed to the discovery.py script when it is executed, and - defaults to DEFAULT_PORT if it can't be parsed. - The uuid should be passed to the discovery.py script when it is executed, and defaults to None if it can't be parsed. - If the arguments appear several times, the value returned by parse_cli_args will be the value of the last argument. - """ - arg_parser = argparse.ArgumentParser() - arg_parser.add_argument("--port", default=DEFAULT_PORT) - arg_parser.add_argument("--uuid") - parsed_args, _ = arg_parser.parse_known_args(args) - - return int(parsed_args.port), parsed_args.uuid - - class PayloadDict(TypedDict): cwd: str status: Literal["success", "error"] @@ -141,15 +122,16 @@ def post_response( start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) # Perform test discovery. - port, uuid = parse_discovery_cli_args(argv[:index]) + testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT)) + testUuid = os.environ.get("TEST_UUID") # Post this discovery payload. - if uuid is not None: - payload = discover_tests(start_dir, pattern, top_level_dir, uuid) - post_response(payload, port, uuid) + if testUuid is not None: + payload = discover_tests(start_dir, pattern, top_level_dir, testUuid) + post_response(payload, testPort, testUuid) # Post EOT token. eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} - post_response(eot_payload, port, uuid) + post_response(eot_payload, testPort, testUuid) else: print("Error: no uuid provided or parsed.") eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} - post_response(eot_payload, port, "") + post_response(eot_payload, testPort, "") diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index a208056c6682..0684ada8e44b 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import argparse import atexit import enum import json @@ -18,40 +17,17 @@ sys.path.append(os.fspath(script_dir)) sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) -from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict - from testing_tools import process_json_util, socket_manager +from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict from unittestadapter.utils import parse_unittest_args DEFAULT_PORT = "45454" - -def parse_execution_cli_args( - args: List[str], -) -> Tuple[int, Union[str, None]]: - """Parse command-line arguments that should be processed by the script. - - So far this includes the port number that it needs to connect to, the uuid passed by the TS side, - and the list of test ids to report. - The port is passed to the execution.py script when it is executed, and - defaults to DEFAULT_PORT if it can't be parsed. - The list of test ids is passed to the execution.py script when it is executed, and defaults to an empty list if it can't be parsed. - The uuid should be passed to the execution.py script when it is executed, and defaults to None if it can't be parsed. - If the arguments appear several times, the value returned by parse_cli_args will be the value of the last argument. - """ - arg_parser = argparse.ArgumentParser() - arg_parser.add_argument("--port", default=DEFAULT_PORT) - arg_parser.add_argument("--uuid") - parsed_args, _ = arg_parser.parse_known_args(args) - - return (int(parsed_args.port), parsed_args.uuid) - - ErrorType = Union[ Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None] ] -PORT = 0 -UUID = 0 +testPort = 0 +testUuid = 0 START_DIR = "" @@ -148,9 +124,9 @@ def formatResult( "subtest": subtest.id() if subtest else None, } self.formatted[test_id] = result - if PORT == 0 or UUID == 0: + if testPort == 0 or testUuid == 0: print("Error sending response, port or uuid unknown to python server.") - send_run_data(result, PORT, UUID) + send_run_data(result, testPort, testUuid) class TestExecutionStatus(str, enum.Enum): @@ -322,11 +298,12 @@ def post_response( print(f"Error: Could not connect to runTestIdsPort: {e}") print("Error: Could not connect to runTestIdsPort") - PORT, UUID = parse_execution_cli_args(argv[:index]) + testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT)) + testUuid = os.environ.get("TEST_UUID") if test_ids_from_buffer: # Perform test execution. payload = run_tests( - start_dir, test_ids_from_buffer, pattern, top_level_dir, UUID + start_dir, test_ids_from_buffer, pattern, top_level_dir, testUuid ) else: cwd = os.path.abspath(start_dir) @@ -338,8 +315,8 @@ def post_response( "result": None, } eot_payload: EOTPayloadDict = {"command_type": "execution", "eot": True} - if UUID is None: + if testUuid is None: print("Error sending response, uuid unknown to python server.") - post_response(eot_payload, PORT, "unknown") + post_response(eot_payload, testPort, "unknown") else: - post_response(eot_payload, PORT, UUID) + post_response(eot_payload, testPort, testUuid) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index f5827f87e1b4..e870136e3dc1 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -683,7 +683,7 @@ def send_post_request( cls_encoder -- a custom encoder if needed. """ testPort = os.getenv("TEST_PORT", 45454) - testuuid = os.getenv("TEST_UUID") + testUuid = os.getenv("TEST_UUID") addr = ("localhost", int(testPort)) global __socket @@ -698,7 +698,7 @@ def send_post_request( data = json.dumps(payload, cls=cls_encoder) request = f"""Content-Length: {len(data)} Content-Type: application/json -Request-uuid: {testuuid} +Request-uuid: {testUuid} {data}""" diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 1f9c0223d3fd..46217eab0459 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -179,7 +179,11 @@ export class PythonTestServer implements ITestServer, Disposable { cwd: options.cwd, throwOnStdErr: true, outputChannel: options.outChannel, - extraVariables: { PYTHONPATH: pythonPathCommand }, + extraVariables: { + PYTHONPATH: pythonPathCommand, + TEST_UUID: uuid.toString(), + TEST_PORT: this.getPort().toString(), + }, }; if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; @@ -191,12 +195,7 @@ export class PythonTestServer implements ITestServer, Disposable { }; const execService = await this.executionFactory.createActivatedEnvironment(creationOptions); - // Add the generated UUID to the data to be sent (expecting to receive it back). - // first check if we have testIds passed in (in case of execution) and - // insert appropriate flag and test id array - const args = [options.command.script, '--port', this.getPort().toString(), '--uuid', uuid].concat( - options.command.args, - ); + const args = [options.command.script].concat(options.command.args); if (options.outChannel) { options.outChannel.appendLine(`python ${args.join(' ')}`); diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 92a9a1135f55..02c35e806156 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -190,6 +190,16 @@ suite('Python Test Server, Send command etc', () => { RUN_TEST_IDS_PORT_CONST, 'Expect test id port to be in extra variables and set correctly', ); + assert.strictEqual( + options2.extraVariables.TEST_UUID, + FAKE_UUID, + 'Expect test uuid to be in extra variables and set correctly', + ); + assert.strictEqual( + options2.extraVariables.TEST_PORT, + 12345, + 'Expect server port to be set correctly as a env var', + ); } catch (e) { assert(false, 'Error parsing data, extra variables do not match'); } @@ -203,6 +213,8 @@ suite('Python Test Server, Send command etc', () => { return Promise.resolve(execService.object); }); server = new PythonTestServer(execFactory.object, debugLauncher); + sinon.stub(server, 'getPort').returns(12345); + // const portServer = server.getPort(); await server.serverReady(); const options = { command: { script: 'myscript', args: ['-foo', 'foo'] }, @@ -215,8 +227,7 @@ suite('Python Test Server, Send command etc', () => { await deferred2.promise; mockProc.trigger('close'); - const port = server.getPort(); - const expectedArgs = ['myscript', '--port', `${port}`, '--uuid', FAKE_UUID, '-foo', 'foo']; + const expectedArgs = ['myscript', '-foo', 'foo']; execService.verify((x) => x.execObservable(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); }); @@ -254,8 +265,7 @@ suite('Python Test Server, Send command etc', () => { await deferred.promise; mockProc.trigger('close'); - const port = server.getPort(); - const expected = ['python', 'myscript', '--port', `${port}`, '--uuid', FAKE_UUID, '-foo', 'foo'].join(' '); + const expected = ['python', 'myscript', '-foo', 'foo'].join(' '); assert.deepStrictEqual(output2, [expected]); }); From ff0d4df88c9aa612853b5bd43cce65440a7ce0ec Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 4 Oct 2023 10:34:14 -0700 Subject: [PATCH 0228/1136] handle key error pytest (#22151) fixes https://github.com/microsoft/vscode-python/issues/22149 --- pythonFiles/vscode_pytest/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index e870136e3dc1..2fab4d77c2f8 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -200,8 +200,9 @@ def pytest_report_teststatus(report, config): elif report.failed: report_value = "failure" message = report.longreprtext - node_path = map_id_to_path[report.nodeid] - if not node_path: + try: + node_path = map_id_to_path[report.nodeid] + except KeyError: node_path = cwd # Calculate the absolute test id and use this as the ID moving forward. absolute_node_id = get_absolute_test_id(report.nodeid, node_path) From ae427391c9058f49fecfd5b8a20511624d2bb262 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 4 Oct 2023 11:29:53 -0700 Subject: [PATCH 0229/1136] Remove unsupported command from readme (#22153) --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8a5df6720717..0a8766f086af 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Extensions installed through the marketplace are subject to the [Marketplace Ter ## Jupyter Notebook quick start -The Python extension offers support for Jupyter notebooks via the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) to provide you a great Python notebook experience in VS Code. +The Python extension offers support for Jupyter notebooks via the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) to provide you a great Python notebook experience in VS Code. - Install the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter). @@ -60,7 +60,6 @@ Open the Command Palette (Command+Shift+P on macOS and Ctrl+Shift+P on Windows/L | `Python: Select Interpreter` | Switch between Python interpreters, versions, and environments. | | `Python: Start REPL` | Start an interactive Python REPL using the selected interpreter in the VS Code terminal. | | `Python: Run Python File in Terminal` | Runs the active Python file in the VS Code terminal. You can also run a Python file by right-clicking on the file and selecting `Run Python File in Terminal`. | -| `Python: Select Linter` | Switch from Pylint to Flake8 or other supported linters. | | `Format Document` | Formats code using the provided [formatter](https://code.visualstudio.com/docs/python/editing#_formatting) in the `settings.json` file. | | `Python: Configure Tests` | Select a test framework and configure it to display the Test Explorer. | @@ -82,7 +81,7 @@ Learn more about the rich features of the Python extension: - [Environments](https://code.visualstudio.com/docs/python/environments): Automatically activate and switch between virtualenv, venv, pipenv, conda and pyenv environments -- [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring): Restructure your Python code with variable extraction and method extraction. Additionally, there is componentized support to enable additional refactoring, such as import sorting, through extensions including [isort](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) and [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff). +- [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring): Restructure your Python code with variable extraction and method extraction. Additionally, there is componentized support to enable additional refactoring, such as import sorting, through extensions including [isort](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) and [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff). From ab6ab06e60b26109fe22843ea1aa46e918864e10 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 4 Oct 2023 12:29:57 -0700 Subject: [PATCH 0230/1136] Use python 3.12-dev (#22043) --- .github/workflows/build.yml | 2 +- .github/workflows/pr-check.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1509a7b433e..56d9c04f0cd1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -120,7 +120,7 @@ jobs: # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. os: [ubuntu-latest, windows-latest] # Run the tests on the oldest and most recent versions of Python. - python: ['3.8', '3.x'] + python: ['3.8', '3.x', '3.12-dev'] steps: - name: Checkout diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index b7d2ed0c0545..9229393ce5cc 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -94,7 +94,7 @@ jobs: # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. os: [ubuntu-latest, windows-latest] # Run the tests on the oldest and most recent versions of Python. - python: ['3.8', '3.x'] + python: ['3.8', '3.x', '3.12-dev'] steps: - name: Checkout From 66cea2169ef4c46a96e42725c4793ccce3b5c5df Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 4 Oct 2023 16:29:51 -0700 Subject: [PATCH 0231/1136] Show notification when deactivate command is run in terminal (#22133) Closes https://github.com/microsoft/vscode-python/issues/22121 --- package.json | 3 +- .../common/application/applicationShell.ts | 12 +- src/client/common/application/types.ts | 18 ++ src/client/common/utils/localize.ts | 4 + src/client/interpreter/activation/types.ts | 8 - src/client/interpreter/serviceRegistry.ts | 13 +- src/client/telemetry/constants.ts | 1 + src/client/telemetry/index.ts | 18 ++ .../deactivatePrompt.ts | 91 +++++++ .../indicatorPrompt.ts} | 6 +- .../envCollectionActivation/service.ts} | 7 +- src/client/terminals/serviceRegistry.ts | 38 ++- src/client/terminals/types.ts | 8 + ...erminalEnvVarCollectionPrompt.unit.test.ts | 8 +- ...rminalEnvVarCollectionService.unit.test.ts | 2 +- .../deactivatePrompt.unit.test.ts | 251 ++++++++++++++++++ .../terminals/serviceRegistry.unit.test.ts | 16 ++ ...scode.proposed.terminalDataWriteEvent.d.ts | 31 +++ 18 files changed, 490 insertions(+), 45 deletions(-) create mode 100644 src/client/terminals/envCollectionActivation/deactivatePrompt.ts rename src/client/{interpreter/activation/terminalEnvVarCollectionPrompt.ts => terminals/envCollectionActivation/indicatorPrompt.ts} (95%) rename src/client/{interpreter/activation/terminalEnvVarCollectionService.ts => terminals/envCollectionActivation/service.ts} (98%) create mode 100644 src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts create mode 100644 typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts diff --git a/package.json b/package.json index 47faec663015..df2d2546e4d8 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "quickPickSortByLabel", "testObserver", "quickPickItemTooltip", - "saveEditor" + "saveEditor", + "terminalDataWriteEvent" ], "author": { "name": "Microsoft Corporation" diff --git a/src/client/common/application/applicationShell.ts b/src/client/common/application/applicationShell.ts index 454662472010..aadf80186900 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -10,6 +10,7 @@ import { DocumentSelector, env, Event, + EventEmitter, InputBox, InputBoxOptions, languages, @@ -37,7 +38,8 @@ import { WorkspaceFolder, WorkspaceFolderPickOptions, } from 'vscode'; -import { IApplicationShell } from './types'; +import { traceError } from '../../logging'; +import { IApplicationShell, TerminalDataWriteEvent } from './types'; @injectable() export class ApplicationShell implements IApplicationShell { @@ -172,4 +174,12 @@ export class ApplicationShell implements IApplicationShell { public createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem { return languages.createLanguageStatusItem(id, selector); } + public get onDidWriteTerminalData(): Event { + try { + return window.onDidWriteTerminalData; + } catch (ex) { + traceError('Failed to get proposed API onDidWriteTerminalData', ex); + return new EventEmitter().event; + } + } } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index fa2ced6c45da..863f5e4651b2 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -67,6 +67,17 @@ import { Resource } from '../types'; import { ICommandNameArgumentTypeMapping } from './commands'; import { ExtensionContextKey } from './contextKeys'; +export interface TerminalDataWriteEvent { + /** + * The {@link Terminal} for which the data was written. + */ + readonly terminal: Terminal; + /** + * The data being written. + */ + readonly data: string; +} + export const IApplicationShell = Symbol('IApplicationShell'); export interface IApplicationShell { /** @@ -75,6 +86,13 @@ export interface IApplicationShell { */ readonly onDidChangeWindowState: Event; + /** + * An event which fires when the terminal's child pseudo-device is written to (the shell). + * In other words, this provides access to the raw data stream from the process running + * within the terminal, including VT sequences. + */ + readonly onDidWriteTerminalData: Event; + showInformationMessage(message: string, ...items: string[]): Thenable; /** diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index bc32c1078cad..fc118699d2c7 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -201,6 +201,10 @@ export namespace Interpreters { export const terminalEnvVarCollectionPrompt = l10n.t( 'The Python extension automatically activates all terminals using the selected environment, even when the name of the environment{0} is not present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', ); + export const terminalDeactivatePrompt = l10n.t( + 'Deactivating virtual environments may not work by default due to a technical limitation in our activation approach, but it can be resolved with a few simple steps.', + ); + export const deactivateDoneButton = l10n.t('Done, it works'); export const activatedCondaEnvLaunch = l10n.t( 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', ); diff --git a/src/client/interpreter/activation/types.ts b/src/client/interpreter/activation/types.ts index 2b364cbeb862..e00ef9b62b3f 100644 --- a/src/client/interpreter/activation/types.ts +++ b/src/client/interpreter/activation/types.ts @@ -21,11 +21,3 @@ export interface IEnvironmentActivationService { interpreter?: PythonEnvironment, ): Promise; } - -export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); -export interface ITerminalEnvVarCollectionService { - /** - * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. - */ - isTerminalPromptSetCorrectly(resource?: Resource): boolean; -} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 018e7abfdc46..422776bd5e43 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -6,9 +6,7 @@ import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { EnvironmentActivationService } from './activation/service'; -import { TerminalEnvVarCollectionPrompt } from './activation/terminalEnvVarCollectionPrompt'; -import { TerminalEnvVarCollectionService } from './activation/terminalEnvVarCollectionService'; -import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './activation/types'; +import { IEnvironmentActivationService } from './activation/types'; import { InterpreterAutoSelectionService } from './autoSelection/index'; import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy'; import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types'; @@ -110,13 +108,4 @@ export function registerTypes(serviceManager: IServiceManager): void { IEnvironmentActivationService, EnvironmentActivationService, ); - serviceManager.addSingleton( - ITerminalEnvVarCollectionService, - TerminalEnvVarCollectionService, - ); - serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); - serviceManager.addSingleton( - IExtensionSingleActivationService, - TerminalEnvVarCollectionPrompt, - ); } diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index c680b91094cb..4b4dc302dc3f 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -29,6 +29,7 @@ export enum EventName { TERMINAL_SHELL_IDENTIFICATION = 'TERMINAL_SHELL_IDENTIFICATION', PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT', PYTHON_NOT_INSTALLED_PROMPT = 'PYTHON_NOT_INSTALLED_PROMPT', + TERMINAL_DEACTIVATE_PROMPT = 'TERMINAL_DEACTIVATE_PROMPT', CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT', REQUIRE_JUPYTER_PROMPT = 'REQUIRE_JUPYTER_PROMPT', ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 600f9a2d48ff..bd60b9281a93 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1328,6 +1328,24 @@ export interface IEventNamePropertyMapping { */ selection: 'Allow' | 'Close' | undefined; }; + /** + * Telemetry event sent with details when user clicks the prompt with the following message: + * + * 'Deactivating virtual environments may not work by default due to a technical limitation in our activation approach, but it can be resolved with a few simple steps.' + */ + /* __GDPR__ + "terminal_deactivate_prompt" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karrtikr" } + } + */ + [EventName.TERMINAL_DEACTIVATE_PROMPT]: { + /** + * `See Instructions` When 'See Instructions' option is selected + * `Done, it works` When 'Done, it works' option is selected + * `Don't show again` When 'Don't show again' option is selected + */ + selection: 'See Instructions' | 'Done, it works' | "Don't show again" | undefined; + }; /** * Telemetry event sent with details when user attempts to run in interactive window when Jupyter is not installed. */ diff --git a/src/client/terminals/envCollectionActivation/deactivatePrompt.ts b/src/client/terminals/envCollectionActivation/deactivatePrompt.ts new file mode 100644 index 000000000000..460144303f18 --- /dev/null +++ b/src/client/terminals/envCollectionActivation/deactivatePrompt.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IApplicationEnvironment, IApplicationShell } from '../../common/application/types'; +import { IBrowserService, IDisposableRegistry, IExperimentService, IPersistentStateFactory } from '../../common/types'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { TerminalShellType } from '../../common/terminal/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +export const terminalDeactivationPromptKey = 'TERMINAL_DEACTIVATION_PROMPT_KEY'; + +@injectable() +export class TerminalDeactivateLimitationPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IBrowserService) private readonly browserService: IBrowserService, + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + ) {} + + public async activate(): Promise { + if (!inTerminalEnvVarExperiment(this.experimentService)) { + return; + } + this.disposableRegistry.push( + this.appShell.onDidWriteTerminalData(async (e) => { + if (!e.data.includes('deactivate')) { + return; + } + const shellType = identifyShellFromShellPath(this.appEnvironment.shell); + if (shellType === TerminalShellType.commandPrompt) { + return; + } + const { terminal } = e; + const cwd = + 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd + ? terminal.creationOptions.cwd + : undefined; + const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.type !== PythonEnvType.Virtual) { + return; + } + await this.notifyUsers(); + }), + ); + } + + private async notifyUsers(): Promise { + const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + terminalDeactivationPromptKey, + true, + ); + if (!notificationPromptEnabled.value) { + return; + } + const prompts = [Common.seeInstructions, Interpreters.deactivateDoneButton, Common.doNotShowAgain]; + const telemetrySelections: ['See Instructions', 'Done, it works', "Don't show again"] = [ + 'See Instructions', + 'Done, it works', + "Don't show again", + ]; + const selection = await this.appShell.showWarningMessage(Interpreters.terminalDeactivatePrompt, ...prompts); + if (!selection) { + return; + } + sendTelemetryEvent(EventName.TERMINAL_DEACTIVATE_PROMPT, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, + }); + if (selection === prompts[0]) { + const url = `https://aka.ms/AAmx2ft`; + this.browserService.launch(url); + } + if (selection === prompts[1] || selection === prompts[2]) { + await notificationPromptEnabled.updateValue(false); + } + } +} diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts b/src/client/terminals/envCollectionActivation/indicatorPrompt.ts similarity index 95% rename from src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts rename to src/client/terminals/envCollectionActivation/indicatorPrompt.ts index c8aea205a32a..bf648eefe8e9 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts +++ b/src/client/terminals/envCollectionActivation/indicatorPrompt.ts @@ -14,15 +14,15 @@ import { } from '../../common/types'; import { Common, Interpreters } from '../../common/utils/localize'; import { IExtensionSingleActivationService } from '../../activation/types'; -import { ITerminalEnvVarCollectionService } from './types'; import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; -import { IInterpreterService } from '../contracts'; +import { IInterpreterService } from '../../interpreter/contracts'; import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { ITerminalEnvVarCollectionService } from '../types'; export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; @injectable() -export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivationService { +export class TerminalIndicatorPrompt implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; constructor( diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/terminals/envCollectionActivation/service.ts similarity index 98% rename from src/client/interpreter/activation/terminalEnvVarCollectionService.ts rename to src/client/terminals/envCollectionActivation/service.ts index c11ec221d4d7..ae346d264eeb 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -28,9 +28,9 @@ import { import { Deferred, createDeferred } from '../../common/utils/async'; import { Interpreters } from '../../common/utils/localize'; import { traceDecoratorVerbose, traceError, traceVerbose, traceWarn } from '../../logging'; -import { IInterpreterService } from '../contracts'; -import { defaultShells } from './service'; -import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { defaultShells } from '../../interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../interpreter/activation/types'; import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; @@ -38,6 +38,7 @@ import { TerminalShellType } from '../../common/terminal/types'; import { OSType } from '../../common/utils/platform'; import { normCase } from '../../common/platform/fs-paths'; import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { ITerminalEnvVarCollectionService } from '../types'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { diff --git a/src/client/terminals/serviceRegistry.ts b/src/client/terminals/serviceRegistry.ts index a39ef31a8fe4..a9da776d011a 100644 --- a/src/client/terminals/serviceRegistry.ts +++ b/src/client/terminals/serviceRegistry.ts @@ -1,25 +1,26 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { interfaces } from 'inversify'; -import { ClassType } from '../ioc/types'; +import { IServiceManager } from '../ioc/types'; import { TerminalAutoActivation } from './activation'; import { CodeExecutionManager } from './codeExecution/codeExecutionManager'; import { DjangoShellCodeExecutionProvider } from './codeExecution/djangoShellCodeExecution'; import { CodeExecutionHelper } from './codeExecution/helper'; import { ReplProvider } from './codeExecution/repl'; import { TerminalCodeExecutionProvider } from './codeExecution/terminalCodeExecution'; -import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, ITerminalAutoActivation } from './types'; +import { + ICodeExecutionHelper, + ICodeExecutionManager, + ICodeExecutionService, + ITerminalAutoActivation, + ITerminalEnvVarCollectionService, +} from './types'; +import { TerminalEnvVarCollectionService } from './envCollectionActivation/service'; +import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; +import { TerminalDeactivateLimitationPrompt } from './envCollectionActivation/deactivatePrompt'; +import { TerminalIndicatorPrompt } from './envCollectionActivation/indicatorPrompt'; -interface IServiceRegistry { - addSingleton( - serviceIdentifier: interfaces.ServiceIdentifier, - constructor: ClassType, - name?: string | number | symbol, - ): void; -} - -export function registerTypes(serviceManager: IServiceRegistry): void { +export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ICodeExecutionHelper, CodeExecutionHelper); serviceManager.addSingleton(ICodeExecutionManager, CodeExecutionManager); @@ -37,4 +38,17 @@ export function registerTypes(serviceManager: IServiceRegistry): void { serviceManager.addSingleton(ICodeExecutionService, ReplProvider, 'repl'); serviceManager.addSingleton(ITerminalAutoActivation, TerminalAutoActivation); + serviceManager.addSingleton( + ITerminalEnvVarCollectionService, + TerminalEnvVarCollectionService, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalIndicatorPrompt, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalDeactivateLimitationPrompt, + ); + serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); } diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index 47ac16d9e08b..48d60adf3f39 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -33,3 +33,11 @@ export interface ITerminalAutoActivation extends IDisposable { register(): void; disableAutoActivation(terminal: Terminal): void; } + +export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); +export interface ITerminalEnvVarCollectionService { + /** + * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. + */ + isTerminalPromptSetCorrectly(resource?: Resource): boolean; +} diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts index baa83c8b11c5..5d4da49ebb45 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts @@ -13,13 +13,13 @@ import { IPersistentStateFactory, IPythonSettings, } from '../../../client/common/types'; -import { TerminalEnvVarCollectionPrompt } from '../../../client/interpreter/activation/terminalEnvVarCollectionPrompt'; -import { ITerminalEnvVarCollectionService } from '../../../client/interpreter/activation/types'; +import { TerminalIndicatorPrompt } from '../../../client/terminals/envCollectionActivation/indicatorPrompt'; import { Common, Interpreters } from '../../../client/common/utils/localize'; import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; import { sleep } from '../../core'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { ITerminalEnvVarCollectionService } from '../../../client/terminals/types'; suite('Terminal Environment Variable Collection Prompt', () => { let shell: IApplicationShell; @@ -28,7 +28,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { let activeResourceService: IActiveResourceService; let terminalEnvVarCollectionService: ITerminalEnvVarCollectionService; let persistentStateFactory: IPersistentStateFactory; - let terminalEnvVarCollectionPrompt: TerminalEnvVarCollectionPrompt; + let terminalEnvVarCollectionPrompt: TerminalIndicatorPrompt; let terminalEventEmitter: EventEmitter; let notificationEnabled: IPersistentState; let configurationService: IConfigurationService; @@ -61,7 +61,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { ); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); when(terminalManager.onDidOpenTerminal).thenReturn(terminalEventEmitter.event); - terminalEnvVarCollectionPrompt = new TerminalEnvVarCollectionPrompt( + terminalEnvVarCollectionPrompt = new TerminalIndicatorPrompt( instance(shell), instance(persistentStateFactory), instance(terminalManager), diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index e41d6ce4d53c..5e572e7ad06f 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -32,7 +32,7 @@ import { import { Interpreters } from '../../../client/common/utils/localize'; import { OSType, getOSType } from '../../../client/common/utils/platform'; import { defaultShells } from '../../../client/interpreter/activation/service'; -import { TerminalEnvVarCollectionService } from '../../../client/interpreter/activation/terminalEnvVarCollectionService'; +import { TerminalEnvVarCollectionService } from '../../../client/terminals/envCollectionActivation/service'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PathUtils } from '../../../client/common/platform/pathUtils'; diff --git a/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts b/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts new file mode 100644 index 000000000000..acd8ee99e5d7 --- /dev/null +++ b/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; +import { EventEmitter, Terminal, TerminalDataWriteEvent, Uri } from 'vscode'; +import { IApplicationEnvironment, IApplicationShell } from '../../../client/common/application/types'; +import { + IBrowserService, + IExperimentService, + IPersistentState, + IPersistentStateFactory, +} from '../../../client/common/types'; +import { Common, Interpreters } from '../../../client/common/utils/localize'; +import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; +import { sleep } from '../../core'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { TerminalDeactivateLimitationPrompt } from '../../../client/terminals/envCollectionActivation/deactivatePrompt'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; +import { TerminalShellType } from '../../../client/common/terminal/types'; + +suite('Terminal Deactivation Limitation Prompt', () => { + let shell: IApplicationShell; + let experimentService: IExperimentService; + let persistentStateFactory: IPersistentStateFactory; + let appEnvironment: IApplicationEnvironment; + let deactivatePrompt: TerminalDeactivateLimitationPrompt; + let terminalWriteEvent: EventEmitter; + let notificationEnabled: IPersistentState; + let browserService: IBrowserService; + let interpreterService: IInterpreterService; + const prompts = [Common.seeInstructions, Interpreters.deactivateDoneButton, Common.doNotShowAgain]; + const expectedMessage = Interpreters.terminalDeactivatePrompt; + + setup(async () => { + shell = mock(); + interpreterService = mock(); + experimentService = mock(); + persistentStateFactory = mock(); + appEnvironment = mock(); + when(appEnvironment.shell).thenReturn('bash'); + browserService = mock(); + notificationEnabled = mock>(); + terminalWriteEvent = new EventEmitter(); + when(persistentStateFactory.createGlobalPersistentState(anything(), true)).thenReturn( + instance(notificationEnabled), + ); + when(shell.onDidWriteTerminalData).thenReturn(terminalWriteEvent.event); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); + deactivatePrompt = new TerminalDeactivateLimitationPrompt( + instance(shell), + instance(persistentStateFactory), + [], + instance(interpreterService), + instance(browserService), + instance(appEnvironment), + instance(experimentService), + ); + }); + + test('Show notification when "deactivate" command is run when a virtual env is selected', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); + }); + + test('When using cmd, do not show notification for the same', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + reset(appEnvironment); + when(appEnvironment.shell).thenReturn(TerminalShellType.commandPrompt); + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); + }); + + test('When not in experiment, do not show notification for the same', async () => { + reset(experimentService); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); + }); + + test('Do not show notification if notification is disabled', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(false); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); + }); + + test('Do not show notification when virtual env is not activated for terminal', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Conda, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); + }); + + test("Disable notification if `Don't show again` is clicked", async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(Common.doNotShowAgain)); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).once(); + }); + + test('Disable notification if `Done, it works` is clicked', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn( + Promise.resolve(Interpreters.deactivateDoneButton), + ); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).once(); + }); + + test('Open link to workaround if `See instructions` is clicked', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(Common.seeInstructions)); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); + verify(browserService.launch(anything())).once(); + }); + + test('Do not perform any action if prompt is closed', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); + verify(notificationEnabled.updateValue(false)).never(); + verify(browserService.launch(anything())).never(); + }); +}); diff --git a/src/test/terminals/serviceRegistry.unit.test.ts b/src/test/terminals/serviceRegistry.unit.test.ts index 38a9a9744e91..816afa17cf88 100644 --- a/src/test/terminals/serviceRegistry.unit.test.ts +++ b/src/test/terminals/serviceRegistry.unit.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import * as typemoq from 'typemoq'; +import { IExtensionActivationService, IExtensionSingleActivationService } from '../../client/activation/types'; import { IServiceManager } from '../../client/ioc/types'; import { TerminalAutoActivation } from '../../client/terminals/activation'; import { CodeExecutionManager } from '../../client/terminals/codeExecution/codeExecutionManager'; @@ -9,12 +10,16 @@ import { DjangoShellCodeExecutionProvider } from '../../client/terminals/codeExe import { CodeExecutionHelper } from '../../client/terminals/codeExecution/helper'; import { ReplProvider } from '../../client/terminals/codeExecution/repl'; import { TerminalCodeExecutionProvider } from '../../client/terminals/codeExecution/terminalCodeExecution'; +import { TerminalDeactivateLimitationPrompt } from '../../client/terminals/envCollectionActivation/deactivatePrompt'; +import { TerminalIndicatorPrompt } from '../../client/terminals/envCollectionActivation/indicatorPrompt'; +import { TerminalEnvVarCollectionService } from '../../client/terminals/envCollectionActivation/service'; import { registerTypes } from '../../client/terminals/serviceRegistry'; import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, ITerminalAutoActivation, + ITerminalEnvVarCollectionService, } from '../../client/terminals/types'; suite('Terminal - Service Registry', () => { @@ -27,6 +32,9 @@ suite('Terminal - Service Registry', () => { [ICodeExecutionService, ReplProvider, 'repl'], [ITerminalAutoActivation, TerminalAutoActivation], [ICodeExecutionService, TerminalCodeExecutionProvider, 'standard'], + [ITerminalEnvVarCollectionService, TerminalEnvVarCollectionService], + [IExtensionSingleActivationService, TerminalIndicatorPrompt], + [IExtensionSingleActivationService, TerminalDeactivateLimitationPrompt], ].forEach((args) => { if (args.length === 2) { services @@ -50,6 +58,14 @@ suite('Terminal - Service Registry', () => { .verifiable(typemoq.Times.once()); } }); + services + .setup((s) => + s.addBinding( + typemoq.It.is((v) => ITerminalEnvVarCollectionService === v), + typemoq.It.is((value) => IExtensionActivationService === value), + ), + ) + .verifiable(typemoq.Times.once()); registerTypes(services.object); diff --git a/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts b/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts new file mode 100644 index 000000000000..6913b862c70f --- /dev/null +++ b/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/78502 + // + // This API is still proposed but we don't intent on promoting it to stable due to problems + // around performance. See #145234 for a more likely API to get stabilized. + + export interface TerminalDataWriteEvent { + /** + * The {@link Terminal} for which the data was written. + */ + readonly terminal: Terminal; + /** + * The data being written. + */ + readonly data: string; + } + + namespace window { + /** + * An event which fires when the terminal's child pseudo-device is written to (the shell). + * In other words, this provides access to the raw data stream from the process running + * within the terminal, including VT sequences. + */ + export const onDidWriteTerminalData: Event; + } +} From 514bce666d3388dc47d115da44cb2d127560757d Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 5 Oct 2023 11:11:34 -0700 Subject: [PATCH 0232/1136] Revert "Show notification when deactivate command is run in terminal" (#22158) Reverts microsoft/vscode-python#22133 --- package.json | 3 +- .../common/application/applicationShell.ts | 12 +- src/client/common/application/types.ts | 18 -- src/client/common/utils/localize.ts | 4 - .../terminalEnvVarCollectionPrompt.ts} | 6 +- .../terminalEnvVarCollectionService.ts} | 7 +- src/client/interpreter/activation/types.ts | 8 + src/client/interpreter/serviceRegistry.ts | 13 +- src/client/telemetry/constants.ts | 1 - src/client/telemetry/index.ts | 18 -- .../deactivatePrompt.ts | 91 ------- src/client/terminals/serviceRegistry.ts | 38 +-- src/client/terminals/types.ts | 8 - ...erminalEnvVarCollectionPrompt.unit.test.ts | 8 +- ...rminalEnvVarCollectionService.unit.test.ts | 2 +- .../deactivatePrompt.unit.test.ts | 251 ------------------ .../terminals/serviceRegistry.unit.test.ts | 16 -- ...scode.proposed.terminalDataWriteEvent.d.ts | 31 --- 18 files changed, 45 insertions(+), 490 deletions(-) rename src/client/{terminals/envCollectionActivation/indicatorPrompt.ts => interpreter/activation/terminalEnvVarCollectionPrompt.ts} (95%) rename src/client/{terminals/envCollectionActivation/service.ts => interpreter/activation/terminalEnvVarCollectionService.ts} (98%) delete mode 100644 src/client/terminals/envCollectionActivation/deactivatePrompt.ts delete mode 100644 src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts delete mode 100644 typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts diff --git a/package.json b/package.json index df2d2546e4d8..47faec663015 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,7 @@ "quickPickSortByLabel", "testObserver", "quickPickItemTooltip", - "saveEditor", - "terminalDataWriteEvent" + "saveEditor" ], "author": { "name": "Microsoft Corporation" diff --git a/src/client/common/application/applicationShell.ts b/src/client/common/application/applicationShell.ts index aadf80186900..454662472010 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -10,7 +10,6 @@ import { DocumentSelector, env, Event, - EventEmitter, InputBox, InputBoxOptions, languages, @@ -38,8 +37,7 @@ import { WorkspaceFolder, WorkspaceFolderPickOptions, } from 'vscode'; -import { traceError } from '../../logging'; -import { IApplicationShell, TerminalDataWriteEvent } from './types'; +import { IApplicationShell } from './types'; @injectable() export class ApplicationShell implements IApplicationShell { @@ -174,12 +172,4 @@ export class ApplicationShell implements IApplicationShell { public createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem { return languages.createLanguageStatusItem(id, selector); } - public get onDidWriteTerminalData(): Event { - try { - return window.onDidWriteTerminalData; - } catch (ex) { - traceError('Failed to get proposed API onDidWriteTerminalData', ex); - return new EventEmitter().event; - } - } } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 863f5e4651b2..fa2ced6c45da 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -67,17 +67,6 @@ import { Resource } from '../types'; import { ICommandNameArgumentTypeMapping } from './commands'; import { ExtensionContextKey } from './contextKeys'; -export interface TerminalDataWriteEvent { - /** - * The {@link Terminal} for which the data was written. - */ - readonly terminal: Terminal; - /** - * The data being written. - */ - readonly data: string; -} - export const IApplicationShell = Symbol('IApplicationShell'); export interface IApplicationShell { /** @@ -86,13 +75,6 @@ export interface IApplicationShell { */ readonly onDidChangeWindowState: Event; - /** - * An event which fires when the terminal's child pseudo-device is written to (the shell). - * In other words, this provides access to the raw data stream from the process running - * within the terminal, including VT sequences. - */ - readonly onDidWriteTerminalData: Event; - showInformationMessage(message: string, ...items: string[]): Thenable; /** diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index fc118699d2c7..bc32c1078cad 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -201,10 +201,6 @@ export namespace Interpreters { export const terminalEnvVarCollectionPrompt = l10n.t( 'The Python extension automatically activates all terminals using the selected environment, even when the name of the environment{0} is not present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', ); - export const terminalDeactivatePrompt = l10n.t( - 'Deactivating virtual environments may not work by default due to a technical limitation in our activation approach, but it can be resolved with a few simple steps.', - ); - export const deactivateDoneButton = l10n.t('Done, it works'); export const activatedCondaEnvLaunch = l10n.t( 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', ); diff --git a/src/client/terminals/envCollectionActivation/indicatorPrompt.ts b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts similarity index 95% rename from src/client/terminals/envCollectionActivation/indicatorPrompt.ts rename to src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts index bf648eefe8e9..c8aea205a32a 100644 --- a/src/client/terminals/envCollectionActivation/indicatorPrompt.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts @@ -14,15 +14,15 @@ import { } from '../../common/types'; import { Common, Interpreters } from '../../common/utils/localize'; import { IExtensionSingleActivationService } from '../../activation/types'; +import { ITerminalEnvVarCollectionService } from './types'; import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; -import { IInterpreterService } from '../../interpreter/contracts'; +import { IInterpreterService } from '../contracts'; import { PythonEnvironment } from '../../pythonEnvironments/info'; -import { ITerminalEnvVarCollectionService } from '../types'; export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; @injectable() -export class TerminalIndicatorPrompt implements IExtensionSingleActivationService { +export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; constructor( diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts similarity index 98% rename from src/client/terminals/envCollectionActivation/service.ts rename to src/client/interpreter/activation/terminalEnvVarCollectionService.ts index ae346d264eeb..c11ec221d4d7 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -28,9 +28,9 @@ import { import { Deferred, createDeferred } from '../../common/utils/async'; import { Interpreters } from '../../common/utils/localize'; import { traceDecoratorVerbose, traceError, traceVerbose, traceWarn } from '../../logging'; -import { IInterpreterService } from '../../interpreter/contracts'; -import { defaultShells } from '../../interpreter/activation/service'; -import { IEnvironmentActivationService } from '../../interpreter/activation/types'; +import { IInterpreterService } from '../contracts'; +import { defaultShells } from './service'; +import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; @@ -38,7 +38,6 @@ import { TerminalShellType } from '../../common/terminal/types'; import { OSType } from '../../common/utils/platform'; import { normCase } from '../../common/platform/fs-paths'; import { PythonEnvType } from '../../pythonEnvironments/base/info'; -import { ITerminalEnvVarCollectionService } from '../types'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { diff --git a/src/client/interpreter/activation/types.ts b/src/client/interpreter/activation/types.ts index e00ef9b62b3f..2b364cbeb862 100644 --- a/src/client/interpreter/activation/types.ts +++ b/src/client/interpreter/activation/types.ts @@ -21,3 +21,11 @@ export interface IEnvironmentActivationService { interpreter?: PythonEnvironment, ): Promise; } + +export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); +export interface ITerminalEnvVarCollectionService { + /** + * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. + */ + isTerminalPromptSetCorrectly(resource?: Resource): boolean; +} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 422776bd5e43..018e7abfdc46 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -6,7 +6,9 @@ import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { EnvironmentActivationService } from './activation/service'; -import { IEnvironmentActivationService } from './activation/types'; +import { TerminalEnvVarCollectionPrompt } from './activation/terminalEnvVarCollectionPrompt'; +import { TerminalEnvVarCollectionService } from './activation/terminalEnvVarCollectionService'; +import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './activation/types'; import { InterpreterAutoSelectionService } from './autoSelection/index'; import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy'; import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types'; @@ -108,4 +110,13 @@ export function registerTypes(serviceManager: IServiceManager): void { IEnvironmentActivationService, EnvironmentActivationService, ); + serviceManager.addSingleton( + ITerminalEnvVarCollectionService, + TerminalEnvVarCollectionService, + ); + serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalEnvVarCollectionPrompt, + ); } diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 4b4dc302dc3f..c680b91094cb 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -29,7 +29,6 @@ export enum EventName { TERMINAL_SHELL_IDENTIFICATION = 'TERMINAL_SHELL_IDENTIFICATION', PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT', PYTHON_NOT_INSTALLED_PROMPT = 'PYTHON_NOT_INSTALLED_PROMPT', - TERMINAL_DEACTIVATE_PROMPT = 'TERMINAL_DEACTIVATE_PROMPT', CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT', REQUIRE_JUPYTER_PROMPT = 'REQUIRE_JUPYTER_PROMPT', ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index bd60b9281a93..600f9a2d48ff 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1328,24 +1328,6 @@ export interface IEventNamePropertyMapping { */ selection: 'Allow' | 'Close' | undefined; }; - /** - * Telemetry event sent with details when user clicks the prompt with the following message: - * - * 'Deactivating virtual environments may not work by default due to a technical limitation in our activation approach, but it can be resolved with a few simple steps.' - */ - /* __GDPR__ - "terminal_deactivate_prompt" : { - "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karrtikr" } - } - */ - [EventName.TERMINAL_DEACTIVATE_PROMPT]: { - /** - * `See Instructions` When 'See Instructions' option is selected - * `Done, it works` When 'Done, it works' option is selected - * `Don't show again` When 'Don't show again' option is selected - */ - selection: 'See Instructions' | 'Done, it works' | "Don't show again" | undefined; - }; /** * Telemetry event sent with details when user attempts to run in interactive window when Jupyter is not installed. */ diff --git a/src/client/terminals/envCollectionActivation/deactivatePrompt.ts b/src/client/terminals/envCollectionActivation/deactivatePrompt.ts deleted file mode 100644 index 460144303f18..000000000000 --- a/src/client/terminals/envCollectionActivation/deactivatePrompt.ts +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { IApplicationEnvironment, IApplicationShell } from '../../common/application/types'; -import { IBrowserService, IDisposableRegistry, IExperimentService, IPersistentStateFactory } from '../../common/types'; -import { Common, Interpreters } from '../../common/utils/localize'; -import { IExtensionSingleActivationService } from '../../activation/types'; -import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; -import { IInterpreterService } from '../../interpreter/contracts'; -import { PythonEnvType } from '../../pythonEnvironments/base/info'; -import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; -import { TerminalShellType } from '../../common/terminal/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; - -export const terminalDeactivationPromptKey = 'TERMINAL_DEACTIVATION_PROMPT_KEY'; - -@injectable() -export class TerminalDeactivateLimitationPrompt implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IBrowserService) private readonly browserService: IBrowserService, - @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, - @inject(IExperimentService) private readonly experimentService: IExperimentService, - ) {} - - public async activate(): Promise { - if (!inTerminalEnvVarExperiment(this.experimentService)) { - return; - } - this.disposableRegistry.push( - this.appShell.onDidWriteTerminalData(async (e) => { - if (!e.data.includes('deactivate')) { - return; - } - const shellType = identifyShellFromShellPath(this.appEnvironment.shell); - if (shellType === TerminalShellType.commandPrompt) { - return; - } - const { terminal } = e; - const cwd = - 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd - ? terminal.creationOptions.cwd - : undefined; - const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; - const interpreter = await this.interpreterService.getActiveInterpreter(resource); - if (interpreter?.type !== PythonEnvType.Virtual) { - return; - } - await this.notifyUsers(); - }), - ); - } - - private async notifyUsers(): Promise { - const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( - terminalDeactivationPromptKey, - true, - ); - if (!notificationPromptEnabled.value) { - return; - } - const prompts = [Common.seeInstructions, Interpreters.deactivateDoneButton, Common.doNotShowAgain]; - const telemetrySelections: ['See Instructions', 'Done, it works', "Don't show again"] = [ - 'See Instructions', - 'Done, it works', - "Don't show again", - ]; - const selection = await this.appShell.showWarningMessage(Interpreters.terminalDeactivatePrompt, ...prompts); - if (!selection) { - return; - } - sendTelemetryEvent(EventName.TERMINAL_DEACTIVATE_PROMPT, undefined, { - selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, - }); - if (selection === prompts[0]) { - const url = `https://aka.ms/AAmx2ft`; - this.browserService.launch(url); - } - if (selection === prompts[1] || selection === prompts[2]) { - await notificationPromptEnabled.updateValue(false); - } - } -} diff --git a/src/client/terminals/serviceRegistry.ts b/src/client/terminals/serviceRegistry.ts index a9da776d011a..a39ef31a8fe4 100644 --- a/src/client/terminals/serviceRegistry.ts +++ b/src/client/terminals/serviceRegistry.ts @@ -1,26 +1,25 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { IServiceManager } from '../ioc/types'; +import { interfaces } from 'inversify'; +import { ClassType } from '../ioc/types'; import { TerminalAutoActivation } from './activation'; import { CodeExecutionManager } from './codeExecution/codeExecutionManager'; import { DjangoShellCodeExecutionProvider } from './codeExecution/djangoShellCodeExecution'; import { CodeExecutionHelper } from './codeExecution/helper'; import { ReplProvider } from './codeExecution/repl'; import { TerminalCodeExecutionProvider } from './codeExecution/terminalCodeExecution'; -import { - ICodeExecutionHelper, - ICodeExecutionManager, - ICodeExecutionService, - ITerminalAutoActivation, - ITerminalEnvVarCollectionService, -} from './types'; -import { TerminalEnvVarCollectionService } from './envCollectionActivation/service'; -import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; -import { TerminalDeactivateLimitationPrompt } from './envCollectionActivation/deactivatePrompt'; -import { TerminalIndicatorPrompt } from './envCollectionActivation/indicatorPrompt'; +import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, ITerminalAutoActivation } from './types'; -export function registerTypes(serviceManager: IServiceManager): void { +interface IServiceRegistry { + addSingleton( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol, + ): void; +} + +export function registerTypes(serviceManager: IServiceRegistry): void { serviceManager.addSingleton(ICodeExecutionHelper, CodeExecutionHelper); serviceManager.addSingleton(ICodeExecutionManager, CodeExecutionManager); @@ -38,17 +37,4 @@ export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ICodeExecutionService, ReplProvider, 'repl'); serviceManager.addSingleton(ITerminalAutoActivation, TerminalAutoActivation); - serviceManager.addSingleton( - ITerminalEnvVarCollectionService, - TerminalEnvVarCollectionService, - ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - TerminalIndicatorPrompt, - ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - TerminalDeactivateLimitationPrompt, - ); - serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); } diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index 48d60adf3f39..47ac16d9e08b 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -33,11 +33,3 @@ export interface ITerminalAutoActivation extends IDisposable { register(): void; disableAutoActivation(terminal: Terminal): void; } - -export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); -export interface ITerminalEnvVarCollectionService { - /** - * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. - */ - isTerminalPromptSetCorrectly(resource?: Resource): boolean; -} diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts index 5d4da49ebb45..baa83c8b11c5 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts @@ -13,13 +13,13 @@ import { IPersistentStateFactory, IPythonSettings, } from '../../../client/common/types'; -import { TerminalIndicatorPrompt } from '../../../client/terminals/envCollectionActivation/indicatorPrompt'; +import { TerminalEnvVarCollectionPrompt } from '../../../client/interpreter/activation/terminalEnvVarCollectionPrompt'; +import { ITerminalEnvVarCollectionService } from '../../../client/interpreter/activation/types'; import { Common, Interpreters } from '../../../client/common/utils/localize'; import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; import { sleep } from '../../core'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { ITerminalEnvVarCollectionService } from '../../../client/terminals/types'; suite('Terminal Environment Variable Collection Prompt', () => { let shell: IApplicationShell; @@ -28,7 +28,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { let activeResourceService: IActiveResourceService; let terminalEnvVarCollectionService: ITerminalEnvVarCollectionService; let persistentStateFactory: IPersistentStateFactory; - let terminalEnvVarCollectionPrompt: TerminalIndicatorPrompt; + let terminalEnvVarCollectionPrompt: TerminalEnvVarCollectionPrompt; let terminalEventEmitter: EventEmitter; let notificationEnabled: IPersistentState; let configurationService: IConfigurationService; @@ -61,7 +61,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { ); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); when(terminalManager.onDidOpenTerminal).thenReturn(terminalEventEmitter.event); - terminalEnvVarCollectionPrompt = new TerminalIndicatorPrompt( + terminalEnvVarCollectionPrompt = new TerminalEnvVarCollectionPrompt( instance(shell), instance(persistentStateFactory), instance(terminalManager), diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 5e572e7ad06f..e41d6ce4d53c 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -32,7 +32,7 @@ import { import { Interpreters } from '../../../client/common/utils/localize'; import { OSType, getOSType } from '../../../client/common/utils/platform'; import { defaultShells } from '../../../client/interpreter/activation/service'; -import { TerminalEnvVarCollectionService } from '../../../client/terminals/envCollectionActivation/service'; +import { TerminalEnvVarCollectionService } from '../../../client/interpreter/activation/terminalEnvVarCollectionService'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PathUtils } from '../../../client/common/platform/pathUtils'; diff --git a/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts b/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts deleted file mode 100644 index acd8ee99e5d7..000000000000 --- a/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; -import { EventEmitter, Terminal, TerminalDataWriteEvent, Uri } from 'vscode'; -import { IApplicationEnvironment, IApplicationShell } from '../../../client/common/application/types'; -import { - IBrowserService, - IExperimentService, - IPersistentState, - IPersistentStateFactory, -} from '../../../client/common/types'; -import { Common, Interpreters } from '../../../client/common/utils/localize'; -import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; -import { sleep } from '../../core'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { TerminalDeactivateLimitationPrompt } from '../../../client/terminals/envCollectionActivation/deactivatePrompt'; -import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; -import { TerminalShellType } from '../../../client/common/terminal/types'; - -suite('Terminal Deactivation Limitation Prompt', () => { - let shell: IApplicationShell; - let experimentService: IExperimentService; - let persistentStateFactory: IPersistentStateFactory; - let appEnvironment: IApplicationEnvironment; - let deactivatePrompt: TerminalDeactivateLimitationPrompt; - let terminalWriteEvent: EventEmitter; - let notificationEnabled: IPersistentState; - let browserService: IBrowserService; - let interpreterService: IInterpreterService; - const prompts = [Common.seeInstructions, Interpreters.deactivateDoneButton, Common.doNotShowAgain]; - const expectedMessage = Interpreters.terminalDeactivatePrompt; - - setup(async () => { - shell = mock(); - interpreterService = mock(); - experimentService = mock(); - persistentStateFactory = mock(); - appEnvironment = mock(); - when(appEnvironment.shell).thenReturn('bash'); - browserService = mock(); - notificationEnabled = mock>(); - terminalWriteEvent = new EventEmitter(); - when(persistentStateFactory.createGlobalPersistentState(anything(), true)).thenReturn( - instance(notificationEnabled), - ); - when(shell.onDidWriteTerminalData).thenReturn(terminalWriteEvent.event); - when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); - deactivatePrompt = new TerminalDeactivateLimitationPrompt( - instance(shell), - instance(persistentStateFactory), - [], - instance(interpreterService), - instance(browserService), - instance(appEnvironment), - instance(experimentService), - ); - }); - - test('Show notification when "deactivate" command is run when a virtual env is selected', async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); - }); - - test('When using cmd, do not show notification for the same', async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - reset(appEnvironment); - when(appEnvironment.shell).thenReturn(TerminalShellType.commandPrompt); - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); - }); - - test('When not in experiment, do not show notification for the same', async () => { - reset(experimentService); - when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); - }); - - test('Do not show notification if notification is disabled', async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(false); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); - }); - - test('Do not show notification when virtual env is not activated for terminal', async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Conda, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); - }); - - test("Disable notification if `Don't show again` is clicked", async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(Common.doNotShowAgain)); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(notificationEnabled.updateValue(false)).once(); - }); - - test('Disable notification if `Done, it works` is clicked', async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn( - Promise.resolve(Interpreters.deactivateDoneButton), - ); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(notificationEnabled.updateValue(false)).once(); - }); - - test('Open link to workaround if `See instructions` is clicked', async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(Common.seeInstructions)); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); - verify(browserService.launch(anything())).once(); - }); - - test('Do not perform any action if prompt is closed', async () => { - const resource = Uri.file('a'); - const terminal = ({ - creationOptions: { - cwd: resource, - }, - } as unknown) as Terminal; - when(notificationEnabled.value).thenReturn(true); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ - type: PythonEnvType.Virtual, - } as unknown) as PythonEnvironment); - when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); - - await deactivatePrompt.activate(); - terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); - await sleep(1); - - verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); - verify(notificationEnabled.updateValue(false)).never(); - verify(browserService.launch(anything())).never(); - }); -}); diff --git a/src/test/terminals/serviceRegistry.unit.test.ts b/src/test/terminals/serviceRegistry.unit.test.ts index 816afa17cf88..38a9a9744e91 100644 --- a/src/test/terminals/serviceRegistry.unit.test.ts +++ b/src/test/terminals/serviceRegistry.unit.test.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import * as typemoq from 'typemoq'; -import { IExtensionActivationService, IExtensionSingleActivationService } from '../../client/activation/types'; import { IServiceManager } from '../../client/ioc/types'; import { TerminalAutoActivation } from '../../client/terminals/activation'; import { CodeExecutionManager } from '../../client/terminals/codeExecution/codeExecutionManager'; @@ -10,16 +9,12 @@ import { DjangoShellCodeExecutionProvider } from '../../client/terminals/codeExe import { CodeExecutionHelper } from '../../client/terminals/codeExecution/helper'; import { ReplProvider } from '../../client/terminals/codeExecution/repl'; import { TerminalCodeExecutionProvider } from '../../client/terminals/codeExecution/terminalCodeExecution'; -import { TerminalDeactivateLimitationPrompt } from '../../client/terminals/envCollectionActivation/deactivatePrompt'; -import { TerminalIndicatorPrompt } from '../../client/terminals/envCollectionActivation/indicatorPrompt'; -import { TerminalEnvVarCollectionService } from '../../client/terminals/envCollectionActivation/service'; import { registerTypes } from '../../client/terminals/serviceRegistry'; import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, ITerminalAutoActivation, - ITerminalEnvVarCollectionService, } from '../../client/terminals/types'; suite('Terminal - Service Registry', () => { @@ -32,9 +27,6 @@ suite('Terminal - Service Registry', () => { [ICodeExecutionService, ReplProvider, 'repl'], [ITerminalAutoActivation, TerminalAutoActivation], [ICodeExecutionService, TerminalCodeExecutionProvider, 'standard'], - [ITerminalEnvVarCollectionService, TerminalEnvVarCollectionService], - [IExtensionSingleActivationService, TerminalIndicatorPrompt], - [IExtensionSingleActivationService, TerminalDeactivateLimitationPrompt], ].forEach((args) => { if (args.length === 2) { services @@ -58,14 +50,6 @@ suite('Terminal - Service Registry', () => { .verifiable(typemoq.Times.once()); } }); - services - .setup((s) => - s.addBinding( - typemoq.It.is((v) => ITerminalEnvVarCollectionService === v), - typemoq.It.is((value) => IExtensionActivationService === value), - ), - ) - .verifiable(typemoq.Times.once()); registerTypes(services.object); diff --git a/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts b/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts deleted file mode 100644 index 6913b862c70f..000000000000 --- a/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - // https://github.com/microsoft/vscode/issues/78502 - // - // This API is still proposed but we don't intent on promoting it to stable due to problems - // around performance. See #145234 for a more likely API to get stabilized. - - export interface TerminalDataWriteEvent { - /** - * The {@link Terminal} for which the data was written. - */ - readonly terminal: Terminal; - /** - * The data being written. - */ - readonly data: string; - } - - namespace window { - /** - * An event which fires when the terminal's child pseudo-device is written to (the shell). - * In other words, this provides access to the raw data stream from the process running - * within the terminal, including VT sequences. - */ - export const onDidWriteTerminalData: Event; - } -} From e7dfef8f5ed9f9b59b75fd9d0d64118734224bdd Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 6 Oct 2023 11:57:18 -0700 Subject: [PATCH 0233/1136] fix to get user defined env and use it to set up testing subprocess (#22165) fixes https://github.com/microsoft/vscode-python/issues/21642 and https://github.com/microsoft/vscode-python/issues/22166 --- .../testing/testController/common/server.ts | 18 ++++---- .../testing/testController/common/types.ts | 2 + .../testing/testController/controller.ts | 6 +++ .../pytest/pytestDiscoveryAdapter.ts | 15 ++++--- .../pytest/pytestExecutionAdapter.ts | 32 +++++++------ .../unittest/testDiscoveryAdapter.ts | 17 +++++-- .../unittest/testExecutionAdapter.ts | 8 +++- .../testing/common/testingAdapter.test.ts | 15 +++++++ .../pytestDiscoveryAdapter.unit.test.ts | 45 ++++++++++++------- .../pytestExecutionAdapter.unit.test.ts | 37 ++++++++------- .../testController/server.unit.test.ts | 32 ++++++++----- .../testCancellationRunAdapters.unit.test.ts | 2 + .../testDiscoveryAdapter.unit.test.ts | 36 +++++++-------- 13 files changed, 171 insertions(+), 94 deletions(-) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 46217eab0459..7437a44d6080 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -23,6 +23,7 @@ import { extractJsonPayload, } from './utils'; import { createDeferred } from '../../../common/utils/async'; +import { EnvironmentVariables } from '../../../api/types'; export class PythonTestServer implements ITestServer, Disposable { private _onDataReceived: EventEmitter = new EventEmitter(); @@ -165,28 +166,29 @@ export class PythonTestServer implements ITestServer, Disposable { async sendCommand( options: TestCommandOptions, + env: EnvironmentVariables, runTestIdPort?: string, runInstance?: TestRun, testIds?: string[], callback?: () => void, ): Promise { const { uuid } = options; - + // get and edit env vars + const mutableEnv = { ...env }; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [options.cwd, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_UUID = uuid.toString(); + mutableEnv.TEST_PORT = this.getPort().toString(); + mutableEnv.RUN_TEST_IDS_PORT = runTestIdPort; + const spawnOptions: SpawnOptions = { token: options.token, cwd: options.cwd, throwOnStdErr: true, outputChannel: options.outChannel, - extraVariables: { - PYTHONPATH: pythonPathCommand, - TEST_UUID: uuid.toString(), - TEST_PORT: this.getPort().toString(), - }, + env: mutableEnv, }; - - if (spawnOptions.extraVariables) spawnOptions.extraVariables.RUN_TEST_IDS_PORT = runTestIdPort; const isRun = runTestIdPort !== undefined; // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 32e0c4ba8cc6..e51270eb4f9e 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -15,6 +15,7 @@ import { import { ITestDebugLauncher, TestDiscoveryOptions } from '../../common/types'; import { IPythonExecutionFactory } from '../../../common/process/types'; import { Deferred } from '../../../common/utils/async'; +import { EnvironmentVariables } from '../../../common/variables/types'; export type TestRunInstanceOptions = TestRunOptions & { exclude?: readonly TestItem[]; @@ -177,6 +178,7 @@ export interface ITestServer { readonly onDiscoveryDataReceived: Event; sendCommand( options: TestCommandOptions, + env: EnvironmentVariables, runTestIdsPort?: string, runInstance?: TestRun, testIds?: string[], diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index af77ab2b2525..a87017a26a51 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -50,6 +50,7 @@ import { ITestDebugLauncher } from '../common/types'; import { IServiceContainer } from '../../ioc/types'; import { PythonResultResolver } from './common/resultResolver'; import { onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis'; +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; // Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER]; @@ -100,6 +101,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, @inject(ITestOutputChannel) private readonly testOutputChannel: ITestOutputChannel, @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, + @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider, ) { this.refreshCancellation = new CancellationTokenSource(); @@ -174,12 +176,14 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.configSettings, this.testOutputChannel, resultResolver, + this.envVarsService, ); executionAdapter = new UnittestTestExecutionAdapter( this.pythonTestServer, this.configSettings, this.testOutputChannel, resultResolver, + this.envVarsService, ); } else { testProvider = PYTEST_PROVIDER; @@ -189,12 +193,14 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.configSettings, this.testOutputChannel, resultResolver, + this.envVarsService, ); executionAdapter = new PytestTestExecutionAdapter( this.pythonTestServer, this.configSettings, this.testOutputChannel, resultResolver, + this.envVarsService, ); } diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index c0e1a310ee4a..4ed2570ba7cc 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -19,6 +19,7 @@ import { ITestServer, } from '../common/types'; import { createDiscoveryErrorPayload, createEOTPayload, createTestingDeferred } from '../common/utils'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied @@ -29,6 +30,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} async discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { @@ -61,18 +63,21 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const { pytestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + // get and edit env vars + const mutableEnv = { + ...(await this.envVarsService?.getEnvironmentVariables(uri)), + }; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_UUID = uuid.toString(); + mutableEnv.TEST_PORT = this.testServer.getPort().toString(); const spawnOptions: SpawnOptions = { cwd, throwOnStdErr: true, - extraVariables: { - PYTHONPATH: pythonPathCommand, - TEST_UUID: uuid.toString(), - TEST_PORT: this.testServer.getPort().toString(), - }, outputChannel: this.outputChannel, + env: mutableEnv, }; // Create the Python environment in which to execute the command. diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 8020be17cf90..eb8e9b6f935a 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -24,6 +24,7 @@ import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { PYTEST_PROVIDER } from '../../common/constants'; import { EXTENSION_ROOT_DIR } from '../../../common/constants'; import * as utils from '../common/utils'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; export class PytestTestExecutionAdapter implements ITestExecutionAdapter { constructor( @@ -31,6 +32,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} async runTests( @@ -46,6 +48,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const deferredTillEOT: Deferred = utils.createTestingDeferred(); const dataReceivedDisposable = this.testServer.onRunDataReceived((e: DataReceivedEvent) => { + runInstance?.token.isCancellationRequested; if (runInstance) { const eParsed = JSON.parse(e.data); this.resultResolver?.resolveExecution(eParsed, runInstance, deferredTillEOT); @@ -105,20 +108,13 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; - + // get and edit env vars + const mutableEnv = { ...(await this.envVarsService?.getEnvironmentVariables(uri)) }; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - const spawnOptions: SpawnOptions = { - cwd, - throwOnStdErr: true, - extraVariables: { - PYTHONPATH: pythonPathCommand, - TEST_UUID: uuid.toString(), - TEST_PORT: this.testServer.getPort().toString(), - }, - outputChannel: this.outputChannel, - stdinStr: testIds.toString(), - }; + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_UUID = uuid.toString(); + mutableEnv.TEST_PORT = this.testServer.getPort().toString(); // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { @@ -141,9 +137,17 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testArgs.push('--capture', 'no'); } + // add port with run test ids to env vars const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); - if (spawnOptions.extraVariables) - spawnOptions.extraVariables.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); + mutableEnv.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); + + const spawnOptions: SpawnOptions = { + cwd, + throwOnStdErr: true, + outputChannel: this.outputChannel, + stdinStr: testIds.toString(), + env: mutableEnv, + }; if (debugBool) { const pytestPort = this.testServer.getPort().toString(); diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 440df4f94dc6..75e29afc9712 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -15,6 +15,7 @@ import { TestDiscoveryCommand, } from '../common/types'; import { Deferred, createDeferred } from '../../../common/utils/async'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../../common/variables/types'; /** * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. @@ -25,13 +26,17 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} public async discoverTests(uri: Uri): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; - + let env: EnvironmentVariables | undefined = await this.envVarsService?.getEnvironmentVariables(uri); + if (env === undefined) { + env = {} as EnvironmentVariables; + } const command = buildDiscoveryCommand(unittestArgs); const uuid = this.testServer.createUUID(uri.fsPath); @@ -52,7 +57,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { dataReceivedDisposable.dispose(); }; - await this.callSendCommand(options, () => { + await this.callSendCommand(options, env, () => { disposeDataReceiver?.(this.testServer); }); await deferredTillEOT.promise; @@ -66,8 +71,12 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { return discoveryPayload; } - private async callSendCommand(options: TestCommandOptions, callback: () => void): Promise { - await this.testServer.sendCommand(options, undefined, undefined, [], callback); + private async callSendCommand( + options: TestCommandOptions, + env: EnvironmentVariables, + callback: () => void, + ): Promise { + await this.testServer.sendCommand(options, env, undefined, undefined, [], callback); const discoveryPayload: DiscoveredTestPayload = { cwd: '', status: 'success' }; return discoveryPayload; } diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 9da0872ef601..d90581a93110 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -17,6 +17,7 @@ import { } from '../common/types'; import { traceError, traceInfo, traceLog } from '../../../logging'; import { startTestIdServer } from '../common/utils'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../../common/variables/types'; /** * Wrapper Class for unittest test execution. This is where we call `runTestCommand`? @@ -28,6 +29,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { public configSettings: IConfigurationService, private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} public async runTests( @@ -78,6 +80,10 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; const command = buildExecutionCommand(unittestArgs); + let env: EnvironmentVariables | undefined = await this.envVarsService?.getEnvironmentVariables(uri); + if (env === undefined) { + env = {} as EnvironmentVariables; + } const options: TestCommandOptions = { workspaceFolder: uri, @@ -92,7 +98,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const runTestIdsPort = await startTestIdServer(testIds); - await this.testServer.sendCommand(options, runTestIdsPort.toString(), runInstance, testIds, () => { + await this.testServer.sendCommand(options, env, runTestIdsPort.toString(), runInstance, testIds, () => { deferredTillEOT?.resolve(); }); // placeholder until after the rewrite is adopted diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 4f46f1cf738c..3b5ef0062a98 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -20,6 +20,7 @@ import { UnittestTestExecutionAdapter } from '../../../client/testing/testContro import { PythonResultResolver } from '../../../client/testing/testController/common/resultResolver'; import { TestProvider } from '../../../client/testing/types'; import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; suite('End to End Tests: test adapters', () => { let resultResolver: ITestResultResolver; @@ -28,6 +29,7 @@ suite('End to End Tests: test adapters', () => { let debugLauncher: ITestDebugLauncher; let configService: IConfigurationService; let serviceContainer: IServiceContainer; + let envVarsService: IEnvironmentVariablesProvider; let workspaceUri: Uri; let testOutputChannel: typeMoq.IMock; let testController: TestController; @@ -67,6 +69,7 @@ suite('End to End Tests: test adapters', () => { pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); debugLauncher = serviceContainer.get(ITestDebugLauncher); testController = serviceContainer.get(ITestController); + envVarsService = serviceContainer.get(IEnvironmentVariablesProvider); // create objects that were not injected pythonTestServer = new PythonTestServer(pythonExecFactory, debugLauncher); @@ -121,6 +124,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); await discoveryAdapter.discoverTests(workspaceUri).finally(() => { @@ -167,6 +171,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); await discoveryAdapter.discoverTests(workspaceUri).finally(() => { @@ -206,6 +211,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); // set workspace to test workspace folder @@ -248,6 +254,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); // set workspace to test workspace folder @@ -301,6 +308,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -353,6 +361,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -403,6 +412,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -467,6 +477,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -524,6 +535,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -579,6 +591,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); // set workspace to test workspace folder @@ -641,6 +654,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); const testRun = typeMoq.Mock.ofType(); testRun @@ -701,6 +715,7 @@ suite('End to End Tests: test adapters', () => { configService, testOutputChannel.object, resultResolver, + envVarsService, ); const testRun = typeMoq.Mock.ofType(); testRun diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 8ba7dd9a6f00..7badb5a0350d 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -33,6 +33,7 @@ suite('pytest test discovery adapter', () => { let uri: Uri; let expectedExtraVariables: Record; let mockProc: MockChildProcess; + let deferred2: Deferred; setup(() => { const mockExtensionRootDir = typeMoq.Mock.ofType(); @@ -73,20 +74,25 @@ suite('pytest test discovery adapter', () => { // set up exec service with child process mockProc = new MockChildProcess('', ['']); execService = typeMoq.Mock.ofType(); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + outputChannel = typeMoq.Mock.ofType(); + const output = new Observable>(() => { /* no op */ }); + deferred2 = createDeferred(); execService .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => ({ - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - })); - execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); - outputChannel = typeMoq.Mock.ofType(); + .returns(() => { + deferred2.resolve(); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); }); test('Discovery should call exec with correct basic args', async () => { // set up exec mock @@ -98,24 +104,28 @@ suite('pytest test discovery adapter', () => { deferred.resolve(); return Promise.resolve(execService.object); }); - adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); adapter.discoverTests(uri, execFactory.object); // add in await and trigger await deferred.promise; + await deferred2.promise; mockProc.trigger('close'); // verification - const expectedArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only', '.']; execService.verify( (x) => x.execObservable( - expectedArgs, + typeMoq.It.isAny(), typeMoq.It.is((options) => { - assert.deepEqual(options.extraVariables, expectedExtraVariables); - assert.equal(options.cwd, expectedPath); - assert.equal(options.throwOnStdErr, true); - return true; + try { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } }), ), typeMoq.Times.once(), @@ -147,6 +157,7 @@ suite('pytest test discovery adapter', () => { adapter.discoverTests(uri, execFactory.object); // add in await and trigger await deferred.promise; + await deferred2.promise; mockProc.trigger('close'); // verification @@ -156,7 +167,7 @@ suite('pytest test discovery adapter', () => { x.execObservable( expectedArgs, typeMoq.It.is((options) => { - assert.deepEqual(options.extraVariables, expectedExtraVariables); + assert.deepEqual(options.env, expectedExtraVariables); assert.equal(options.cwd, expectedPathNew); assert.equal(options.throwOnStdErr, true); return true; diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index a2e5c810dc86..a097df654360 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -30,6 +30,7 @@ suite('pytest test execution adapter', () => { let adapter: PytestTestExecutionAdapter; let execService: typeMoq.IMock; let deferred: Deferred; + let deferred4: Deferred; let debugLauncher: typeMoq.IMock; (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; let myTestPath: string; @@ -59,16 +60,20 @@ suite('pytest test execution adapter', () => { const output = new Observable>(() => { /* no op */ }); + deferred4 = createDeferred(); execService = typeMoq.Mock.ofType(); execService .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => ({ - proc: mockProc, - out: output, - dispose: () => { - /* no-body */ - }, - })); + .returns(() => { + deferred4.resolve(); + return { + proc: mockProc, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); execFactory = typeMoq.Mock.ofType(); utilsStartServerStub = sinon.stub(util, 'startTestIdServer'); debugLauncher = typeMoq.Mock.ofType(); @@ -161,6 +166,7 @@ suite('pytest test execution adapter', () => { await deferred2.promise; await deferred3.promise; + await deferred4.promise; mockProc.trigger('close'); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); @@ -176,10 +182,10 @@ suite('pytest test execution adapter', () => { x.execObservable( expectedArgs, typeMoq.It.is((options) => { - assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); - assert.equal(options.extraVariables?.TEST_UUID, expectedExtraVariables.TEST_UUID); - assert.equal(options.extraVariables?.TEST_PORT, expectedExtraVariables.TEST_PORT); - assert.equal(options.extraVariables?.RUN_TEST_IDS_PORT, '54321'); + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_UUID, expectedExtraVariables.TEST_UUID); + assert.equal(options.env?.TEST_PORT, expectedExtraVariables.TEST_PORT); + assert.equal(options.env?.RUN_TEST_IDS_PORT, '54321'); assert.equal(options.cwd, uri.fsPath); assert.equal(options.throwOnStdErr, true); return true; @@ -227,6 +233,7 @@ suite('pytest test execution adapter', () => { await deferred2.promise; await deferred3.promise; + await deferred4.promise; mockProc.trigger('close'); const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); @@ -243,10 +250,10 @@ suite('pytest test execution adapter', () => { x.execObservable( expectedArgs, typeMoq.It.is((options) => { - assert.equal(options.extraVariables?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); - assert.equal(options.extraVariables?.TEST_UUID, expectedExtraVariables.TEST_UUID); - assert.equal(options.extraVariables?.TEST_PORT, expectedExtraVariables.TEST_PORT); - assert.equal(options.extraVariables?.RUN_TEST_IDS_PORT, '54321'); + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_UUID, expectedExtraVariables.TEST_UUID); + assert.equal(options.env?.TEST_PORT, expectedExtraVariables.TEST_PORT); + assert.equal(options.env?.RUN_TEST_IDS_PORT, '54321'); assert.equal(options.cwd, newCwd); assert.equal(options.throwOnStdErr, true); return true; diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 02c35e806156..eaf94eca5189 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -139,7 +139,7 @@ suite('Python Test Server, DataWithPayloadChunks', () => { traceLog('Socket connection error:', error); }); - server.sendCommand(options); + server.sendCommand(options, {}); await deferred.promise; const expectedResult = dataWithPayloadChunks.data; assert.deepStrictEqual(eventData, expectedResult); @@ -176,32 +176,35 @@ suite('Python Test Server, Send command etc', () => { test('sendCommand should add the port to the command being sent and add the correct extra spawn variables', async () => { const deferred2 = createDeferred(); const RUN_TEST_IDS_PORT_CONST = '5678'; + let error = false; + let errorMessage = ''; execService .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns((_args, options2) => { try { assert.strictEqual( - options2.extraVariables.PYTHONPATH, + options2.env.PYTHONPATH, '/foo/bar', 'Expect python path to exist as extra variable and be set correctly', ); assert.strictEqual( - options2.extraVariables.RUN_TEST_IDS_PORT, + options2.env.RUN_TEST_IDS_PORT, RUN_TEST_IDS_PORT_CONST, 'Expect test id port to be in extra variables and set correctly', ); assert.strictEqual( - options2.extraVariables.TEST_UUID, + options2.env.TEST_UUID, FAKE_UUID, 'Expect test uuid to be in extra variables and set correctly', ); assert.strictEqual( - options2.extraVariables.TEST_PORT, - 12345, + options2.env.TEST_PORT, + '12345', 'Expect server port to be set correctly as a env var', ); } catch (e) { - assert(false, 'Error parsing data, extra variables do not match'); + error = true; + errorMessage = `error occurred, assertion was incorrect, ${e}`; } return typeMoq.Mock.ofType>().object; }); @@ -222,13 +225,20 @@ suite('Python Test Server, Send command etc', () => { cwd: '/foo/bar', uuid: FAKE_UUID, }; - server.sendCommand(options, RUN_TEST_IDS_PORT_CONST); + try { + server.sendCommand(options, {}, RUN_TEST_IDS_PORT_CONST); + } catch (e) { + assert(false, `Error sending command, ${e}`); + } // add in await and trigger await deferred2.promise; mockProc.trigger('close'); const expectedArgs = ['myscript', '-foo', 'foo']; execService.verify((x) => x.execObservable(expectedArgs, typeMoq.It.isAny()), typeMoq.Times.once()); + if (error) { + assert(false, errorMessage); + } }); test('sendCommand should write to an output channel if it is provided as an option', async () => { @@ -260,13 +270,13 @@ suite('Python Test Server, Send command etc', () => { server = new PythonTestServer(execFactory.object, debugLauncher); await server.serverReady(); - server.sendCommand(options); + server.sendCommand(options, {}); // add in await and trigger await deferred.promise; mockProc.trigger('close'); const expected = ['python', 'myscript', '-foo', 'foo'].join(' '); - + assert.equal(output2.length, 1); assert.deepStrictEqual(output2, [expected]); }); @@ -303,7 +313,7 @@ suite('Python Test Server, Send command etc', () => { eventData = JSON.parse(data); }); - server.sendCommand(options); + server.sendCommand(options, {}); await deferred2.promise; await deferred3.promise; assert.notEqual(eventData, undefined); diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts index e85cd2b62834..a0fb4eea8589 100644 --- a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -239,6 +239,7 @@ suite('Execution Flow Run Adapters', () => { typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), + typeMoq.It.isAny(), ), ) .returns(() => { @@ -319,6 +320,7 @@ suite('Execution Flow Run Adapters', () => { typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), + typeMoq.It.isAny(), ), ) .returns(() => { diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index dc883afdf441..0eee88120f6a 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -9,6 +9,7 @@ import { IConfigurationService, ITestOutputChannel } from '../../../../client/co import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; import { UnittestTestDiscoveryAdapter } from '../../../../client/testing/testController/unittest/testDiscoveryAdapter'; +import { createDeferred } from '../../../../client/common/utils/async'; suite('Unittest test discovery adapter', () => { let stubConfigSettings: IConfigurationService; @@ -26,10 +27,12 @@ suite('Unittest test discovery adapter', () => { test('DiscoverTests should send the discovery command to the test server with the correct args', async () => { let options: TestCommandOptions | undefined; + const deferred = createDeferred(); const stubTestServer = ({ sendCommand(opt: TestCommandOptions): Promise { delete opt.outChannel; options = opt; + deferred.resolve(); return Promise.resolve(); }, onDiscoveryDataReceived: () => { @@ -44,15 +47,12 @@ suite('Unittest test discovery adapter', () => { const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); adapter.discoverTests(uri); - assert.deepStrictEqual(options, { - workspaceFolder: uri, - cwd: uri.fsPath, - command: { - script, - args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'], - }, - uuid: '123456789', - }); + await deferred.promise; + assert.deepStrictEqual(options?.command?.args, ['--udiscovery', '-v', '-s', '.', '-p', 'test*']); + assert.deepStrictEqual(options.workspaceFolder, uri); + assert.deepStrictEqual(options.cwd, uri.fsPath); + assert.deepStrictEqual(options.command.script, script); + assert.deepStrictEqual(options.uuid, '123456789'); }); test('DiscoverTests should respect settings.testings.cwd when present', async () => { let options: TestCommandOptions | undefined; @@ -62,10 +62,12 @@ suite('Unittest test discovery adapter', () => { }), } as unknown) as IConfigurationService; + const deferred = createDeferred(); const stubTestServer = ({ sendCommand(opt: TestCommandOptions): Promise { delete opt.outChannel; options = opt; + deferred.resolve(); return Promise.resolve(); }, onDiscoveryDataReceived: () => { @@ -80,15 +82,11 @@ suite('Unittest test discovery adapter', () => { const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); adapter.discoverTests(uri); - - assert.deepStrictEqual(options, { - workspaceFolder: uri, - cwd: newCwd, - command: { - script, - args: ['--udiscovery', '-v', '-s', '.', '-p', 'test*'], - }, - uuid: '123456789', - }); + await deferred.promise; + assert.deepStrictEqual(options?.command?.args, ['--udiscovery', '-v', '-s', '.', '-p', 'test*']); + assert.deepStrictEqual(options.workspaceFolder, uri); + assert.deepStrictEqual(options.cwd, newCwd); + assert.deepStrictEqual(options.command.script, script); + assert.deepStrictEqual(options.uuid, '123456789'); }); }); From 091e121df020ee7eaafd5e310535245477d40d99 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 6 Oct 2023 18:34:48 -0700 Subject: [PATCH 0234/1136] fix bug with unittest debug not having args (#22169) --- src/client/testing/common/debugLauncher.ts | 12 +--- .../testing/testController/common/server.ts | 2 + .../testController/server.unit.test.ts | 60 ++++++++++++++++++- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 63e2a4543beb..c76557699ff2 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -205,19 +205,13 @@ export class DebugLauncher implements ITestDebugLauncher { } launchArgs.request = 'launch'; - // Both types of tests need to have the port for the test result server. - if (options.runTestIdsPort) { - launchArgs.env = { - ...launchArgs.env, - RUN_TEST_IDS_PORT: options.runTestIdsPort, - }; - } - if (options.testProvider === 'pytest' && pythonTestAdapterRewriteExperiment) { - if (options.pytestPort && options.pytestUUID) { + if (pythonTestAdapterRewriteExperiment) { + if (options.pytestPort && options.pytestUUID && options.runTestIdsPort) { launchArgs.env = { ...launchArgs.env, TEST_PORT: options.pytestPort, TEST_UUID: options.pytestUUID, + RUN_TEST_IDS_PORT: options.runTestIdsPort, }; } else { throw Error( diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 7437a44d6080..50ae1f3f7536 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -211,6 +211,8 @@ export class PythonTestServer implements ITestServer, Disposable { token: options.token, testProvider: UNITTEST_PROVIDER, runTestIdsPort: runTestIdPort, + pytestUUID: uuid.toString(), + pytestPort: this.getPort().toString(), }; traceInfo(`Running DEBUG unittest with arguments: ${args}\r\n`); diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index eaf94eca5189..742492b33ba8 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -16,7 +16,7 @@ import { Output, } from '../../../client/common/process/types'; import { PythonTestServer } from '../../../client/testing/testController/common/server'; -import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { ITestDebugLauncher, LaunchOptions } from '../../../client/testing/common/types'; import { Deferred, createDeferred } from '../../../client/common/utils/async'; import { MockChildProcess } from '../../mocks/mockChildProcess'; import { @@ -240,6 +240,64 @@ suite('Python Test Server, Send command etc', () => { assert(false, errorMessage); } }); + test('sendCommand should add right extra variables to command during debug', async () => { + const deferred2 = createDeferred(); + const RUN_TEST_IDS_PORT_CONST = '5678'; + const error = false; + const errorMessage = ''; + const debugLauncherMock = typeMoq.Mock.ofType(); + let actualLaunchOptions: LaunchOptions = {} as LaunchOptions; + const deferred4 = createDeferred(); + debugLauncherMock + .setup((x) => x.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((options, _) => { + actualLaunchOptions = options; + deferred4.resolve(); + return Promise.resolve(); + }); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => typeMoq.Mock.ofType>().object); + const execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + server = new PythonTestServer(execFactory.object, debugLauncherMock.object); + sinon.stub(server, 'getPort').returns(12345); + // const portServer = server.getPort(); + await server.serverReady(); + const options = { + command: { script: 'myscript', args: ['-foo', 'foo'] }, + workspaceFolder: Uri.file('/foo/bar'), + cwd: '/foo/bar', + uuid: FAKE_UUID, + debugBool: true, + }; + try { + server.sendCommand(options, {}, RUN_TEST_IDS_PORT_CONST); + } catch (e) { + assert(false, `Error sending command, ${e}`); + } + // add in await and trigger + await deferred2.promise; + await deferred4.promise; + mockProc.trigger('close'); + + assert.notDeepEqual(actualLaunchOptions, {}, 'launch options should be set'); + assert.strictEqual(actualLaunchOptions.cwd, '/foo/bar'); + assert.strictEqual(actualLaunchOptions.testProvider, 'unittest'); + assert.strictEqual(actualLaunchOptions.pytestPort, '12345'); + assert.strictEqual(actualLaunchOptions.pytestUUID, 'fake-uuid'); + assert.strictEqual(actualLaunchOptions.runTestIdsPort, '5678'); + + debugLauncherMock.verify((x) => x.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny()), typeMoq.Times.once()); + if (error) { + assert(false, errorMessage); + } + }); test('sendCommand should write to an output channel if it is provided as an option', async () => { const output2: string[] = []; From 92c2a2f0b6fd4fbc6f6f845b5dc2bf36f032b1ce Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 10 Oct 2023 01:13:28 -0700 Subject: [PATCH 0235/1136] Remove `isort` support (#22187) This feature is now moved to `ms-python.isort` extension. For https://github.com/microsoft/vscode-python/issues/22183 Closes https://github.com/microsoft/vscode-python/issues/22147 --- .github/dependabot.yml | 1 - .vscode/settings.json | 1 - build/test-requirements.txt | 1 - package.json | 17 ---- package.nls.json | 5 -- resources/report_issue_user_settings.json | 4 - src/client/common/application/commands.ts | 1 - src/client/common/configSettings.ts | 11 --- src/client/common/constants.ts | 1 - src/client/common/types.ts | 6 -- src/client/common/utils/localize.ts | 7 -- .../codeActionProvider/isortPrompt.ts | 89 ------------------- .../providers/codeActionProvider/main.ts | 25 +----- src/client/telemetry/index.ts | 8 +- src/test/.vscode/settings.json | 1 - src/test/common.ts | 1 - .../configSettings.unit.test.ts | 2 - src/test/common/productsToTest.ts | 1 - .../codeActionProvider/main.unit.test.ts | 6 +- src/testMultiRootWkspc/multi.code-workspace | 4 - 20 files changed, 6 insertions(+), 186 deletions(-) delete mode 100644 src/client/providers/codeActionProvider/isortPrompt.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml index de5ebfe9158b..14c8e18d475d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -37,7 +37,6 @@ updates: - dependency-name: prospector # Due to Python 2.7 and #14477. - dependency-name: pytest # Due to Python 2.7 and #13776. - dependency-name: py # Due to Python 2.7. - - dependency-name: isort - dependency-name: jedi-language-server labels: - 'no-changelog' diff --git a/.vscode/settings.json b/.vscode/settings.json index a5dbb4869fd9..6a9c299aa72b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -47,7 +47,6 @@ "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version "python.linting.enabled": false, "python.formatting.provider": "black", - "python.sortImports.args": ["--profile", "black"], "typescript.preferences.quoteStyle": "single", "javascript.preferences.quoteStyle": "single", "typescriptHero.imports.stringQuoteStyle": "'", diff --git a/build/test-requirements.txt b/build/test-requirements.txt index c732b3bcb228..433bd0f86682 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -17,7 +17,6 @@ flask fastapi uvicorn django -isort # Integrated TensorBoard tests tensorboard diff --git a/package.json b/package.json index 47faec663015..074ae69bca5e 100644 --- a/package.json +++ b/package.json @@ -1161,23 +1161,6 @@ "scope": "machine-overridable", "type": "string" }, - "python.sortImports.args": { - "default": [], - "description": "%python.sortImports.args.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "deprecationMessage": "%python.sortImports.args.deprecationMessage%" - }, - "python.sortImports.path": { - "default": "", - "description": "%python.sortImports.path.description%", - "scope": "machine-overridable", - "type": "string", - "deprecationMessage": "%python.sortImports.path.deprecationMessage%" - }, "python.tensorBoard.logDirectory": { "default": "", "description": "%python.tensorBoard.logDirectory.description%", diff --git a/package.nls.json b/package.nls.json index 7d14baf40d64..7a6f789fdf2d 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,5 +1,4 @@ { - "python.command.python.sortImports.title": "Sort Imports", "python.command.python.startREPL.title": "Start REPL", "python.command.python.createEnvironment.title": "Create Environment...", "python.command.python.createNewFile.title": "New Python File", @@ -201,8 +200,6 @@ "python.missingPackage.severity.description": "Set severity of missing packages in requirements.txt or pyproject.toml", "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", "python.poetryPath.description": "Path to the poetry executable.", - "python.sortImports.args.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.sortImports.path.description": "Path to isort script, default using inner version", "python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.", "python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.", "python.terminal.activateEnvironment.description": "Activate Python Environment in all Terminals created.", @@ -220,8 +217,6 @@ "python.testing.unittestEnabled.description": "Enable testing using unittest.", "python.venvFolders.description": "Folders in your home directory to look into for virtual environments (supports pyenv, direnv and virtualenvwrapper by default).", "python.venvPath.description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", - "python.sortImports.args.deprecationMessage": "This setting will be removed soon. Use 'isort.args' instead.", - "python.sortImports.path.deprecationMessage": "This setting will be removed soon. Use 'isort.path' instead.", "walkthrough.pythonWelcome.title": "Get Started with Python Development", "walkthrough.pythonWelcome.description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!", "walkthrough.step.python.createPythonFile.title": "Create a Python file", diff --git a/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json index 778434c5cf0d..677e58d83f21 100644 --- a/resources/report_issue_user_settings.json +++ b/resources/report_issue_user_settings.json @@ -69,10 +69,6 @@ "memory": true, "symbolsHierarchyDepthLimit": false }, - "sortImports": { - "args": "placeholder", - "path": "placeholder" - }, "formatting": { "autopep8Args": "placeholder", "autopep8Path": "placeholder", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index d8944fe2b057..763fa4dde79d 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -93,7 +93,6 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu ['workbench.action.openIssueReporter']: [{ extensionId: string; issueBody: string }]; [Commands.GetSelectedInterpreterPath]: [{ workspaceFolder: string } | string[]]; [Commands.TriggerEnvironmentSelection]: [undefined | Uri]; - [Commands.Sort_Imports]: [undefined, Uri]; [Commands.Exec_In_Terminal]: [undefined, Uri]; [Commands.Exec_In_Terminal_Icon]: [undefined, Uri]; [Commands.Debug_In_Terminal]: [Uri]; diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index db5944cc794b..3e4b75b8b087 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -32,7 +32,6 @@ import { IInterpreterSettings, ILintingSettings, IPythonSettings, - ISortImportSettings, ITensorBoardSettings, ITerminalSettings, Resource, @@ -120,8 +119,6 @@ export class PythonSettings implements IPythonSettings { public terminal!: ITerminalSettings; - public sortImports!: ISortImportSettings; - public globalModuleInstallation = false; public experiments!: IExperiments; @@ -319,14 +316,6 @@ export class PythonSettings implements IPythonSettings { this.globalModuleInstallation = pythonSettings.get('globalModuleInstallation') === true; - const sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports'))!; - if (this.sortImports) { - Object.assign(this.sortImports, sortImportSettings); - } else { - this.sortImports = sortImportSettings; - } - // Support for travis. - this.sortImports = this.sortImports ? this.sortImports : { path: '', args: [] }; // Support for travis. this.linting = this.linting ? this.linting diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index c3705a3c6504..cd6d305f624a 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -58,7 +58,6 @@ export namespace Commands { export const ReportIssue = 'python.reportIssue'; export const Set_Interpreter = 'python.setInterpreter'; export const Set_ShebangInterpreter = 'python.setShebangInterpreter'; - export const Sort_Imports = 'python.sortImports'; export const Start_REPL = 'python.startREPL'; export const Tests_Configure = 'python.configureTests'; export const TriggerEnvironmentSelection = 'python.triggerEnvSelection'; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index b48a2daadaa6..07f1fea6b86b 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -106,7 +106,6 @@ export enum Product { autopep8 = 10, mypy = 11, unittest = 12, - isort = 15, black = 16, bandit = 17, tensorboard = 24, @@ -190,7 +189,6 @@ export interface IPythonSettings { readonly testing: ITestingSettings; readonly autoComplete: IAutoCompleteSettings; readonly terminal: ITerminalSettings; - readonly sortImports: ISortImportSettings; readonly envFile: string; readonly globalModuleInstallation: boolean; readonly experiments: IExperiments; @@ -204,10 +202,6 @@ export interface IPythonSettings { export interface ITensorBoardSettings { logDirectory: string | undefined; } -export interface ISortImportSettings { - readonly path: string; - readonly args: string[]; -} export interface IPylintCategorySeverity { readonly convention: DiagnosticSeverity; diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index bc32c1078cad..c6086071363f 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -39,9 +39,6 @@ export namespace Diagnostics { 'Your settings needs to be updated to change the setting "python.unitTest." to "python.testing.", otherwise testing Python code using the extension may not work. Would you like to automatically update your settings now?', ); export const updateSettings = l10n.t('Yes, update settings'); - export const checkIsort5UpgradeGuide = l10n.t( - 'We found outdated configuration for sorting imports in this workspace. Check the [isort upgrade guide](https://aka.ms/AA9j5x4) to update your settings.', - ); export const pylanceDefaultMessage = l10n.t( "The Python extension now includes Pylance to improve completions, code navigation, overall performance and much more! You can learn more about the update and learn how to change your language server [here](https://aka.ms/new-python-bundle).\n\nRead Pylance's license [here](https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license).", ); @@ -517,12 +514,8 @@ export namespace ToolsExtensions { export const pylintPromptMessage = l10n.t( 'Use the Pylint extension to enable easier configuration and new features such as quick fixes.', ); - export const isortPromptMessage = l10n.t( - 'To use sort imports, install the isort extension. It provides easier configuration and new features such as code actions.', - ); export const installPylintExtension = l10n.t('Install Pylint extension'); export const installFlake8Extension = l10n.t('Install Flake8 extension'); - export const installISortExtension = l10n.t('Install isort extension'); export const selectBlackFormatterPrompt = l10n.t( 'You have the Black formatter extension installed, would you like to use that as the default formatter?', diff --git a/src/client/providers/codeActionProvider/isortPrompt.ts b/src/client/providers/codeActionProvider/isortPrompt.ts deleted file mode 100644 index ffef481b498d..000000000000 --- a/src/client/providers/codeActionProvider/isortPrompt.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IApplicationEnvironment } from '../../common/application/types'; -import { IPersistentState, IPersistentStateFactory } from '../../common/types'; -import { Common, ToolsExtensions } from '../../common/utils/localize'; -import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { isExtensionDisabled, isExtensionEnabled } from '../../common/vscodeApis/extensionsApi'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../ioc/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; - -export const ISORT_EXTENSION = 'ms-python.isort'; -const ISORT_PROMPT_DONOTSHOW_KEY = 'showISortExtensionPrompt'; - -function doNotShowPromptState(serviceContainer: IServiceContainer, promptKey: string): IPersistentState { - const persistFactory: IPersistentStateFactory = serviceContainer.get( - IPersistentStateFactory, - ); - return persistFactory.createWorkspacePersistentState(promptKey, false); -} - -export class ISortExtensionPrompt { - private shownThisSession = false; - - public constructor(private readonly serviceContainer: IServiceContainer) {} - - public async showPrompt(): Promise { - const isEnabled = isExtensionEnabled(ISORT_EXTENSION); - if (isEnabled || isExtensionDisabled(ISORT_EXTENSION)) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED, undefined, { - extensionId: ISORT_EXTENSION, - isEnabled, - }); - return true; - } - - const doNotShow = doNotShowPromptState(this.serviceContainer, ISORT_PROMPT_DONOTSHOW_KEY); - if (this.shownThisSession || doNotShow.value) { - return false; - } - - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_SHOWN, undefined, { extensionId: ISORT_EXTENSION }); - this.shownThisSession = true; - const response = await showInformationMessage( - ToolsExtensions.isortPromptMessage, - ToolsExtensions.installISortExtension, - Common.doNotShowAgain, - ); - - if (response === Common.doNotShowAgain) { - await doNotShow.updateValue(true); - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED, undefined, { - extensionId: ISORT_EXTENSION, - dismissType: 'doNotShow', - }); - return false; - } - - if (response === ToolsExtensions.installISortExtension) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_INSTALL_SELECTED, undefined, { - extensionId: ISORT_EXTENSION, - }); - const appEnv: IApplicationEnvironment = this.serviceContainer.get( - IApplicationEnvironment, - ); - await executeCommand('workbench.extensions.installExtension', ISORT_EXTENSION, { - installPreReleaseVersion: appEnv.extensionChannel === 'insiders', - }); - return true; - } - - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED, undefined, { - extensionId: ISORT_EXTENSION, - dismissType: 'close', - }); - - return false; - } -} - -let _prompt: ISortExtensionPrompt | undefined; -export function getOrCreateISortPrompt(serviceContainer: IServiceContainer): ISortExtensionPrompt { - if (!_prompt) { - _prompt = new ISortExtensionPrompt(serviceContainer); - } - return _prompt; -} diff --git a/src/client/providers/codeActionProvider/main.ts b/src/client/providers/codeActionProvider/main.ts index 40afd4dbb2b2..259f42848606 100644 --- a/src/client/providers/codeActionProvider/main.ts +++ b/src/client/providers/codeActionProvider/main.ts @@ -4,23 +4,14 @@ import { inject, injectable } from 'inversify'; import * as vscodeTypes from 'vscode'; import { IExtensionSingleActivationService } from '../../activation/types'; -import { Commands } from '../../common/constants'; import { IDisposableRegistry } from '../../common/types'; -import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; -import { isExtensionEnabled } from '../../common/vscodeApis/extensionsApi'; -import { IServiceContainer } from '../../ioc/types'; -import { traceLog } from '../../logging'; -import { getOrCreateISortPrompt, ISORT_EXTENSION } from './isortPrompt'; import { LaunchJsonCodeActionProvider } from './launchJsonCodeActionProvider'; @injectable() export class CodeActionProviderService implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - constructor( - @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, - @inject(IServiceContainer) private serviceContainer: IServiceContainer, - ) {} + constructor(@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry) {} public async activate(): Promise { // eslint-disable-next-line global-require @@ -35,19 +26,5 @@ export class CodeActionProviderService implements IExtensionSingleActivationServ providedCodeActionKinds: [vscode.CodeActionKind.QuickFix], }), ); - this.disposableRegistry.push( - registerCommand(Commands.Sort_Imports, async () => { - const prompt = getOrCreateISortPrompt(this.serviceContainer); - await prompt.showPrompt(); - if (!isExtensionEnabled(ISORT_EXTENSION)) { - traceLog( - 'Sort Imports: Please install and enable `ms-python.isort` extension to use this feature.', - ); - return; - } - - executeCommand('editor.action.organizeImports'); - }), - ); } } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 600f9a2d48ff..7883e0cd7555 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -2181,7 +2181,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; + extensionId: 'ms-python.pylint' | 'ms-python.flake8'; isEnabled: boolean; }; /** @@ -2193,7 +2193,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.TOOLS_EXTENSIONS_PROMPT_SHOWN]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; + extensionId: 'ms-python.pylint' | 'ms-python.flake8'; }; /** * Telemetry event sent when clicking to install linter or formatter extension from the suggestion prompt. @@ -2204,7 +2204,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.TOOLS_EXTENSIONS_INSTALL_SELECTED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; + extensionId: 'ms-python.pylint' | 'ms-python.flake8'; }; /** * Telemetry event sent when dismissing prompt suggesting to install the linter or formatter extension. @@ -2216,7 +2216,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8' | 'ms-python.isort'; + extensionId: 'ms-python.pylint' | 'ms-python.flake8'; dismissType: 'close' | 'doNotShow'; }; /* __GDPR__ diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index ef9292849a9d..771962b5a909 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -3,7 +3,6 @@ "python.linting.flake8Enabled": false, "python.testing.pytestArgs": [], "python.testing.unittestArgs": ["-s=./tests", "-p=test_*.py", "-v", "-s", ".", "-p", "*test*.py"], - "python.sortImports.args": [], "python.linting.lintOnSave": false, "python.linting.enabled": true, "python.linting.pycodestyleEnabled": false, diff --git a/src/test/common.ts b/src/test/common.ts index 95345f91e5e0..4cc985c795b6 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -53,7 +53,6 @@ export type PythonSettingKeys = | 'testing.pytestArgs' | 'testing.unittestArgs' | 'formatting.provider' - | 'sortImports.args' | 'testing.pytestEnabled' | 'testing.unittestEnabled' | 'envFile' diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index eeaed6aa996b..113770122fbc 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -22,7 +22,6 @@ import { IFormattingSettings, IInterpreterSettings, ILintingSettings, - ISortImportSettings, ITerminalSettings, } from '../../../client/common/types'; import { noop } from '../../../client/common/utils/misc'; @@ -118,7 +117,6 @@ suite('Python Settings', async () => { // complex settings config.setup((c) => c.get('interpreter')).returns(() => sourceSettings.interpreter); config.setup((c) => c.get('linting')).returns(() => sourceSettings.linting); - config.setup((c) => c.get('sortImports')).returns(() => sourceSettings.sortImports); config.setup((c) => c.get('formatting')).returns(() => sourceSettings.formatting); config.setup((c) => c.get('autoComplete')).returns(() => sourceSettings.autoComplete); config.setup((c) => c.get('testing')).returns(() => sourceSettings.testing); diff --git a/src/test/common/productsToTest.ts b/src/test/common/productsToTest.ts index 7fc06863f67c..861bab898509 100644 --- a/src/test/common/productsToTest.ts +++ b/src/test/common/productsToTest.ts @@ -17,7 +17,6 @@ export function getProductsForInstallerTests(): { name: string; value: Product } 'yapf', 'autopep8', 'mypy', - 'isort', 'black', 'bandit', ].includes(p.name), diff --git a/src/test/providers/codeActionProvider/main.unit.test.ts b/src/test/providers/codeActionProvider/main.unit.test.ts index 501c3c7eca2b..55644d80ae54 100644 --- a/src/test/providers/codeActionProvider/main.unit.test.ts +++ b/src/test/providers/codeActionProvider/main.unit.test.ts @@ -8,7 +8,6 @@ import rewiremock from 'rewiremock'; import * as typemoq from 'typemoq'; import { CodeActionKind, CodeActionProvider, CodeActionProviderMetadata, DocumentSelector } from 'vscode'; import { IDisposableRegistry } from '../../../client/common/types'; -import { IServiceContainer } from '../../../client/ioc/types'; import { LaunchJsonCodeActionProvider } from '../../../client/providers/codeActionProvider/launchJsonCodeActionProvider'; import { CodeActionProviderService } from '../../../client/providers/codeActionProvider/main'; @@ -38,10 +37,7 @@ suite('Code Action Provider service', async () => { }; rewiremock.enable(); rewiremock('vscode').with(vscodeMock); - const quickFixService = new CodeActionProviderService( - typemoq.Mock.ofType().object, - typemoq.Mock.ofType().object, - ); + const quickFixService = new CodeActionProviderService(typemoq.Mock.ofType().object); await quickFixService.activate(); diff --git a/src/testMultiRootWkspc/multi.code-workspace b/src/testMultiRootWkspc/multi.code-workspace index 1daf409a0836..9d5c8ac77475 100644 --- a/src/testMultiRootWkspc/multi.code-workspace +++ b/src/testMultiRootWkspc/multi.code-workspace @@ -38,10 +38,6 @@ "python.linting.pycodestyleEnabled": false, "python.linting.prospectorEnabled": false, "python.formatting.provider": "yapf", - "python.sortImports.args": [ - "-sp", - "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/sorting/withconfig" - ], "python.linting.lintOnSave": false, "python.linting.enabled": true, "python.pythonPath": "python" From 066550632f06a6a6b5d0c78ea315e131439fba46 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 10 Oct 2023 09:00:05 -0700 Subject: [PATCH 0236/1136] fix unittest output to remove print of only object reference (#22180) the traceback object was incorrectly printed as just the reference to the object in the error message. Ended up just removing it since it is correctly printed in the traceback object which is where it should ultimately belong. closes: https://github.com/microsoft/vscode-python/issues/22181 --------- Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> --- .../tests/unittestadapter/test_execution.py | 3 +++ pythonFiles/unittestadapter/execution.py | 15 ++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/pythonFiles/tests/unittestadapter/test_execution.py b/pythonFiles/tests/unittestadapter/test_execution.py index f7306e37662e..ccf13d983c60 100644 --- a/pythonFiles/tests/unittestadapter/test_execution.py +++ b/pythonFiles/tests/unittestadapter/test_execution.py @@ -202,6 +202,9 @@ def test_failed_tests(): assert "outcome" in id_result assert id_result["outcome"] == "failure" assert "message" and "traceback" in id_result + assert "2 not greater than 3" in str(id_result["message"]) or "1 == 1" in str( + id_result["traceback"] + ) assert True diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 0684ada8e44b..5f46bda95328 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -104,13 +104,18 @@ def formatResult( subtest: Union[unittest.TestCase, None] = None, ): tb = None - if error and error[2] is not None: - # Format traceback + + message = "" + # error is a tuple of the form returned by sys.exc_info(): (type, value, traceback). + if error is not None: + try: + message = f"{error[0]} {error[1]}" + except Exception: + message = "Error occurred, unknown type or value" formatted = traceback.format_exception(*error) + tb = "".join(formatted) # Remove the 'Traceback (most recent call last)' formatted = formatted[1:] - tb = "".join(formatted) - if subtest: test_id = subtest.id() else: @@ -119,7 +124,7 @@ def formatResult( result = { "test": test.id(), "outcome": outcome, - "message": str(error), + "message": message, "traceback": tb, "subtest": subtest.id() if subtest else None, } From 56661a1576b93430953f249cda582eeef30ff543 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 10 Oct 2023 10:49:05 -0700 Subject: [PATCH 0237/1136] REPL Smart Shift+Enter and Dynamic Smart Cursor (#21779) There are two Feature Requests from: #18105 #21838 They are grouped together to provide the smoothest experience: when user wants to press shift+enter and smoothly move between each executable Python code block without having to manually move their cursor. #19955 (For Execute line/selection and advance to next line, referred to as dynamic smart cursor hereby) Open Issue: #21778 #21838 Steps in implementing REPL Smart Send (smart shift+enter to the REPL) and dynamic cursor move aka. Move to Next Line (next executable line of code to be more precise): 1. Figure out the workflow of where things start and run when user clicks on run selection/line 2. Send the content of selection & document to the Python Side from Typescript side. 3. Respect and follow previous logic/code for EXPLICIT selection (user has highlighting particular text they want to send to REPL), but otherwise, use newly created smart send code. 4. Receive content of document & selection in Python Side 5. Use AST (From Python standard library) to figure out if selection if selection is part of, for example, dictionary, but look for nodes and how each relates to the top level. If some selection is, for example part of a dictionary, we should run the whole dictionary. Look at how to do this for all top level, so that we run the Minimum Viable Block possible. (For example, if user selects part of a dictionary to run in REPL, it will select and send only the dictionary not the whole class or file, etc) 6. Receive the commands to run in typescript side and send it to the REPL 7. After the user has ran shift+enter(non highlight, meaning there was no explicit highlight of text), thus the incurring of smart send, and we have processed the smart selection, figure out the "next" executable line of code in the currently opened Python file. 8. After figuring out the "next" line number, we will move user's cursor to that line number. - [x] Additional scope for telemetry EventName.EXECUTION_CODE with the scope of 'line' in addition to differentiate the explicit selection usage compared to line or executable block. - [x] Drop 3.7 support before merging since end_line attribute of the AST module is only supported for Python version 3.8 and above. - [x] Python tests for both smart selection, dynamic cursor move. - [x] TypeScript tests for smart selection, dynamic cursor move. Notes: * To be shipped after dropping Python3.7 support, since end_lineno, which is critical in smart shift+enter logic, is only for Python version GREATER than 3.7 Update (9/14/23: Python 3.7 support is dropped from the VS Code Python extension: #21962) * Code in regards to this feature(s) should be wrapped in standard experiment (not setting based experiment) * Respective Telemetry should also be attached * EXPLICIT (highlight) selection of the text, and shift+enter/run selection should respect user's selection and send AS IT IS. (When the user selects/highlight specifically what they want to send, we should respect user's selection and send the selection as they are selected) * Smart Shift+Enter should be shipped together with dynamic smart cursor movement for smoothest experience. This way user could shift+enter line by line (or more accurately top block after another top block) as they shift+enter their code. * Be careful with line_no usage between vscode and python as vscode counts line number starting from 0 and python ast start as normal (starts from line 1)) So vscode_lineno + 1 = python_ast_lineno --------- Co-authored-by: Karthik Nadig --- package.json | 12 +- package.nls.json | 1 + pythonFiles/normalizeSelection.py | 149 ++++++- pythonFiles/tests/test_dynamic_cursor.py | 203 +++++++++ pythonFiles/tests/test_normalize_selection.py | 53 +++ pythonFiles/tests/test_smart_selection.py | 388 ++++++++++++++++++ src/client/common/application/commands.ts | 8 + src/client/common/experiments/groups.ts | 4 + src/client/telemetry/index.ts | 4 +- .../codeExecution/codeExecutionManager.ts | 6 +- src/client/terminals/codeExecution/helper.ts | 78 +++- src/client/terminals/types.ts | 2 +- .../terminalExec/sample_smart_selection.py | 21 + src/test/smoke/smartSend.smoke.test.ts | 0 .../terminals/codeExecution/smartSend.test.ts | 229 +++++++++++ .../smokeTests/create_delete_file.py | 5 + 16 files changed, 1148 insertions(+), 15 deletions(-) create mode 100644 pythonFiles/tests/test_dynamic_cursor.py create mode 100644 pythonFiles/tests/test_smart_selection.py create mode 100644 src/test/pythonFiles/terminalExec/sample_smart_selection.py create mode 100644 src/test/smoke/smartSend.smoke.test.ts create mode 100644 src/test/terminals/codeExecution/smartSend.test.ts create mode 100644 src/testMultiRootWkspc/smokeTests/create_delete_file.py diff --git a/package.json b/package.json index 074ae69bca5e..0f93441c6113 100644 --- a/package.json +++ b/package.json @@ -536,14 +536,16 @@ "pythonSurveyNotification", "pythonPromptNewToolsExt", "pythonTerminalEnvVarActivation", - "pythonTestAdapter" + "pythonTestAdapter", + "pythonREPLSmartSend" ], "enumDescriptions": [ "%python.experiments.All.description%", "%python.experiments.pythonSurveyNotification.description%", "%python.experiments.pythonPromptNewToolsExt.description%", "%python.experiments.pythonTerminalEnvVarActivation.description%", - "%python.experiments.pythonTestAdapter.description%" + "%python.experiments.pythonTestAdapter.description%", + "%python.experiments.pythonREPLSmartSend.description%" ] }, "scope": "machine", @@ -559,14 +561,16 @@ "pythonSurveyNotification", "pythonPromptNewToolsExt", "pythonTerminalEnvVarActivation", - "pythonTestAdapter" + "pythonTestAdapter", + "pythonREPLSmartSend" ], "enumDescriptions": [ "%python.experiments.All.description%", "%python.experiments.pythonSurveyNotification.description%", "%python.experiments.pythonPromptNewToolsExt.description%", "%python.experiments.pythonTerminalEnvVarActivation.description%", - "%python.experiments.pythonTestAdapter.description%" + "%python.experiments.pythonTestAdapter.description%", + "%python.experiments.pythonREPLSmartSend.description%" ] }, "scope": "machine", diff --git a/package.nls.json b/package.nls.json index 7a6f789fdf2d..5687e51ab9df 100644 --- a/package.nls.json +++ b/package.nls.json @@ -41,6 +41,7 @@ "python.experiments.pythonPromptNewToolsExt.description": "Denotes the Python Prompt New Tools Extension experiment.", "python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.", "python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.", + "python.experiments.pythonREPLSmartSend.description": "Denotes the Python REPL Smart Send experiment.", "python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.", "python.formatting.autopep8Args.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8).
Learn more [here](https://aka.ms/AAlgvkb).", "python.formatting.autopep8Args.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension. Learn more here: https://aka.ms/AAlgvkb.", diff --git a/pythonFiles/normalizeSelection.py b/pythonFiles/normalizeSelection.py index 0363702717ab..0ac47ab5dc3b 100644 --- a/pythonFiles/normalizeSelection.py +++ b/pythonFiles/normalizeSelection.py @@ -6,6 +6,7 @@ import re import sys import textwrap +from typing import Iterable def split_lines(source): @@ -118,6 +119,8 @@ def normalize_lines(selection): # Insert a newline between each top-level statement, and append a newline to the selection. source = "\n".join(statements) + "\n" + if selection[-2] == "}": + source = source[:-1] except Exception: # If there's a problem when parsing statements, # append a blank line to end the block and send it as-is. @@ -126,17 +129,159 @@ def normalize_lines(selection): return source +top_level_nodes = [] +min_key = None + + +def check_exact_exist(top_level_nodes, start_line, end_line): + exact_nodes = [] + for node in top_level_nodes: + if node.lineno == start_line and node.end_lineno == end_line: + exact_nodes.append(node) + + return exact_nodes + + +def traverse_file(wholeFileContent, start_line, end_line, was_highlighted): + """ + Intended to traverse through a user's given file content and find, collect all appropriate lines + that should be sent to the REPL in case of smart selection. + This could be exact statement such as just a single line print statement, + or a multiline dictionary, or differently styled multi-line list comprehension, etc. + Then call the normalize_lines function to normalize our smartly selected code block. + """ + + parsed_file_content = ast.parse(wholeFileContent) + smart_code = "" + should_run_top_blocks = [] + + # Purpose of this loop is to fetch and collect all the + # AST top level nodes, and its node.body as child nodes. + # Individual nodes will contain information like + # the start line, end line and get source segment information + # that will be used to smartly select, and send normalized code. + for node in ast.iter_child_nodes(parsed_file_content): + top_level_nodes.append(node) + + ast_types_with_nodebody = ( + ast.Module, + ast.Interactive, + ast.Expression, + ast.FunctionDef, + ast.AsyncFunctionDef, + ast.ClassDef, + ast.For, + ast.AsyncFor, + ast.While, + ast.If, + ast.With, + ast.AsyncWith, + ast.Try, + ast.Lambda, + ast.IfExp, + ast.ExceptHandler, + ) + if isinstance(node, ast_types_with_nodebody) and isinstance( + node.body, Iterable + ): + for child_nodes in node.body: + top_level_nodes.append(child_nodes) + + exact_nodes = check_exact_exist(top_level_nodes, start_line, end_line) + + # Just return the exact top level line, if present. + if len(exact_nodes) > 0: + which_line_next = 0 + for same_line_node in exact_nodes: + should_run_top_blocks.append(same_line_node) + smart_code += ( + f"{ast.get_source_segment(wholeFileContent, same_line_node)}\n" + ) + which_line_next = get_next_block_lineno(should_run_top_blocks) + return { + "normalized_smart_result": smart_code, + "which_line_next": which_line_next, + } + + # For each of the nodes in the parsed file content, + # add the appropriate source code line(s) to be sent to the REPL, dependent on + # user is trying to send and execute single line/statement or multiple with smart selection. + for top_node in ast.iter_child_nodes(parsed_file_content): + if start_line == top_node.lineno and end_line == top_node.end_lineno: + should_run_top_blocks.append(top_node) + + smart_code += f"{ast.get_source_segment(wholeFileContent, top_node)}\n" + break # If we found exact match, don't waste computation in parsing extra nodes. + elif start_line >= top_node.lineno and end_line <= top_node.end_lineno: + # Case to apply smart selection for multiple line. + # This is the case for when we have to add multiple lines that should be included in the smart send. + # For example: + # 'my_dictionary': { + # 'Audi': 'Germany', + # 'BMW': 'Germany', + # 'Genesis': 'Korea', + # } + # with the mouse cursor at 'BMW': 'Germany', should send all of the lines that pertains to my_dictionary. + + should_run_top_blocks.append(top_node) + + smart_code += str(ast.get_source_segment(wholeFileContent, top_node)) + smart_code += "\n" + + normalized_smart_result = normalize_lines(smart_code) + which_line_next = get_next_block_lineno(should_run_top_blocks) + return { + "normalized_smart_result": normalized_smart_result, + "which_line_next": which_line_next, + } + + +# Look at the last top block added, find lineno for the next upcoming block, +# This will be used in calculating lineOffset to move cursor in VS Code. +def get_next_block_lineno(which_line_next): + last_ran_lineno = int(which_line_next[-1].end_lineno) + next_lineno = int(which_line_next[-1].end_lineno) + + for reverse_node in top_level_nodes: + if reverse_node.lineno > last_ran_lineno: + next_lineno = reverse_node.lineno + break + return next_lineno + + if __name__ == "__main__": # Content is being sent from the extension as a JSON object. # Decode the data from the raw bytes. stdin = sys.stdin if sys.version_info < (3,) else sys.stdin.buffer raw = stdin.read() contents = json.loads(raw.decode("utf-8")) + # Empty highlight means user has not explicitly selected specific text. + empty_Highlight = contents.get("emptyHighlight", False) - normalized = normalize_lines(contents["code"]) + # We also get the activeEditor selection start line and end line from the typescript VS Code side. + # Remember to add 1 to each of the received since vscode starts line counting from 0 . + vscode_start_line = contents["startLine"] + 1 + vscode_end_line = contents["endLine"] + 1 # Send the normalized code back to the extension in a JSON object. - data = json.dumps({"normalized": normalized}) + data = None + which_line_next = 0 + + if empty_Highlight and contents.get("smartSendExperimentEnabled"): + result = traverse_file( + contents["wholeFileContent"], + vscode_start_line, + vscode_end_line, + not empty_Highlight, + ) + normalized = result["normalized_smart_result"] + which_line_next = result["which_line_next"] + data = json.dumps( + {"normalized": normalized, "nextBlockLineno": result["which_line_next"]} + ) + else: + normalized = normalize_lines(contents["code"]) + data = json.dumps({"normalized": normalized}) stdout = sys.stdout if sys.version_info < (3,) else sys.stdout.buffer stdout.write(data.encode("utf-8")) diff --git a/pythonFiles/tests/test_dynamic_cursor.py b/pythonFiles/tests/test_dynamic_cursor.py new file mode 100644 index 000000000000..7aea59427aa6 --- /dev/null +++ b/pythonFiles/tests/test_dynamic_cursor.py @@ -0,0 +1,203 @@ +import importlib +import textwrap + +import normalizeSelection + + +def test_dictionary_mouse_mover(): + """ + Having the mouse cursor on second line, + 'my_dict = {' + and pressing shift+enter should bring the + mouse cursor to line 6, on and to be able to run + 'print('only send the dictionary')' + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + not_dictionary = 'hi' + my_dict = { + "key1": "value1", + "key2": "value2" + } + print('only send the dictionary') + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, False) + + assert result["which_line_next"] == 6 + + +def test_beginning_func(): + """ + Pressing shift+enter on the very first line, + of function definition, such as 'my_func():' + It should properly skip the comment and assert the + next executable line to be executed is line 5 at + 'my_dict = {' + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def my_func(): + print("line 2") + print("line 3") + # Skip line 4 because it is a comment + my_dict = { + "key1": "value1", + "key2": "value2" + } + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["which_line_next"] == 5 + + +def test_cursor_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + lucid_dream = ["Corgi", "Husky", "Pomsky"] + for dogs in lucid_dream: # initial starting position + print(dogs) + print("I wish I had a dog!") + + print("This should be the next block that should be ran") + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, False) + + assert result["which_line_next"] == 6 + + +def test_inside_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for food in lucid_dream: + print("We are starting") # initial starting position + print("Next cursor should be here!") + + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, False) + + assert result["which_line_next"] == 3 + + +def test_skip_sameline_statements(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("Audi");print("BMW");print("Mercedes") + print("Next line to be run is here!") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["which_line_next"] == 2 + + +def test_skip_multi_comp_lambda(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + + my_lambda = lambda x: ( + x + 1 + ) + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + # Shift enter from the very first ( should make + # next executable statement as the lambda expression + assert result["which_line_next"] == 7 + + +def test_move_whole_class(): + """ + Shift+enter on a class definition + should move the cursor after running whole class. + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + print("We should be here after running whole class") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["which_line_next"] == 7 + + +def test_def_to_def(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + + # Skip here + def next_func(): + print("Not here but above") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["which_line_next"] == 9 + + +def test_try_catch_move(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + print("Should be here afterwards") + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["which_line_next"] == 6 + + +def test_skip_nested(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + + print("Cursor should be here after running line 1") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["which_line_next"] == 8 diff --git a/pythonFiles/tests/test_normalize_selection.py b/pythonFiles/tests/test_normalize_selection.py index 138c5ad2f522..5f4d6d7d4a1f 100644 --- a/pythonFiles/tests/test_normalize_selection.py +++ b/pythonFiles/tests/test_normalize_selection.py @@ -1,8 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + +import importlib import textwrap +# __file__ = "/Users/anthonykim/Desktop/vscode-python/pythonFiles/normalizeSelection.py" +# sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__)))) import normalizeSelection @@ -215,3 +219,52 @@ def show_something(): ) result = normalizeSelection.normalize_lines(src) assert result == expected + + def test_fstring(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + name = "Ahri" + age = 10 + + print(f'My name is {name}') + """ + ) + + expected = textwrap.dedent( + """\ + name = "Ahri" + age = 10 + print(f'My name is {name}') + """ + ) + result = normalizeSelection.normalize_lines(src) + + assert result == expected + + def test_list_comp(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + names = ['Ahri', 'Bobby', 'Charlie'] + breed = ['Pomeranian', 'Welsh Corgi', 'Siberian Husky'] + dogs = [(name, breed) for name, breed in zip(names, breed)] + + print(dogs) + my_family_dog = 'Corgi' + """ + ) + + expected = textwrap.dedent( + """\ + names = ['Ahri', 'Bobby', 'Charlie'] + breed = ['Pomeranian', 'Welsh Corgi', 'Siberian Husky'] + dogs = [(name, breed) for name, breed in zip(names, breed)] + print(dogs) + my_family_dog = 'Corgi' + """ + ) + + result = normalizeSelection.normalize_lines(src) + + assert result == expected diff --git a/pythonFiles/tests/test_smart_selection.py b/pythonFiles/tests/test_smart_selection.py new file mode 100644 index 000000000000..b86e6f9dc82e --- /dev/null +++ b/pythonFiles/tests/test_smart_selection.py @@ -0,0 +1,388 @@ +import importlib +import textwrap + +import normalizeSelection + + +def test_part_dictionary(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + not_dictionary = 'hi' + my_dict = { + "key1": "value1", + "key2": "value2" + } + print('only send the dictionary') + """ + ) + + expected = textwrap.dedent( + """\ + my_dict = { + "key1": "value1", + "key2": "value2" + } + """ + ) + + result = normalizeSelection.traverse_file(src, 3, 3, False) + assert result["normalized_smart_result"] == expected + + +def test_nested_loop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + """ + ) + expected = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["normalized_smart_result"] == expected + + +def test_smart_shift_enter_multiple_statements(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + import textwrap + import ast + + print("Porsche") + print("Genesis") + + + print("Audi");print("BMW");print("Mercedes") + + print("dont print me") + + """ + ) + # Expected to printing statement line by line, + # for when multiple print statements are ran + # from the same line. + expected = textwrap.dedent( + """\ + print("Audi") + print("BMW") + print("Mercedes") + """ + ) + result = normalizeSelection.traverse_file(src, 8, 8, False) + assert result["normalized_smart_result"] == expected + + +def test_two_layer_dictionary(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("dont print me") + + two_layered_dictionary = { + 'inner_dict_one': { + 'Audi': 'Germany', + 'BMW': 'Germnay', + 'Genesis': 'Korea', + }, + 'inner_dict_two': { + 'Mercedes': 'Germany', + 'Porsche': 'Germany', + 'Lamborghini': 'Italy', + 'Ferrari': 'Italy', + 'Maserati': 'Italy' + } + } + """ + ) + expected = textwrap.dedent( + """\ + two_layered_dictionary = { + 'inner_dict_one': { + 'Audi': 'Germany', + 'BMW': 'Germnay', + 'Genesis': 'Korea', + }, + 'inner_dict_two': { + 'Mercedes': 'Germany', + 'Porsche': 'Germany', + 'Lamborghini': 'Italy', + 'Ferrari': 'Italy', + 'Maserati': 'Italy' + } + } + """ + ) + result = normalizeSelection.traverse_file(src, 6, 7, False) + + assert result["normalized_smart_result"] == expected + + +def test_run_whole_func(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("Decide which dog you will choose") + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + """ + ) + + expected = textwrap.dedent( + """\ + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + + """ + ) + result = normalizeSelection.traverse_file(src, 2, 2, False) + + assert result["normalized_smart_result"] == expected + + +def test_small_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + """ + ) + expected = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + + """ + ) + + # Cover the whole for loop block with multiple inner statements + # Make sure to contain all of the print statements included. + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["normalized_smart_result"] == expected + + +def inner_for_loop_component(): + """ + Pressing shift+enter inside a for loop, + specifically on a viable expression + by itself, such as print(i) + should only return that exact expression + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + """ + ) + result = normalizeSelection.traverse_file(src, 2, 2, False) + expected = textwrap.dedent( + """\ + print(i) + """ + ) + + assert result["normalized_smart_result"] == expected + + +def test_dict_comprehension(): + """ + Having the mouse cursor on the first line, + and pressing shift+enter should return the + whole dictionary comp, respecting user's code style. + """ + + importlib.reload + src = textwrap.dedent( + """\ + my_dict_comp = {temp_mover: + temp_mover for temp_mover in range(1, 7)} + """ + ) + + expected = textwrap.dedent( + """\ + my_dict_comp = {temp_mover: + temp_mover for temp_mover in range(1, 7)} + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["normalized_smart_result"] == expected + + +def test_send_whole_generator(): + """ + Pressing shift+enter on the first line, which is the '(' + should be returning the whole generator expression instead of just the '(' + """ + + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + """ + ) + + expected = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + + assert result["normalized_smart_result"] == expected + + +def test_multiline_lambda(): + """ + Shift+enter on part of the lambda expression + should return the whole lambda expression, + regardless of whether all the component of + lambda expression is on the same or not. + """ + + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + my_lambda = lambda x: ( + x + 1 + ) + """ + ) + expected = textwrap.dedent( + """\ + my_lambda = lambda x: ( + x + 1 + ) + + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["normalized_smart_result"] == expected + + +def test_send_whole_class(): + """ + Shift+enter on a class definition + should send the whole class definition + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + print("We should be here after running whole class") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + expected = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + + """ + ) + assert result["normalized_smart_result"] == expected + + +def test_send_whole_if_statement(): + """ + Shift+enter on an if statement + should send the whole if statement + including statements inside and else. + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + if True: + print('send this') + else: + print('also send this') + + print('cursor here afterwards') + """ + ) + expected = textwrap.dedent( + """\ + if True: + print('send this') + else: + print('also send this') + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["normalized_smart_result"] == expected + + +def test_send_try(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + print("Not running this") + """ + ) + expected = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, False) + assert result["normalized_smart_result"] == expected diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 763fa4dde79d..ba29f0dcd956 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -99,4 +99,12 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [Commands.Tests_Configure]: [undefined, undefined | CommandSource, undefined | Uri]; [Commands.LaunchTensorBoard]: [TensorBoardEntrypoint, TensorBoardEntrypointTrigger]; ['workbench.view.testing.focus']: []; + ['cursorMove']: [ + { + to: string; + by: string; + value: number; + }, + ]; + ['cursorEnd']: []; } diff --git a/src/client/common/experiments/groups.ts b/src/client/common/experiments/groups.ts index 1ee06469095c..b7a598e0a08a 100644 --- a/src/client/common/experiments/groups.ts +++ b/src/client/common/experiments/groups.ts @@ -18,3 +18,7 @@ export enum ShowFormatterExtensionPrompt { export enum EnableTestAdapterRewrite { experiment = 'pythonTestAdapter', } +// Experiment to enable smart shift+enter, advance cursor. +export enum EnableREPLSmartSend { + experiment = 'pythonREPLSmartSend', +} diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 7883e0cd7555..95496c828018 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -821,11 +821,11 @@ export interface IEventNamePropertyMapping { */ [EventName.EXECUTION_CODE]: { /** - * Whether the user executed a file in the terminal or just the selected text. + * Whether the user executed a file in the terminal or just the selected text or line by shift+enter. * * @type {('file' | 'selection')} */ - scope: 'file' | 'selection'; + scope: 'file' | 'selection' | 'line'; /** * How was the code executed (through the command or by clicking the `Run File` icon). * diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index 2dd619a1816a..ed31e194b2d2 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -159,7 +159,11 @@ export class CodeExecutionManager implements ICodeExecutionManager { } const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor!); - const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!); + let wholeFileContent = ''; + if (activeEditor && activeEditor.document) { + wholeFileContent = activeEditor.document.getText(); + } + const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!, wholeFileContent); if (!normalizedCode || normalizedCode.trim().length === 0) { return; } diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index 0d5694b4a28d..c560de9c17b7 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -5,7 +5,12 @@ import '../../common/extensions'; import { inject, injectable } from 'inversify'; import { l10n, Position, Range, TextEditor, Uri } from 'vscode'; -import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../common/application/types'; +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService, +} from '../../common/application/types'; import { PYTHON_LANGUAGE } from '../../common/constants'; import * as internalScripts from '../../common/process/internal/scripts'; import { IProcessServiceFactory } from '../../common/process/types'; @@ -14,7 +19,10 @@ import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { ICodeExecutionHelper } from '../types'; import { traceError } from '../../logging'; -import { Resource } from '../../common/types'; +import { IConfigurationService, IExperimentService, Resource } from '../../common/types'; +import { EnableREPLSmartSend } from '../../common/experiments/groups'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; @injectable() export class CodeExecutionHelper implements ICodeExecutionHelper { @@ -26,14 +34,22 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { private readonly interpreterService: IInterpreterService; + private readonly commandManager: ICommandManager; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error TS6133: 'configSettings' is declared but its value is never read. + private readonly configSettings: IConfigurationService; + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { this.documentManager = serviceContainer.get(IDocumentManager); this.applicationShell = serviceContainer.get(IApplicationShell); this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); this.interpreterService = serviceContainer.get(IInterpreterService); + this.configSettings = serviceContainer.get(IConfigurationService); + this.commandManager = serviceContainer.get(ICommandManager); } - public async normalizeLines(code: string, resource?: Uri): Promise { + public async normalizeLines(code: string, wholeFileContent?: string, resource?: Uri): Promise { try { if (code.trim().length === 0) { return ''; @@ -42,6 +58,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { // So just remove cr from the input. code = code.replace(new RegExp('\\r', 'g'), ''); + const activeEditor = this.documentManager.activeTextEditor; const interpreter = await this.interpreterService.getActiveInterpreter(resource); const processService = await this.processServiceFactory.create(resource); @@ -63,10 +80,24 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { normalizeOutput.resolve(normalized); }, }); - + // If there is no explicit selection, we are exeucting 'line' or 'block'. + if (activeEditor?.selection?.isEmpty) { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'line' }); + } // The normalization script expects a serialized JSON object, with the selection under the "code" key. // We're using a JSON object so that we don't have to worry about encoding, or escaping non-ASCII characters. - const input = JSON.stringify({ code }); + const startLineVal = activeEditor?.selection?.start.line ?? 0; + const endLineVal = activeEditor?.selection?.end.line ?? 0; + const emptyHighlightVal = activeEditor?.selection?.isEmpty ?? true; + const smartSendExperimentEnabledVal = pythonSmartSendEnabled(this.serviceContainer); + const input = JSON.stringify({ + code, + wholeFileContent, + startLine: startLineVal, + endLine: endLineVal, + emptyHighlight: emptyHighlightVal, + smartSendExperimentEnabled: smartSendExperimentEnabledVal, + }); observable.proc?.stdin?.write(input); observable.proc?.stdin?.end(); @@ -74,6 +105,11 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const result = await normalizeOutput.promise; const object = JSON.parse(result); + if (activeEditor?.selection) { + const lineOffset = object.nextBlockLineno - activeEditor!.selection.start.line - 1; + await this.moveToNextBlock(lineOffset, activeEditor); + } + return parse(object.normalized); } catch (ex) { traceError(ex, 'Python: Failed to normalize code for execution in terminal'); @@ -81,6 +117,30 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { } } + /** + * Depending on whether or not user is in experiment for smart send, + * dynamically move the cursor to the next block of code. + * The cursor movement is not moved by one everytime, + * since with the smart selection, the next executable code block + * can be multiple lines away. + * Intended to provide smooth shift+enter user experience + * bringing user's cursor to the next executable block of code when used with smart selection. + */ + // eslint-disable-next-line class-methods-use-this + private async moveToNextBlock(lineOffset: number, activeEditor?: TextEditor): Promise { + if (pythonSmartSendEnabled(this.serviceContainer)) { + if (activeEditor?.selection?.isEmpty) { + await this.commandManager.executeCommand('cursorMove', { + to: 'down', + by: 'line', + value: Number(lineOffset), + }); + await this.commandManager.executeCommand('cursorEnd'); + } + } + return Promise.resolve(); + } + public async getFileToExecute(): Promise { const activeEditor = this.documentManager.activeTextEditor; if (!activeEditor) { @@ -110,6 +170,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const { selection } = textEditor; let code: string; + if (selection.isEmpty) { code = textEditor.document.lineAt(selection.start.line).text; } else if (selection.isSingleLine) { @@ -117,6 +178,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { } else { code = getMultiLineSelectionText(textEditor); } + return code; } @@ -235,3 +297,9 @@ function getMultiLineSelectionText(textEditor: TextEditor): string { // ↑<---------------- To here return selectionText; } + +function pythonSmartSendEnabled(serviceContainer: IServiceContainer): boolean { + const experiment = serviceContainer.get(IExperimentService); + + return experiment ? experiment.inExperimentSync(EnableREPLSmartSend.experiment) : false; +} diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index 47ac16d9e08b..48e39d4e1c81 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -15,7 +15,7 @@ export interface ICodeExecutionService { export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper'); export interface ICodeExecutionHelper { - normalizeLines(code: string): Promise; + normalizeLines(code: string, wholeFileContent?: string): Promise; getFileToExecute(): Promise; saveFileIfDirty(file: Uri): Promise; getSelectedTextToExecute(textEditor: TextEditor): Promise; diff --git a/src/test/pythonFiles/terminalExec/sample_smart_selection.py b/src/test/pythonFiles/terminalExec/sample_smart_selection.py new file mode 100644 index 000000000000..3933f06b5d65 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample_smart_selection.py @@ -0,0 +1,21 @@ +my_dict = { + "key1": "value1", + "key2": "value2" +} +#Sample + +print("Audi");print("BMW");print("Mercedes") + +# print("dont print me") + +def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + +# Skip me to prove that you did a good job +def next_func(): + print("You") + diff --git a/src/test/smoke/smartSend.smoke.test.ts b/src/test/smoke/smartSend.smoke.test.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/terminals/codeExecution/smartSend.test.ts b/src/test/terminals/codeExecution/smartSend.test.ts new file mode 100644 index 000000000000..8d70ab6e01e0 --- /dev/null +++ b/src/test/terminals/codeExecution/smartSend.test.ts @@ -0,0 +1,229 @@ +import * as TypeMoq from 'typemoq'; +import * as path from 'path'; +import { TextEditor, Selection, Position, TextDocument } from 'vscode'; +import * as fs from 'fs-extra'; +import { SemVer } from 'semver'; +import { assert, expect } from 'chai'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../../../client/common/application/types'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IConfigurationService, IExperimentService } from '../../../client/common/types'; +import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { ICodeExecutionHelper } from '../../../client/terminals/types'; +import { EnableREPLSmartSend } from '../../../client/common/experiments/groups'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { PYTHON_PATH } from '../../common'; +import { Architecture } from '../../../client/common/utils/platform'; +import { ProcessService } from '../../../client/common/process/proc'; + +const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'terminalExec'); + +suite('REPL - Smart Send', () => { + let documentManager: TypeMoq.IMock; + let applicationShell: TypeMoq.IMock; + + let interpreterService: TypeMoq.IMock; + let commandManager: TypeMoq.IMock; + + let processServiceFactory: TypeMoq.IMock; + let configurationService: TypeMoq.IMock; + + let serviceContainer: TypeMoq.IMock; + let codeExecutionHelper: ICodeExecutionHelper; + let experimentService: TypeMoq.IMock; + + let processService: TypeMoq.IMock; + + let document: TypeMoq.IMock; + const workingPython: PythonEnvironment = { + path: PYTHON_PATH, + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + displayName: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, + }; + + // suite set up only run once for each suite. Very start + // set up --- before each test + // tests -- actual tests + // tear down -- run after each test + // suite tear down only run once at the very end. + + // all object that is common to every test. What each test needs + setup(() => { + documentManager = TypeMoq.Mock.ofType(); + applicationShell = TypeMoq.Mock.ofType(); + processServiceFactory = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + commandManager = TypeMoq.Mock.ofType(); + configurationService = TypeMoq.Mock.ofType(); + serviceContainer = TypeMoq.Mock.ofType(); + experimentService = TypeMoq.Mock.ofType(); + processService = TypeMoq.Mock.ofType(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + processService.setup((x: any) => x.then).returns(() => undefined); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager))) + .returns(() => documentManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) + .returns(() => applicationShell.object); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory))) + .returns(() => processServiceFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => commandManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configurationService.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IExperimentService))) + .returns(() => experimentService.object); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(workingPython)); + + codeExecutionHelper = new CodeExecutionHelper(serviceContainer.object); + document = TypeMoq.Mock.ofType(); + }); + + test('Cursor is not moved when explicit selection is present', async () => { + experimentService + .setup((exp) => exp.inExperimentSync(TypeMoq.It.isValue(EnableREPLSmartSend.experiment))) + .returns(() => true); + + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => false); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + await codeExecutionHelper.normalizeLines('my_dict = {', wholeFileContent); + + commandManager + .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny())) + .callback((_, arg2) => { + assert.deepEqual(arg2, { + to: 'down', + by: 'line', + value: 3, + }); + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.never()); + + commandManager + .setup((c) => c.executeCommand('cursorEnd')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + commandManager.verifyAll(); + }); + + test('Smart send should perform smart selection and move cursor', async () => { + experimentService + .setup((exp) => exp.inExperimentSync(TypeMoq.It.isValue(EnableREPLSmartSend.experiment))) + .returns(() => true); + + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => true); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + const actualSmartOutput = await codeExecutionHelper.normalizeLines('my_dict = {', wholeFileContent); + + // my_dict = { <----- smart shift+enter here + // "key1": "value1", + // "key2": "value2" + // } <---- cursor should be here afterwards, hence offset 3 + commandManager + .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny())) + .callback((_, arg2) => { + assert.deepEqual(arg2, { + to: 'down', + by: 'line', + value: 3, + }); + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.once()); + + commandManager + .setup((c) => c.executeCommand('cursorEnd')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const expectedSmartOutput = 'my_dict = {\n "key1": "value1",\n "key2": "value2"\n}\n'; + expect(actualSmartOutput).to.be.equal(expectedSmartOutput); + commandManager.verifyAll(); + }); + + // Do not perform smart selection when there is explicit selection + test('Smart send should not perform smart selection when there is explicit selection', async () => { + experimentService + .setup((exp) => exp.inExperimentSync(TypeMoq.It.isValue(EnableREPLSmartSend.experiment))) + .returns(() => true); + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => false); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + const actualNonSmartResult = await codeExecutionHelper.normalizeLines('my_dict = {', wholeFileContent); + const expectedNonSmartResult = 'my_dict = {\n\n'; // Standard for previous normalization logic + expect(actualNonSmartResult).to.be.equal(expectedNonSmartResult); + }); +}); diff --git a/src/testMultiRootWkspc/smokeTests/create_delete_file.py b/src/testMultiRootWkspc/smokeTests/create_delete_file.py new file mode 100644 index 000000000000..399bc4863c15 --- /dev/null +++ b/src/testMultiRootWkspc/smokeTests/create_delete_file.py @@ -0,0 +1,5 @@ +with open('smart_send_smoke.txt', 'w') as f: + f.write('This is for smart send smoke test') +import os + +os.remove('smart_send_smoke.txt') From bc0c7144d586d5a7514921ddfc8cd495f1838ba1 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 11 Oct 2023 10:18:16 -0700 Subject: [PATCH 0238/1136] add clickable show logs on discovery error (#22199) fixes https://github.com/microsoft/vscode-python/issues/22175 --- .../testController/common/resultResolver.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 79cee6452a8c..cf757d77243d 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -1,7 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, TestController, TestItem, Uri, TestMessage, Location, TestRun } from 'vscode'; +import { + CancellationToken, + TestController, + TestItem, + Uri, + TestMessage, + Location, + TestRun, + MarkdownString, +} from 'vscode'; import * as util from 'util'; import { DiscoveredTestPayload, EOTTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; import { TestProvider } from '../../types'; @@ -78,7 +87,11 @@ export class PythonResultResolver implements ITestResultResolver { errorNode = createErrorTestItem(this.testController, options); this.testController.items.add(errorNode); } - errorNode.error = message; + const errorNodeLabel: MarkdownString = new MarkdownString( + `[Show output](command:python.viewOutput) to view error logs`, + ); + errorNodeLabel.isTrusted = true; + errorNode.error = errorNodeLabel; } else { // remove error node only if no errors exist. this.testController.items.delete(`DiscoveryError:${workspacePath}`); From 75e707be42bf67aac316c900f7d095c1e21bae28 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Wed, 11 Oct 2023 11:11:57 -0700 Subject: [PATCH 0239/1136] Update LSP to latest version to support completion itemDefaults (#22200) Dirk added this feature here: https://github.com/microsoft/vscode-languageserver-node/commit/0b7acc15abd7132c9154d94140f478ccf5ba5769 We want to use this in Pylance in order to speedup completions. For the degenerate case, this can speedup completion results by 30%. See https://github.com/microsoft/pyrx/issues/4113 and https://github.com/microsoft/pylance-release/issues/4919 --- package-lock.json | 167 ++++++++++++++++++++++++++++++---------------- package.json | 8 +-- 2 files changed, 113 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5d1ee32bc08b..c4e177468706 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,10 +37,10 @@ "untildify": "^4.0.0", "vscode-debugadapter": "^1.28.0", "vscode-debugprotocol": "^1.28.0", - "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageclient": "^8.1.0", - "vscode-languageserver": "^8.1.0", - "vscode-languageserver-protocol": "^3.17.3", + "vscode-jsonrpc": "^8.2.0", + "vscode-languageclient": "^9.0.1", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", "vscode-tas-client": "^0.1.63", "which": "^2.0.2", "winreg": "^1.2.4", @@ -1843,6 +1843,41 @@ "vscode": "^1.67.0-insider" } }, + "node_modules/@vscode/jupyter-lsp-middleware/node_modules/vscode-jsonrpc": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", + "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vscode/jupyter-lsp-middleware/node_modules/vscode-languageclient": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", + "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.3" + }, + "engines": { + "vscode": "^1.67.0" + } + }, + "node_modules/@vscode/jupyter-lsp-middleware/node_modules/vscode-languageserver-protocol": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", + "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", + "dependencies": { + "vscode-jsonrpc": "8.1.0", + "vscode-languageserver-types": "3.17.3" + } + }, + "node_modules/@vscode/jupyter-lsp-middleware/node_modules/vscode-languageserver-types": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", + "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" + }, "node_modules/@vscode/lsp-notebook-concat": { "version": "0.1.16", "resolved": "https://registry.npmjs.org/@vscode/lsp-notebook-concat/-/lsp-notebook-concat-0.1.16.tgz", @@ -14554,58 +14589,50 @@ "deprecated": "This package has been renamed to @vscode/debugprotocol, please update to the new name" }, "node_modules/vscode-jsonrpc": { - "version": "8.0.2-next.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2-next.1.tgz", - "integrity": "sha512-sbbvGSWja7NVBLHPGawtgezc8DHYJaP4qfr/AaJiyDapWcSFtHyPtm18+LnYMLTmB7bhOUW/lf5PeeuLpP6bKA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageclient": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", - "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", "dependencies": { "minimatch": "^5.1.0", "semver": "^7.3.7", - "vscode-languageserver-protocol": "3.17.3" + "vscode-languageserver-protocol": "3.17.5" }, "engines": { - "vscode": "^1.67.0" + "vscode": "^1.82.0" } }, "node_modules/vscode-languageserver": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz", - "integrity": "sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", "dependencies": { - "vscode-languageserver-protocol": "3.17.3" + "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", - "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "dependencies": { - "vscode-jsonrpc": "8.1.0", - "vscode-languageserver-types": "3.17.3" - } - }, - "node_modules/vscode-languageserver-protocol/node_modules/vscode-jsonrpc": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", - "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==", - "engines": { - "node": ">=14.0.0" + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" } }, "node_modules/vscode-languageserver-types": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", - "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, "node_modules/vscode-tas-client": { "version": "0.1.63", @@ -16750,6 +16777,37 @@ "vscode-languageclient": "^8.0.2-next.4", "vscode-languageserver-protocol": "^3.17.2-next.5", "vscode-uri": "^3.0.2" + }, + "dependencies": { + "vscode-jsonrpc": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", + "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==" + }, + "vscode-languageclient": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", + "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", + "requires": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.3" + } + }, + "vscode-languageserver-protocol": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", + "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", + "requires": { + "vscode-jsonrpc": "8.1.0", + "vscode-languageserver-types": "3.17.3" + } + }, + "vscode-languageserver-types": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", + "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" + } } }, "@vscode/lsp-notebook-concat": { @@ -26716,48 +26774,41 @@ "integrity": "sha512-+OMm11R1bGYbpIJ5eQIkwoDGFF4GvBz3Ztl6/VM+/RNNb2Gjk2c0Ku+oMmfhlTmTlPCpgHBsH4JqVCbUYhu5bA==" }, "vscode-jsonrpc": { - "version": "8.0.2-next.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2-next.1.tgz", - "integrity": "sha512-sbbvGSWja7NVBLHPGawtgezc8DHYJaP4qfr/AaJiyDapWcSFtHyPtm18+LnYMLTmB7bhOUW/lf5PeeuLpP6bKA==" + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==" }, "vscode-languageclient": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", - "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", "requires": { "minimatch": "^5.1.0", "semver": "^7.3.7", - "vscode-languageserver-protocol": "3.17.3" + "vscode-languageserver-protocol": "3.17.5" } }, "vscode-languageserver": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.1.0.tgz", - "integrity": "sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", "requires": { - "vscode-languageserver-protocol": "3.17.3" + "vscode-languageserver-protocol": "3.17.5" } }, "vscode-languageserver-protocol": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", - "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "requires": { - "vscode-jsonrpc": "8.1.0", - "vscode-languageserver-types": "3.17.3" - }, - "dependencies": { - "vscode-jsonrpc": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", - "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==" - } + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" } }, "vscode-languageserver-types": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", - "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, "vscode-tas-client": { "version": "0.1.63", diff --git a/package.json b/package.json index 0f93441c6113..490e615f26f2 100644 --- a/package.json +++ b/package.json @@ -2081,10 +2081,10 @@ "untildify": "^4.0.0", "vscode-debugadapter": "^1.28.0", "vscode-debugprotocol": "^1.28.0", - "vscode-jsonrpc": "8.0.2-next.1", - "vscode-languageclient": "^8.1.0", - "vscode-languageserver": "^8.1.0", - "vscode-languageserver-protocol": "^3.17.3", + "vscode-jsonrpc": "^8.2.0", + "vscode-languageclient": "^9.0.1", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", "vscode-tas-client": "^0.1.63", "which": "^2.0.2", "winreg": "^1.2.4", From 055a352285db83158be4374a2e57bdc48b28fda8 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 11 Oct 2023 12:05:39 -0700 Subject: [PATCH 0240/1136] Remove formatting support (#22196) --- .../common/installer/moduleInstaller.ts | 6 - src/client/common/installer/productNames.ts | 3 - src/client/common/installer/productPath.ts | 14 - src/client/common/installer/productService.ts | 3 - .../common/installer/serviceRegistry.ts | 6 - src/client/common/types.ts | 5 - src/client/extensionActivation.ts | 23 +- src/client/formatters/autoPep8Formatter.ts | 43 --- src/client/formatters/baseFormatter.ts | 149 -------- src/client/formatters/blackFormatter.ts | 55 --- src/client/formatters/dummyFormatter.ts | 19 - src/client/formatters/helper.ts | 53 --- src/client/formatters/serviceRegistry.ts | 10 - src/client/formatters/types.ts | 20 -- src/client/formatters/yapfFormatter.ts | 38 -- src/client/logging/settingLogs.ts | 42 +++ src/client/providers/formatProvider.ts | 135 ------- .../prompts/installFormatterPrompt.ts | 145 -------- src/client/providers/prompts/promptUtils.ts | 38 -- src/client/providers/prompts/types.ts | 12 - src/client/providers/serviceRegistry.ts | 3 - src/test/common/installer.test.ts | 12 +- .../installer/channelManager.unit.test.ts | 6 +- .../common/installer/productPath.unit.test.ts | 45 +-- .../installer/serviceRegistry.unit.test.ts | 8 - src/test/common/moduleInstaller.test.ts | 1 - src/test/format/extension.format.test.ts | 205 ----------- src/test/format/format.helper.test.ts | 117 ------ src/test/format/formatter.unit.test.ts | 171 --------- src/test/linters/lint.multiroot.test.ts | 11 +- src/test/linters/lint.test.ts | 11 +- .../installFormatterPrompt.unit.test.ts | 335 ------------------ src/test/serviceRegistry.ts | 5 - 33 files changed, 53 insertions(+), 1696 deletions(-) delete mode 100644 src/client/formatters/autoPep8Formatter.ts delete mode 100644 src/client/formatters/baseFormatter.ts delete mode 100644 src/client/formatters/blackFormatter.ts delete mode 100644 src/client/formatters/dummyFormatter.ts delete mode 100644 src/client/formatters/helper.ts delete mode 100644 src/client/formatters/serviceRegistry.ts delete mode 100644 src/client/formatters/types.ts delete mode 100644 src/client/formatters/yapfFormatter.ts create mode 100644 src/client/logging/settingLogs.ts delete mode 100644 src/client/providers/formatProvider.ts delete mode 100644 src/client/providers/prompts/installFormatterPrompt.ts delete mode 100644 src/client/providers/prompts/promptUtils.ts delete mode 100644 src/client/providers/prompts/types.ts delete mode 100644 src/test/format/extension.format.test.ts delete mode 100644 src/test/format/format.helper.test.ts delete mode 100644 src/test/format/formatter.unit.test.ts delete mode 100644 src/test/providers/prompt/installFormatterPrompt.unit.test.ts diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index f70dd937aba9..5a4f245900ea 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -248,16 +248,10 @@ export function translateProductToModule(product: Product): string { return 'pylint'; case Product.pytest: return 'pytest'; - case Product.autopep8: - return 'autopep8'; - case Product.black: - return 'black'; case Product.pycodestyle: return 'pycodestyle'; case Product.pydocstyle: return 'pydocstyle'; - case Product.yapf: - return 'yapf'; case Product.flake8: return 'flake8'; case Product.unittest: diff --git a/src/client/common/installer/productNames.ts b/src/client/common/installer/productNames.ts index 378fd5a38dba..9b917d2f1d76 100644 --- a/src/client/common/installer/productNames.ts +++ b/src/client/common/installer/productNames.ts @@ -4,9 +4,7 @@ import { Product } from '../types'; export const ProductNames = new Map(); -ProductNames.set(Product.autopep8, 'autopep8'); ProductNames.set(Product.bandit, 'bandit'); -ProductNames.set(Product.black, 'black'); ProductNames.set(Product.flake8, 'flake8'); ProductNames.set(Product.mypy, 'mypy'); ProductNames.set(Product.pycodestyle, 'pycodestyle'); @@ -15,7 +13,6 @@ ProductNames.set(Product.prospector, 'prospector'); ProductNames.set(Product.pydocstyle, 'pydocstyle'); ProductNames.set(Product.pylint, 'pylint'); ProductNames.set(Product.pytest, 'pytest'); -ProductNames.set(Product.yapf, 'yapf'); ProductNames.set(Product.tensorboard, 'tensorboard'); ProductNames.set(Product.torchProfilerInstallName, 'torch-tb-profiler'); ProductNames.set(Product.torchProfilerImportName, 'torch_tb_profiler'); diff --git a/src/client/common/installer/productPath.ts b/src/client/common/installer/productPath.ts index 5c36a6bbd3bd..3b3f1d7c1794 100644 --- a/src/client/common/installer/productPath.ts +++ b/src/client/common/installer/productPath.ts @@ -6,7 +6,6 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; -import { IFormatterHelper } from '../../formatters/types'; import { IServiceContainer } from '../../ioc/types'; import { ILinterManager } from '../../linters/types'; import { ITestingService } from '../../testing/types'; @@ -37,19 +36,6 @@ export abstract class BaseProductPathsService implements IProductPathService { } } -@injectable() -export class FormatterProductPathService extends BaseProductPathsService { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer); - } - public getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const settings = this.configService.getSettings(resource); - const formatHelper = this.serviceContainer.get(IFormatterHelper); - const settingsPropNames = formatHelper.getSettingsPropertyNames(product); - return settings.formatting[settingsPropNames.pathName] as string; - } -} - @injectable() export class LinterProductPathService extends BaseProductPathsService { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { diff --git a/src/client/common/installer/productService.ts b/src/client/common/installer/productService.ts index 26a01e37c3ba..af2192755fe8 100644 --- a/src/client/common/installer/productService.ts +++ b/src/client/common/installer/productService.ts @@ -22,9 +22,6 @@ export class ProductService implements IProductService { this.ProductTypes.set(Product.pylint, ProductType.Linter); this.ProductTypes.set(Product.pytest, ProductType.TestFramework); this.ProductTypes.set(Product.unittest, ProductType.TestFramework); - this.ProductTypes.set(Product.autopep8, ProductType.Formatter); - this.ProductTypes.set(Product.black, ProductType.Formatter); - this.ProductTypes.set(Product.yapf, ProductType.Formatter); this.ProductTypes.set(Product.tensorboard, ProductType.DataScience); this.ProductTypes.set(Product.torchProfilerInstallName, ProductType.DataScience); this.ProductTypes.set(Product.torchProfilerImportName, ProductType.DataScience); diff --git a/src/client/common/installer/serviceRegistry.ts b/src/client/common/installer/serviceRegistry.ts index c262c7571711..c4e7c1a089c6 100644 --- a/src/client/common/installer/serviceRegistry.ts +++ b/src/client/common/installer/serviceRegistry.ts @@ -11,7 +11,6 @@ import { PipInstaller } from './pipInstaller'; import { PoetryInstaller } from './poetryInstaller'; import { DataScienceProductPathService, - FormatterProductPathService, LinterProductPathService, TestFrameworkProductPathService, } from './productPath'; @@ -25,11 +24,6 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IModuleInstaller, PoetryInstaller); serviceManager.addSingleton(IInstallationChannelManager, InstallationChannelManager); serviceManager.addSingleton(IProductService, ProductService); - serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); serviceManager.addSingleton( IProductPathService, diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 07f1fea6b86b..8b90443703c6 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -87,7 +87,6 @@ export enum ProductInstallStatus { export enum ProductType { Linter = 'Linter', - Formatter = 'Formatter', TestFramework = 'TestFramework', RefactoringLibrary = 'RefactoringLibrary', DataScience = 'DataScience', @@ -102,11 +101,8 @@ export enum Product { pylama = 6, prospector = 7, pydocstyle = 8, - yapf = 9, - autopep8 = 10, mypy = 11, unittest = 12, - black = 16, bandit = 17, tensorboard = 24, torchProfilerInstallName = 25, @@ -185,7 +181,6 @@ export interface IPythonSettings { readonly poetryPath: string; readonly devOptions: string[]; readonly linting: ILintingSettings; - readonly formatting: IFormattingSettings; readonly testing: ITestingSettings; readonly autoComplete: IAutoCompleteSettings; readonly terminal: ITerminalSettings; diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 807698f3ec29..0d3b04d9bb8c 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -10,7 +10,7 @@ import { IExtensionActivationManager } from './activation/types'; import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; import { IApplicationDiagnostics } from './application/types'; import { IApplicationEnvironment, ICommandManager, IWorkspaceService } from './common/application/types'; -import { Commands, PYTHON, PYTHON_LANGUAGE, UseProposedApi } from './common/constants'; +import { Commands, PYTHON_LANGUAGE, UseProposedApi } from './common/constants'; import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; import { IFileSystem } from './common/platform/types'; import { @@ -25,11 +25,9 @@ import { noop } from './common/utils/misc'; import { DebuggerTypeName } from './debugger/constants'; import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; import { IDebugConfigurationService, IDynamicDebugConfigurationService } from './debugger/extension/types'; -import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; -import { PythonFormattingEditProvider } from './providers/formatProvider'; import { ReplProvider } from './providers/replProvider'; import { registerTypes as providersRegisterTypes } from './providers/serviceRegistry'; import { TerminalProvider } from './providers/terminalProvider'; @@ -51,10 +49,10 @@ import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; import { WorkspaceService } from './common/application/workspace'; import { DynamicPythonDebugConfigurationService } from './debugger/extension/configuration/dynamicdebugConfigurationService'; import { IInterpreterQuickPick } from './interpreter/configuration/types'; -import { registerInstallFormatterPrompt } from './providers/prompts/installFormatterPrompt'; import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger'; import { initializePersistentStateForTriggers } from './common/persistentState'; +import { logAndNotifyOnFormatterSetting } from './logging/settingLogs'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -110,7 +108,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components): // See https://github.com/microsoft/vscode-python/issues/10454. async function activateLegacy(ext: ExtensionState): Promise { - const { context, legacyIOC } = ext; + const { legacyIOC } = ext; const { serviceManager, serviceContainer } = legacyIOC; // register "services" @@ -125,7 +123,6 @@ async function activateLegacy(ext: ExtensionState): Promise { // Feature specific registrations. unitTestsRegisterTypes(serviceManager); lintersRegisterTypes(serviceManager); - formattersRegisterTypes(serviceManager); installerRegisterTypes(serviceManager); commonRegisterTerminalTypes(serviceManager); debugConfigurationRegisterTypes(serviceManager); @@ -134,7 +131,6 @@ async function activateLegacy(ext: ExtensionState): Promise { const extensions = serviceContainer.get(IExtensions); await setDefaultLanguageServer(extensions, serviceManager); - const configuration = serviceManager.get(IConfigurationService); // Settings are dependent on Experiment service, so we need to initialize it after experiments are activated. serviceContainer.get(IConfigurationService).getSettings().register(); @@ -165,20 +161,9 @@ async function activateLegacy(ext: ExtensionState): Promise { serviceContainer.get(IApplicationDiagnostics).register(); serviceManager.get(ITerminalAutoActivation).register(); - const pythonSettings = configuration.getSettings(); serviceManager.get(ICodeExecutionManager).registerCommands(); - if ( - pythonSettings && - pythonSettings.formatting && - pythonSettings.formatting.provider !== 'internalConsole' - ) { - const formatProvider = new PythonFormattingEditProvider(context, serviceContainer); - disposables.push(languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); - disposables.push(languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); - } - disposables.push(new ReplProvider(serviceContainer)); const terminalProvider = new TerminalProvider(serviceContainer); @@ -200,7 +185,7 @@ async function activateLegacy(ext: ExtensionState): Promise { ), ); - registerInstallFormatterPrompt(serviceContainer); + logAndNotifyOnFormatterSetting(); registerCreateEnvironmentTriggers(disposables); initializePersistentStateForTriggers(ext.context); } diff --git a/src/client/formatters/autoPep8Formatter.ts b/src/client/formatters/autoPep8Formatter.ts deleted file mode 100644 index bf1285a60b58..000000000000 --- a/src/client/formatters/autoPep8Formatter.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as vscode from 'vscode'; -import { Product } from '../common/installer/productInstaller'; -import { IConfigurationService } from '../common/types'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class AutoPep8Formatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('autopep8', Product.autopep8, serviceContainer); - } - - public formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Thenable { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer - .get(IConfigurationService) - .getSettings(document.uri); - const hasCustomArgs = - Array.isArray(settings.formatting.autopep8Args) && settings.formatting.autopep8Args.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - const autoPep8Args = ['--diff']; - if (formatSelection) { - autoPep8Args.push( - ...['--line-range', (range!.start.line + 1).toString(), (range!.end.line + 1).toString()], - ); - } - const promise = super.provideDocumentFormattingEdits(document, options, token, autoPep8Args); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { - tool: 'autopep8', - hasCustomArgs, - formatSelection, - }); - return promise; - } -} diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts deleted file mode 100644 index 64e7d15a3d45..000000000000 --- a/src/client/formatters/baseFormatter.ts +++ /dev/null @@ -1,149 +0,0 @@ -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../common/application/types'; -import '../common/extensions'; -import { isNotInstalledError } from '../common/helpers'; -import { IFileSystem } from '../common/platform/types'; -import { IPythonToolExecutionService } from '../common/process/types'; -import { IDisposableRegistry, IInstaller, Product } from '../common/types'; -import { isNotebookCell } from '../common/utils/misc'; -import { IServiceContainer } from '../ioc/types'; -import { traceError } from '../logging'; -import { getTempFileWithDocumentContents, getTextEditsFromPatch } from './../common/editor'; -import { IFormatterHelper } from './types'; -import { IInstallFormatterPrompt } from '../providers/prompts/types'; - -export abstract class BaseFormatter { - protected readonly workspace: IWorkspaceService; - private readonly helper: IFormatterHelper; - private errorShown: boolean = false; - - constructor(public Id: string, private product: Product, protected serviceContainer: IServiceContainer) { - this.helper = serviceContainer.get(IFormatterHelper); - this.workspace = serviceContainer.get(IWorkspaceService); - } - - public abstract formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Thenable; - protected getDocumentPath(document: vscode.TextDocument, fallbackPath: string) { - if (path.basename(document.uri.fsPath) === document.uri.fsPath) { - return fallbackPath; - } - return path.dirname(document.fileName); - } - protected getWorkspaceUri(document: vscode.TextDocument) { - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - if (workspaceFolder) { - return workspaceFolder.uri; - } - const folders = this.workspace.workspaceFolders; - if (Array.isArray(folders) && folders.length > 0) { - return folders[0].uri; - } - return vscode.Uri.file(__dirname); - } - protected async provideDocumentFormattingEdits( - document: vscode.TextDocument, - _options: vscode.FormattingOptions, - token: vscode.CancellationToken, - args: string[], - cwd?: string, - ): Promise { - if (typeof cwd !== 'string' || cwd.length === 0) { - cwd = this.getWorkspaceUri(document).fsPath; - } - - // autopep8 and yapf have the ability to read from the process input stream and return the formatted code out of the output stream. - // However they don't support returning the diff of the formatted text when reading data from the input stream. - // Yet getting text formatted that way avoids having to create a temporary file, however the diffing will have - // to be done here in node (extension), i.e. extension CPU, i.e. less responsive solution. - // Also, always create temp files for Notebook cells. - const tempFile = await this.createTempFile(document); - if (this.checkCancellation(document.fileName, tempFile, token)) { - return []; - } - - const executionInfo = this.helper.getExecutionInfo(this.product, args, document.uri); - executionInfo.args.push(tempFile); - const pythonToolsExecutionService = this.serviceContainer.get( - IPythonToolExecutionService, - ); - const promise = pythonToolsExecutionService - .exec(executionInfo, { cwd, throwOnStdErr: false, token }, document.uri) - .then((output) => output.stdout) - .then((data) => { - if (this.checkCancellation(document.fileName, tempFile, token)) { - return [] as vscode.TextEdit[]; - } - return getTextEditsFromPatch(document.getText(), data); - }) - .catch((error) => { - if (this.checkCancellation(document.fileName, tempFile, token)) { - return [] as vscode.TextEdit[]; - } - - this.handleError(this.Id, error, document.uri).catch(() => {}); - return [] as vscode.TextEdit[]; - }) - .then((edits) => { - this.deleteTempFile(document.fileName, tempFile).ignoreErrors(); - return edits; - }); - - const appShell = this.serviceContainer.get(IApplicationShell); - const disposableRegistry = this.serviceContainer.get(IDisposableRegistry); - const disposable = appShell.setStatusBarMessage(`Formatting with ${this.Id}`, promise); - disposableRegistry.push(disposable); - return promise; - } - - protected async handleError(_expectedFileName: string, error: Error, resource?: vscode.Uri) { - if (isNotInstalledError(error)) { - const prompt = this.serviceContainer.get(IInstallFormatterPrompt); - if (!(await prompt.showInstallFormatterPrompt(resource))) { - const installer = this.serviceContainer.get(IInstaller); - const isInstalled = await installer.isInstalled(this.product, resource); - if (!isInstalled && !this.errorShown) { - traceError( - `\nPlease install '${this.Id}' into your environment.`, - "\nIf you don't want to use it you can turn it off or use another formatter in the settings.", - ); - this.errorShown = true; - } - } - } - - traceError(`Formatting with ${this.Id} failed:\n${error}`); - } - - /** - * Always create a temporary file when formatting notebook cells. - * This is because there is no physical file associated with notebook cells (they are all virtual). - */ - private async createTempFile(document: vscode.TextDocument): Promise { - const fs = this.serviceContainer.get(IFileSystem); - return document.isDirty || isNotebookCell(document) - ? getTempFileWithDocumentContents(document, fs) - : document.fileName; - } - - private deleteTempFile(originalFile: string, tempFile: string): Promise { - if (originalFile !== tempFile) { - const fs = this.serviceContainer.get(IFileSystem); - return fs.deleteFile(tempFile); - } - return Promise.resolve(); - } - - private checkCancellation(originalFile: string, tempFile: string, token?: vscode.CancellationToken): boolean { - if (token && token.isCancellationRequested) { - this.deleteTempFile(originalFile, tempFile).ignoreErrors(); - return true; - } - return false; - } -} diff --git a/src/client/formatters/blackFormatter.ts b/src/client/formatters/blackFormatter.ts deleted file mode 100644 index 0a8109e163e0..000000000000 --- a/src/client/formatters/blackFormatter.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IApplicationShell } from '../common/application/types'; -import { Product } from '../common/installer/productInstaller'; -import { IConfigurationService } from '../common/types'; -import { noop } from '../common/utils/misc'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class BlackFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('black', Product.black, serviceContainer); - } - - public async formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Promise { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer - .get(IConfigurationService) - .getSettings(document.uri); - const hasCustomArgs = Array.isArray(settings.formatting.blackArgs) && settings.formatting.blackArgs.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - if (formatSelection) { - const shell = this.serviceContainer.get(IApplicationShell); - // Black does not support partial formatting on purpose. - shell - .showErrorMessage(vscode.l10n.t('Black does not support the "Format Selection" command')) - .then(noop, noop); - return []; - } - - const blackArgs = ['--diff', '--quiet']; - - if (path.extname(document.fileName) === '.pyi') { - blackArgs.push('--pyi'); - } - - const promise = super.provideDocumentFormattingEdits(document, options, token, blackArgs); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'black', hasCustomArgs, formatSelection }); - return promise; - } -} diff --git a/src/client/formatters/dummyFormatter.ts b/src/client/formatters/dummyFormatter.ts deleted file mode 100644 index b4fdba9fbc0f..000000000000 --- a/src/client/formatters/dummyFormatter.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as vscode from 'vscode'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseFormatter } from './baseFormatter'; - -export class DummyFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('none', Product.yapf, serviceContainer); - } - - public formatDocument( - _document: vscode.TextDocument, - _options: vscode.FormattingOptions, - _token: vscode.CancellationToken, - _range?: vscode.Range, - ): Thenable { - return Promise.resolve([]); - } -} diff --git a/src/client/formatters/helper.ts b/src/client/formatters/helper.ts deleted file mode 100644 index ac305b51e785..000000000000 --- a/src/client/formatters/helper.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { ExecutionInfo, IConfigurationService, IFormattingSettings, Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { FormatterId, FormatterSettingsPropertyNames, IFormatterHelper } from './types'; - -@injectable() -export class FormatterHelper implements IFormatterHelper { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} - public translateToId(formatter: Product): FormatterId { - switch (formatter) { - case Product.autopep8: - return 'autopep8'; - case Product.black: - return 'black'; - case Product.yapf: - return 'yapf'; - default: { - throw new Error(`Unrecognized Formatter '${formatter}'`); - } - } - } - public getSettingsPropertyNames(formatter: Product): FormatterSettingsPropertyNames { - const id = this.translateToId(formatter); - return { - argsName: `${id}Args` as keyof IFormattingSettings, - pathName: `${id}Path` as keyof IFormattingSettings, - }; - } - public getExecutionInfo(formatter: Product, customArgs: string[], resource?: Uri): ExecutionInfo { - const settings = this.serviceContainer.get(IConfigurationService).getSettings(resource); - const names = this.getSettingsPropertyNames(formatter); - - const execPath = settings.formatting[names.pathName] as string; - let args: string[] = Array.isArray(settings.formatting[names.argsName]) - ? (settings.formatting[names.argsName] as string[]) - : []; - args = args.concat(customArgs); - - let moduleName: string | undefined; - - // If path information is not available, then treat it as a module, - if (path.basename(execPath) === execPath) { - moduleName = execPath; - } - - return { execPath, moduleName, args, product: formatter }; - } -} diff --git a/src/client/formatters/serviceRegistry.ts b/src/client/formatters/serviceRegistry.ts deleted file mode 100644 index 196e6c806b5f..000000000000 --- a/src/client/formatters/serviceRegistry.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IServiceManager } from '../ioc/types'; -import { FormatterHelper } from './helper'; -import { IFormatterHelper } from './types'; - -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(IFormatterHelper, FormatterHelper); -} diff --git a/src/client/formatters/types.ts b/src/client/formatters/types.ts deleted file mode 100644 index 7f4bcf5b7524..000000000000 --- a/src/client/formatters/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { ExecutionInfo, IFormattingSettings, Product } from '../common/types'; - -export const IFormatterHelper = Symbol('IFormatterHelper'); - -export type FormatterId = 'autopep8' | 'black' | 'yapf'; - -export type FormatterSettingsPropertyNames = { - argsName: keyof IFormattingSettings; - pathName: keyof IFormattingSettings; -}; - -export interface IFormatterHelper { - translateToId(formatter: Product): FormatterId; - getSettingsPropertyNames(formatter: Product): FormatterSettingsPropertyNames; - getExecutionInfo(formatter: Product, customArgs: string[], resource?: Uri): ExecutionInfo; -} diff --git a/src/client/formatters/yapfFormatter.ts b/src/client/formatters/yapfFormatter.ts deleted file mode 100644 index 08729a97694f..000000000000 --- a/src/client/formatters/yapfFormatter.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as vscode from 'vscode'; -import { IConfigurationService, Product } from '../common/types'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class YapfFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('yapf', Product.yapf, serviceContainer); - } - - public formatDocument( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - range?: vscode.Range, - ): Thenable { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer - .get(IConfigurationService) - .getSettings(document.uri); - const hasCustomArgs = Array.isArray(settings.formatting.yapfArgs) && settings.formatting.yapfArgs.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - const yapfArgs = ['--diff']; - if (formatSelection && range !== undefined) { - yapfArgs.push(...['--lines', `${range.start.line + 1}-${range.end.line + 1}`]); - } - // Yapf starts looking for config file starting from the file path. - const fallbarFolder = this.getWorkspaceUri(document).fsPath; - const cwd = this.getDocumentPath(document, fallbarFolder); - const promise = super.provideDocumentFormattingEdits(document, options, token, yapfArgs, cwd); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'yapf', hasCustomArgs, formatSelection }); - return promise; - } -} diff --git a/src/client/logging/settingLogs.ts b/src/client/logging/settingLogs.ts new file mode 100644 index 000000000000..721ab80d4500 --- /dev/null +++ b/src/client/logging/settingLogs.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { l10n } from 'vscode'; +import { traceError, traceInfo } from '.'; +import { Commands, PVSC_EXTENSION_ID } from '../common/constants'; +import { showErrorMessage } from '../common/vscodeApis/windowApis'; +import { getConfiguration, getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; +import { Common } from '../common/utils/localize'; +import { executeCommand } from '../common/vscodeApis/commandApis'; + +export function logAndNotifyOnFormatterSetting(): void { + getWorkspaceFolders()?.forEach(async (workspace) => { + let config = getConfiguration('editor', { uri: workspace.uri, languageId: 'python' }); + if (!config) { + config = getConfiguration('editor', workspace.uri); + if (!config) { + traceError('Unable to get editor configuration'); + } + } + const formatter = config.get('defaultFormatter', ''); + traceInfo(`Default formatter is set to ${formatter} for workspace ${workspace.uri.fsPath}`); + if (formatter === PVSC_EXTENSION_ID) { + traceError('Formatting features have been moved to separate formatter extensions.'); + traceError('Please install the formatter extension you prefer and set it as the default formatter.'); + traceError('For `autopep8` use: https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8'); + traceError( + 'For `black` use: https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter', + ); + traceError('For `yapf` use: https://marketplace.visualstudio.com/items?itemName=eeyore.yapf'); + const response = await showErrorMessage( + l10n.t( + 'Formatting features have been moved to separate formatter extensions. Please install the formatter extension you prefer and set it as the default formatter.', + ), + Common.showLogs, + ); + if (response === Common.showLogs) { + executeCommand(Commands.ViewOutput); + } + } + }); +} diff --git a/src/client/providers/formatProvider.ts b/src/client/providers/formatProvider.ts deleted file mode 100644 index 1ea239c03bec..000000000000 --- a/src/client/providers/formatProvider.ts +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as vscode from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { PYTHON_LANGUAGE } from '../common/constants'; -import { IConfigurationService } from '../common/types'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { AutoPep8Formatter } from '../formatters/autoPep8Formatter'; -import { BaseFormatter } from '../formatters/baseFormatter'; -import { BlackFormatter } from '../formatters/blackFormatter'; -import { DummyFormatter } from '../formatters/dummyFormatter'; -import { YapfFormatter } from '../formatters/yapfFormatter'; - -export class PythonFormattingEditProvider - implements vscode.DocumentFormattingEditProvider, vscode.DocumentRangeFormattingEditProvider, vscode.Disposable { - private readonly config: IConfigurationService; - - private readonly workspace: IWorkspaceService; - - private readonly documentManager: IDocumentManager; - - private readonly commands: ICommandManager; - - private formatters = new Map(); - - private disposables: vscode.Disposable[] = []; - - // Workaround for https://github.com/Microsoft/vscode/issues/41194 - private documentVersionBeforeFormatting = -1; - - private formatterMadeChanges = false; - - private saving = false; - - public constructor(_context: vscode.ExtensionContext, serviceContainer: IServiceContainer) { - const yapfFormatter = new YapfFormatter(serviceContainer); - const autoPep8 = new AutoPep8Formatter(serviceContainer); - const black = new BlackFormatter(serviceContainer); - const dummy = new DummyFormatter(serviceContainer); - this.formatters.set(yapfFormatter.Id, yapfFormatter); - this.formatters.set(black.Id, black); - this.formatters.set(autoPep8.Id, autoPep8); - this.formatters.set(dummy.Id, dummy); - - this.commands = serviceContainer.get(ICommandManager); - this.workspace = serviceContainer.get(IWorkspaceService); - this.documentManager = serviceContainer.get(IDocumentManager); - this.config = serviceContainer.get(IConfigurationService); - const interpreterService = serviceContainer.get(IInterpreterService); - this.disposables.push( - this.documentManager.onDidSaveTextDocument(async (document) => this.onSaveDocument(document)), - ); - this.disposables.push( - interpreterService.onDidChangeInterpreter(async () => { - if (this.documentManager.activeTextEditor) { - return this.onSaveDocument(this.documentManager.activeTextEditor.document); - } - - return undefined; - }), - ); - } - - public dispose(): void { - this.disposables.forEach((d) => d.dispose()); - } - - public provideDocumentFormattingEdits( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - ): Promise { - return this.provideDocumentRangeFormattingEdits(document, undefined, options, token); - } - - public async provideDocumentRangeFormattingEdits( - document: vscode.TextDocument, - range: vscode.Range | undefined, - options: vscode.FormattingOptions, - token: vscode.CancellationToken, - ): Promise { - // Workaround for https://github.com/Microsoft/vscode/issues/41194 - // VSC rejects 'format on save' promise in 750 ms. Python formatting may take quite a bit longer. - // Workaround is to resolve promise to nothing here, then execute format document and force new save. - // However, we need to know if this is 'format document' or formatting on save. - - if (this.saving || document.languageId !== PYTHON_LANGUAGE) { - // We are saving after formatting (see onSaveDocument below) - // so we do not want to format again. - return []; - } - - // Remember content before formatting so we can detect if - // formatting edits have been really applied - const editorConfig = this.workspace.getConfiguration('editor', document.uri); - if (editorConfig.get('formatOnSave') === true) { - this.documentVersionBeforeFormatting = document.version; - } - - const settings = this.config.getSettings(document.uri); - const formatter = this.formatters.get(settings.formatting.provider)!; - const edits = await formatter.formatDocument(document, options, token, range); - - this.formatterMadeChanges = edits.length > 0; - return edits; - } - - private async onSaveDocument(document: vscode.TextDocument): Promise { - // Promise was rejected = formatting took too long. - // Don't format inside the event handler, do it on timeout - setTimeout(() => { - try { - if ( - this.formatterMadeChanges && - !document.isDirty && - document.version === this.documentVersionBeforeFormatting - ) { - // Formatter changes were not actually applied due to the timeout on save. - // Force formatting now and then save the document. - this.commands.executeCommand('editor.action.formatDocument').then(async () => { - this.saving = true; - await document.save(); - this.saving = false; - }); - } - } finally { - this.documentVersionBeforeFormatting = -1; - this.saving = false; - this.formatterMadeChanges = false; - } - }, 50); - } -} diff --git a/src/client/providers/prompts/installFormatterPrompt.ts b/src/client/providers/prompts/installFormatterPrompt.ts deleted file mode 100644 index 5743f8402053..000000000000 --- a/src/client/providers/prompts/installFormatterPrompt.ts +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { inject, injectable } from 'inversify'; -import { IDisposableRegistry } from '../../common/types'; -import { Common, ToolsExtensions } from '../../common/utils/localize'; -import { isExtensionEnabled } from '../../common/vscodeApis/extensionsApi'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { getConfiguration, onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis'; -import { IServiceContainer } from '../../ioc/types'; -import { - doNotShowPromptState, - inFormatterExtensionExperiment, - installFormatterExtension, - updateDefaultFormatter, -} from './promptUtils'; -import { AUTOPEP8_EXTENSION, BLACK_EXTENSION, IInstallFormatterPrompt } from './types'; - -const SHOW_FORMATTER_INSTALL_PROMPT_DONOTSHOW_KEY = 'showFormatterExtensionInstallPrompt'; - -@injectable() -export class InstallFormatterPrompt implements IInstallFormatterPrompt { - private currentlyShown = false; - - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} - - /* - * This method is called when the user saves a python file or a cell. - * Returns true if an extension was selected. Otherwise returns false. - */ - public async showInstallFormatterPrompt(resource?: Uri): Promise { - if (!inFormatterExtensionExperiment(this.serviceContainer)) { - return false; - } - - const promptState = doNotShowPromptState(SHOW_FORMATTER_INSTALL_PROMPT_DONOTSHOW_KEY, this.serviceContainer); - if (this.currentlyShown || promptState.value) { - return false; - } - - const config = getConfiguration('python', resource); - const formatter = config.get('formatting.provider', 'none'); - if (!['autopep8', 'black'].includes(formatter)) { - return false; - } - - const editorConfig = getConfiguration('editor', { uri: resource, languageId: 'python' }); - const defaultFormatter = editorConfig.get('defaultFormatter', ''); - if ([BLACK_EXTENSION, AUTOPEP8_EXTENSION].includes(defaultFormatter)) { - return false; - } - - const black = isExtensionEnabled(BLACK_EXTENSION); - const autopep8 = isExtensionEnabled(AUTOPEP8_EXTENSION); - - let selection: string | undefined; - - if (black || autopep8) { - this.currentlyShown = true; - if (black && autopep8) { - selection = await showInformationMessage( - ToolsExtensions.selectMultipleFormattersPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ); - } else if (black) { - selection = await showInformationMessage( - ToolsExtensions.selectBlackFormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ); - if (selection === Common.bannerLabelYes) { - selection = 'Black'; - } - } else if (autopep8) { - selection = await showInformationMessage( - ToolsExtensions.selectAutopep8FormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ); - if (selection === Common.bannerLabelYes) { - selection = 'Autopep8'; - } - } - } else if (formatter === 'black' && !black) { - this.currentlyShown = true; - selection = await showInformationMessage( - ToolsExtensions.installBlackFormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ); - } else if (formatter === 'autopep8' && !autopep8) { - this.currentlyShown = true; - selection = await showInformationMessage( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ); - } - - let userSelectedAnExtension = false; - if (selection === 'Black') { - if (black) { - userSelectedAnExtension = true; - await updateDefaultFormatter(BLACK_EXTENSION, resource); - } else { - userSelectedAnExtension = true; - await installFormatterExtension(BLACK_EXTENSION, resource); - } - } else if (selection === 'Autopep8') { - if (autopep8) { - userSelectedAnExtension = true; - await updateDefaultFormatter(AUTOPEP8_EXTENSION, resource); - } else { - userSelectedAnExtension = true; - await installFormatterExtension(AUTOPEP8_EXTENSION, resource); - } - } else if (selection === Common.doNotShowAgain) { - userSelectedAnExtension = false; - await promptState.updateValue(true); - } else { - userSelectedAnExtension = false; - } - - this.currentlyShown = false; - return userSelectedAnExtension; - } -} - -export function registerInstallFormatterPrompt(serviceContainer: IServiceContainer): void { - const disposables = serviceContainer.get(IDisposableRegistry); - const installFormatterPrompt = serviceContainer.get(IInstallFormatterPrompt); - disposables.push( - onDidSaveTextDocument(async (e) => { - const editorConfig = getConfiguration('editor', { uri: e.uri, languageId: 'python' }); - if (e.languageId === 'python' && editorConfig.get('formatOnSave')) { - await installFormatterPrompt.showInstallFormatterPrompt(e.uri); - } - }), - ); -} diff --git a/src/client/providers/prompts/promptUtils.ts b/src/client/providers/prompts/promptUtils.ts deleted file mode 100644 index 05b1b28f061a..000000000000 --- a/src/client/providers/prompts/promptUtils.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { ConfigurationTarget, Uri } from 'vscode'; -import { ShowFormatterExtensionPrompt } from '../../common/experiments/groups'; -import { IExperimentService, IPersistentState, IPersistentStateFactory } from '../../common/types'; -import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { isInsider } from '../../common/vscodeApis/extensionsApi'; -import { getConfiguration, getWorkspaceFolder } from '../../common/vscodeApis/workspaceApis'; -import { IServiceContainer } from '../../ioc/types'; - -export function inFormatterExtensionExperiment(serviceContainer: IServiceContainer): boolean { - const experiment = serviceContainer.get(IExperimentService); - return experiment.inExperimentSync(ShowFormatterExtensionPrompt.experiment); -} - -export function doNotShowPromptState(key: string, serviceContainer: IServiceContainer): IPersistentState { - const persistFactory = serviceContainer.get(IPersistentStateFactory); - const promptState = persistFactory.createWorkspacePersistentState(key, false); - return promptState; -} - -export async function updateDefaultFormatter(extensionId: string, resource?: Uri): Promise { - const scope = getWorkspaceFolder(resource) ? ConfigurationTarget.Workspace : ConfigurationTarget.Global; - - const config = getConfiguration('python', resource); - const editorConfig = getConfiguration('editor', { uri: resource, languageId: 'python' }); - await editorConfig.update('defaultFormatter', extensionId, scope, true); - await config.update('formatting.provider', 'none', scope); -} - -export async function installFormatterExtension(extensionId: string, resource?: Uri): Promise { - await executeCommand('workbench.extensions.installExtension', extensionId, { - installPreReleaseVersion: isInsider(), - }); - - await updateDefaultFormatter(extensionId, resource); -} diff --git a/src/client/providers/prompts/types.ts b/src/client/providers/prompts/types.ts deleted file mode 100644 index 4edaadb46b46..000000000000 --- a/src/client/providers/prompts/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; - -export const BLACK_EXTENSION = 'ms-python.black-formatter'; -export const AUTOPEP8_EXTENSION = 'ms-python.autopep8'; - -export const IInstallFormatterPrompt = Symbol('IInstallFormatterPrompt'); -export interface IInstallFormatterPrompt { - showInstallFormatterPrompt(resource?: Uri): Promise; -} diff --git a/src/client/providers/serviceRegistry.ts b/src/client/providers/serviceRegistry.ts index 70fc6dc34135..a96ec14ff5e9 100644 --- a/src/client/providers/serviceRegistry.ts +++ b/src/client/providers/serviceRegistry.ts @@ -6,13 +6,10 @@ import { IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { CodeActionProviderService } from './codeActionProvider/main'; -import { InstallFormatterPrompt } from './prompts/installFormatterPrompt'; -import { IInstallFormatterPrompt } from './prompts/types'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton( IExtensionSingleActivationService, CodeActionProviderService, ); - serviceManager.addSingleton(IInstallFormatterPrompt, InstallFormatterPrompt); } diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 5c1842a2c97c..15c745cbd64f 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -28,11 +28,7 @@ import { EditorUtils } from '../../client/common/editor'; import { ExperimentService } from '../../client/common/experiments/service'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; -import { - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../client/common/installer/productPath'; +import { LinterProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; import { ProductService } from '../../client/common/installer/productService'; import { IInstallationChannelManager, @@ -131,7 +127,6 @@ suite('Installer', () => { ioc.registerFileSystemTypes(); ioc.registerVariableTypes(); ioc.registerLinterTypes(); - ioc.registerFormatterTypes(); ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); @@ -159,11 +154,6 @@ suite('Installer', () => { ioc.registerMockProcessTypes(); ioc.serviceManager.addSingletonInstance(IsWindows, false); ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); ioc.serviceManager.addSingleton( IProductPathService, LinterProductPathService, diff --git a/src/test/common/installer/channelManager.unit.test.ts b/src/test/common/installer/channelManager.unit.test.ts index 319a9647fec7..9789f9f18718 100644 --- a/src/test/common/installer/channelManager.unit.test.ts +++ b/src/test/common/installer/channelManager.unit.test.ts @@ -57,7 +57,7 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { showNoInstallersMessage.resolves(); installChannelManager = new InstallationChannelManager(serviceContainer.object); - const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); expect(channel).to.equal(undefined, 'should be undefined'); assert.ok(showNoInstallersMessage.calledOnceWith(resource)); }); @@ -79,7 +79,7 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { showNoInstallersMessage.resolves(); installChannelManager = new InstallationChannelManager(serviceContainer.object); - const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); assert.ok(showNoInstallersMessage.notCalled); appShell.verifyAll(); expect(channel).to.equal(undefined, 'Channel should not be set'); @@ -107,7 +107,7 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { showNoInstallersMessage.resolves(); installChannelManager = new InstallationChannelManager(serviceContainer.object); - const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); assert.ok(showNoInstallersMessage.notCalled); appShell.verifyAll(); expect(channel).to.not.equal(undefined, 'Channel should be set'); diff --git a/src/test/common/installer/productPath.unit.test.ts b/src/test/common/installer/productPath.unit.test.ts index 0f627289da70..8e65f3a5caed 100644 --- a/src/test/common/installer/productPath.unit.test.ts +++ b/src/test/common/installer/productPath.unit.test.ts @@ -12,21 +12,12 @@ import '../../../client/common/extensions'; import { ProductInstaller } from '../../../client/common/installer/productInstaller'; import { BaseProductPathsService, - FormatterProductPathService, LinterProductPathService, TestFrameworkProductPathService, } from '../../../client/common/installer/productPath'; import { ProductService } from '../../../client/common/installer/productService'; import { IProductService } from '../../../client/common/installer/types'; -import { - IConfigurationService, - IFormattingSettings, - IInstaller, - IPythonSettings, - Product, - ProductType, -} from '../../../client/common/types'; -import { IFormatterHelper } from '../../../client/formatters/types'; +import { IConfigurationService, IInstaller, IPythonSettings, Product, ProductType } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { ILinterInfo, ILinterManager } from '../../../client/linters/types'; import { ITestsHelper } from '../../../client/testing/common/types'; @@ -44,7 +35,6 @@ suite('Product Path', () => { } } let serviceContainer: TypeMoq.IMock; - let formattingSettings: TypeMoq.IMock; let unitTestSettings: TypeMoq.IMock; let configService: TypeMoq.IMock; let productInstaller: ProductInstaller; @@ -54,12 +44,10 @@ suite('Product Path', () => { } serviceContainer = TypeMoq.Mock.ofType(); configService = TypeMoq.Mock.ofType(); - formattingSettings = TypeMoq.Mock.ofType(); unitTestSettings = TypeMoq.Mock.ofType(); productInstaller = new ProductInstaller(serviceContainer.object); const pythonSettings = TypeMoq.Mock.ofType(); - pythonSettings.setup((p) => p.formatting).returns(() => formattingSettings.object); pythonSettings.setup((p) => p.testing).returns(() => unitTestSettings.object); configService .setup((s) => s.getSettings(TypeMoq.It.isValue(resource))) @@ -99,37 +87,6 @@ suite('Product Path', () => { }); const productType = new ProductService().getProductType(product.value); switch (productType) { - case ProductType.Formatter: { - test(`Ensure path is returned for ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const productPathService = new FormatterProductPathService(serviceContainer.object); - const formatterHelper = TypeMoq.Mock.ofType(); - const expectedPath = 'Some Path'; - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IFormatterHelper), TypeMoq.It.isAny())) - .returns(() => formatterHelper.object); - formattingSettings - .setup((f) => f.autopep8Path) - .returns(() => expectedPath) - .verifiable(TypeMoq.Times.atLeastOnce()); - formatterHelper - .setup((f) => f.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) - .returns(() => { - return { - pathName: 'autopep8Path', - argsName: 'autopep8Args', - }; - }) - .verifiable(TypeMoq.Times.once()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - expect(value).to.be.equal(expectedPath); - formattingSettings.verifyAll(); - formatterHelper.verifyAll(); - }); - break; - } case ProductType.Linter: { test(`Ensure path is returned for ${product.name} (${ resource ? 'With a resource' : 'without a resource' diff --git a/src/test/common/installer/serviceRegistry.unit.test.ts b/src/test/common/installer/serviceRegistry.unit.test.ts index a23cff298d6c..5b971790fa9a 100644 --- a/src/test/common/installer/serviceRegistry.unit.test.ts +++ b/src/test/common/installer/serviceRegistry.unit.test.ts @@ -10,7 +10,6 @@ import { PipEnvInstaller } from '../../../client/common/installer/pipEnvInstalle import { PipInstaller } from '../../../client/common/installer/pipInstaller'; import { PoetryInstaller } from '../../../client/common/installer/poetryInstaller'; import { - FormatterProductPathService, LinterProductPathService, TestFrameworkProductPathService, } from '../../../client/common/installer/productPath'; @@ -46,13 +45,6 @@ suite('Common installer Service Registry', () => { ), ).once(); verify(serviceManager.addSingleton(IProductService, ProductService)).once(); - verify( - serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ), - ).once(); verify( serviceManager.addSingleton( IProductPathService, diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index a6b647ad181d..2f73bc520307 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -147,7 +147,6 @@ suite('Module Installer', () => { ioc.registerUnitTestTypes(); ioc.registerVariableTypes(); ioc.registerLinterTypes(); - ioc.registerFormatterTypes(); ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); diff --git a/src/test/format/extension.format.test.ts b/src/test/format/extension.format.test.ts deleted file mode 100644 index 40131be24ec2..000000000000 --- a/src/test/format/extension.format.test.ts +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { CancellationTokenSource, Position, Uri, window, workspace } from 'vscode'; -import { IProcessServiceFactory } from '../../client/common/process/types'; -import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; -import { BlackFormatter } from '../../client/formatters/blackFormatter'; -import { YapfFormatter } from '../../client/formatters/yapfFormatter'; -import { isPythonVersionInProcess } from '../common'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; -import { MockProcessService } from '../mocks/proc'; -import { registerForIOC } from '../pythonEnvironments/legacyIOC'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; -import { compareFiles } from '../textUtils'; - -const ch = window.createOutputChannel('Tests'); -const formatFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); -const workspaceRootPath = path.join(__dirname, '..', '..', '..', 'src', 'test'); -const originalUnformattedFile = path.join(formatFilesPath, 'fileToFormat.py'); - -const autoPep8FileToFormat = path.join(formatFilesPath, 'autoPep8FileToFormat.py'); -const autoPep8Formatted = path.join(formatFilesPath, 'autoPep8Formatted.py'); -const blackFileToFormat = path.join(formatFilesPath, 'blackFileToFormat.py'); -const blackFormatted = path.join(formatFilesPath, 'blackFormatted.py'); -const yapfFileToFormat = path.join(formatFilesPath, 'yapfFileToFormat.py'); -const yapfFormatted = path.join(formatFilesPath, 'yapfFormatted.py'); - -let formattedYapf = ''; -let formattedBlack = ''; -let formattedAutoPep8 = ''; - -suite('Formatting - General', () => { - let ioc: UnitTestIocContainer; - - suiteSetup(async function () { - // https://github.com/microsoft/vscode-python/issues/12564 - // Skipping one test in the file is resulting in the next one failing, so skipping the entire suiteuntil further investigation. - - return this.skip(); - await initialize(); - await initializeDI(); - [autoPep8FileToFormat, blackFileToFormat, yapfFileToFormat].forEach((file) => { - fs.copySync(originalUnformattedFile, file, { overwrite: true }); - }); - formattedYapf = fs.readFileSync(yapfFormatted).toString(); - formattedAutoPep8 = fs.readFileSync(autoPep8Formatted).toString(); - formattedBlack = fs.readFileSync(blackFormatted).toString(); - }); - - async function formattingTestIsBlackSupported(): Promise { - const processService = await ioc.serviceContainer - .get(IProcessServiceFactory) - .create(Uri.file(workspaceRootPath)); - return !(await isPythonVersionInProcess(processService, '2', '3.0', '3.1', '3.2', '3.3', '3.4', '3.5')); - } - - setup(async () => { - await initializeTest(); - await initializeDI(); - }); - suiteTeardown(async () => { - [autoPep8FileToFormat, blackFileToFormat, yapfFileToFormat].forEach((file) => { - if (fs.existsSync(file)) { - fs.unlinkSync(file); - } - }); - ch.dispose(); - await closeActiveWindows(); - }); - teardown(async () => { - await ioc.dispose(); - }); - - async function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerUnitTestTypes(); - ioc.registerFormatterTypes(); - ioc.registerInterpreterStorageTypes(); - - // Mocks. - ioc.registerMockProcessTypes(); - await ioc.registerMockInterpreterTypes(); - - await registerForIOC(ioc.serviceManager, ioc.serviceContainer); - } - - async function injectFormatOutput(outputFileName: string) { - const procService = (await ioc.serviceContainer - .get(IProcessServiceFactory) - .create()) as MockProcessService; - procService.onExecObservable((_file, args, _options, callback) => { - if (args.indexOf('--diff') >= 0) { - callback({ - out: fs.readFileSync(path.join(formatFilesPath, outputFileName), 'utf8'), - source: 'stdout', - }); - } - }); - } - - async function testFormatting( - formatter: AutoPep8Formatter | BlackFormatter | YapfFormatter, - formattedContents: string, - fileToFormat: string, - outputFileName: string, - ) { - const textDocument = await workspace.openTextDocument(fileToFormat); - const textEditor = await window.showTextDocument(textDocument); - const options = { - insertSpaces: textEditor.options.insertSpaces! as boolean, - tabSize: textEditor.options.tabSize! as number, - }; - - await injectFormatOutput(outputFileName); - - const edits = await formatter.formatDocument(textDocument, options, new CancellationTokenSource().token); - await textEditor.edit((editBuilder) => { - edits.forEach((edit) => editBuilder.replace(edit.range, edit.newText)); - }); - compareFiles(formattedContents, textEditor.document.getText()); - } - - test('AutoPep8', async function () { - // https://github.com/microsoft/vscode-python/issues/12564 - - return this.skip(); - await testFormatting( - new AutoPep8Formatter(ioc.serviceContainer), - formattedAutoPep8, - autoPep8FileToFormat, - 'autopep8.output', - ); - }); - - test('Black', async function () { - // https://github.com/microsoft/vscode-python/issues/12564 - - return this.skip(); - if (!(await formattingTestIsBlackSupported())) { - // Skip for versions of python below 3.6, as Black doesn't support them at all. - - return this.skip(); - } - await testFormatting( - new BlackFormatter(ioc.serviceContainer), - formattedBlack, - blackFileToFormat, - 'black.output', - ); - }); - test('Yapf', async () => - testFormatting(new YapfFormatter(ioc.serviceContainer), formattedYapf, yapfFileToFormat, 'yapf.output')); - - test('Yapf on dirty file', async () => { - const sourceDir = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); - const targetDir = path.join(__dirname, '..', 'pythonFiles', 'formatting'); - - const originalName = 'formatWhenDirty.py'; - const resultsName = 'formatWhenDirtyResult.py'; - const fileToFormat = path.join(targetDir, originalName); - const formattedFile = path.join(targetDir, resultsName); - - if (!fs.pathExistsSync(targetDir)) { - fs.mkdirpSync(targetDir); - } - fs.copySync(path.join(sourceDir, originalName), fileToFormat, { overwrite: true }); - fs.copySync(path.join(sourceDir, resultsName), formattedFile, { overwrite: true }); - - const textDocument = await workspace.openTextDocument(fileToFormat); - const textEditor = await window.showTextDocument(textDocument); - await textEditor.edit((builder) => { - // Make file dirty. Trailing blanks will be removed. - builder.insert(new Position(0, 0), '\n \n'); - }); - - const dir = path.dirname(fileToFormat); - const configFile = path.join(dir, '.style.yapf'); - try { - // Create yapf configuration file - const content = '[style]\nbased_on_style = pep8\nindent_width=5\n'; - fs.writeFileSync(configFile, content); - - const options = { insertSpaces: textEditor.options.insertSpaces! as boolean, tabSize: 1 }; - const formatter = new YapfFormatter(ioc.serviceContainer); - const edits = await formatter.formatDocument(textDocument, options, new CancellationTokenSource().token); - await textEditor.edit((editBuilder) => { - edits.forEach((edit) => editBuilder.replace(edit.range, edit.newText)); - }); - - const expected = fs.readFileSync(formattedFile).toString(); - const actual = textEditor.document.getText(); - compareFiles(expected, actual); - } finally { - if (fs.existsSync(configFile)) { - fs.unlinkSync(configFile); - } - } - }); -}); diff --git a/src/test/format/format.helper.test.ts b/src/test/format/format.helper.test.ts deleted file mode 100644 index 50000f1af867..000000000000 --- a/src/test/format/format.helper.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import * as assert from 'assert'; -import * as TypeMoq from 'typemoq'; -import { IConfigurationService, IFormattingSettings, Product } from '../../client/common/types'; -import * as EnumEx from '../../client/common/utils/enum'; -import { FormatterHelper } from '../../client/formatters/helper'; -import { FormatterId } from '../../client/formatters/types'; -import { getExtensionSettings } from '../extensionSettings'; -import { initialize } from '../initialize'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; - -suite('Formatting - Helper', () => { - let ioc: UnitTestIocContainer; - let formatHelper: FormatterHelper; - - suiteSetup(initialize); - setup(() => { - ioc = new UnitTestIocContainer(); - - const config = TypeMoq.Mock.ofType(); - config.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => getExtensionSettings(undefined)); - - ioc.serviceManager.addSingletonInstance(IConfigurationService, config.object); - formatHelper = new FormatterHelper(ioc.serviceManager); - }); - - test('Ensure product is set in Execution Info', async () => { - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const info = formatHelper.getExecutionInfo(formatter, []); - assert.strictEqual( - info.product, - formatter, - `Incorrect products for ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure executable is set in Execution Info', async () => { - const settings = getExtensionSettings(undefined); - - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const info = formatHelper.getExecutionInfo(formatter, []); - const names = formatHelper.getSettingsPropertyNames(formatter); - const execPath = settings.formatting[names.pathName] as string; - - assert.strictEqual( - info.execPath, - execPath, - `Incorrect executable paths for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure arguments are set in Execution Info', async () => { - const settings = getExtensionSettings(undefined); - const customArgs = ['1', '2', '3']; - - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const names = formatHelper.getSettingsPropertyNames(formatter); - const args: string[] = Array.isArray(settings.formatting[names.argsName]) - ? (settings.formatting[names.argsName] as string[]) - : []; - const expectedArgs = args.concat(customArgs).join(','); - - assert.strictEqual( - expectedArgs.endsWith(customArgs.join(',')), - true, - `Incorrect custom arguments for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure correct setting names are returned', async () => { - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const translatedId = formatHelper.translateToId(formatter)!; - const settings = { - argsName: `${translatedId}Args` as keyof IFormattingSettings, - pathName: `${translatedId}Path` as keyof IFormattingSettings, - }; - - assert.deepEqual( - formatHelper.getSettingsPropertyNames(formatter), - settings, - `Incorrect settings for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - test('Ensure translation of ids works', async () => { - const formatterMapping = new Map(); - formatterMapping.set(Product.autopep8, 'autopep8'); - formatterMapping.set(Product.black, 'black'); - formatterMapping.set(Product.yapf, 'yapf'); - - [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { - const translatedId = formatHelper.translateToId(formatter); - assert.strictEqual( - translatedId, - formatterMapping.get(formatter)!, - `Incorrect translation for product ${formatHelper.translateToId(formatter)}`, - ); - }); - }); - - EnumEx.getValues(Product).forEach((product) => { - const formatterMapping = new Map(); - formatterMapping.set(Product.autopep8, 'autopep8'); - formatterMapping.set(Product.black, 'black'); - formatterMapping.set(Product.yapf, 'yapf'); - if (formatterMapping.has(product)) { - return; - } - - test(`Ensure translation of ids throws exceptions for unknown formatters (${product})`, async () => { - assert.throws(() => formatHelper.translateToId(product)); - }); - }); -}); diff --git a/src/test/format/formatter.unit.test.ts b/src/test/format/formatter.unit.test.ts deleted file mode 100644 index 05970d0c71f6..000000000000 --- a/src/test/format/formatter.unit.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as path from 'path'; -import { anything, capture, instance, mock, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { CancellationTokenSource, FormattingOptions, TextDocument, Uri } from 'vscode'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { PythonSettings } from '../../client/common/configSettings'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; -import { IPythonToolExecutionService } from '../../client/common/process/types'; -import { - ExecutionInfo, - IConfigurationService, - IDisposableRegistry, - IFormattingSettings, - ILogOutputChannel, - IPythonSettings, -} from '../../client/common/types'; -import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; -import { BaseFormatter } from '../../client/formatters/baseFormatter'; -import { BlackFormatter } from '../../client/formatters/blackFormatter'; -import { FormatterHelper } from '../../client/formatters/helper'; -import { IFormatterHelper } from '../../client/formatters/types'; -import { YapfFormatter } from '../../client/formatters/yapfFormatter'; -import { ServiceContainer } from '../../client/ioc/container'; -import { IServiceContainer } from '../../client/ioc/types'; -import { noop } from '../core'; -import { MockOutputChannel } from '../mockClasses'; - -suite('Formatting - Test Arguments', () => { - let container: IServiceContainer; - let outputChannel: ILogOutputChannel; - let workspace: IWorkspaceService; - let settings: IPythonSettings; - const workspaceUri = Uri.file(__dirname); - let document: typemoq.IMock; - const docUri = Uri.file(__filename); - let pythonToolExecutionService: IPythonToolExecutionService; - const options: FormattingOptions = { insertSpaces: false, tabSize: 1 }; - const formattingSettingsWithPath: IFormattingSettings = { - autopep8Args: ['1', '2'], - autopep8Path: path.join('a', 'exe'), - blackArgs: ['1', '2'], - blackPath: path.join('a', 'exe'), - provider: '', - yapfArgs: ['1', '2'], - yapfPath: path.join('a', 'exe'), - }; - - const formattingSettingsWithModuleName: IFormattingSettings = { - autopep8Args: ['1', '2'], - autopep8Path: 'module_name', - blackArgs: ['1', '2'], - blackPath: 'module_name', - provider: '', - yapfArgs: ['1', '2'], - yapfPath: 'module_name', - }; - - setup(() => { - container = mock(ServiceContainer); - outputChannel = mock(MockOutputChannel); - workspace = mock(WorkspaceService); - settings = mock(PythonSettings); - document = typemoq.Mock.ofType(); - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => ''); - document.setup((doc) => doc.isDirty).returns(() => false); - document.setup((doc) => doc.fileName).returns(() => docUri.fsPath); - document.setup((doc) => doc.uri).returns(() => docUri); - pythonToolExecutionService = mock(PythonToolExecutionService); - - const configService = mock(ConfigurationService); - const formatterHelper = new FormatterHelper(instance(container)); - - const appShell = mock(ApplicationShell); - when(appShell.setStatusBarMessage(anything(), anything())).thenReturn({ dispose: noop }); - - when(configService.getSettings(anything())).thenReturn(instance(settings)); - when(workspace.getWorkspaceFolder(anything())).thenReturn({ name: '', index: 0, uri: workspaceUri }); - when(container.get(ILogOutputChannel)).thenReturn(instance(outputChannel)); - when(container.get(IApplicationShell)).thenReturn(instance(appShell)); - when(container.get(IFormatterHelper)).thenReturn(formatterHelper); - when(container.get(IWorkspaceService)).thenReturn(instance(workspace)); - when(container.get(IConfigurationService)).thenReturn(instance(configService)); - when(container.get(IPythonToolExecutionService)).thenReturn( - instance(pythonToolExecutionService), - ); - when(container.get(IDisposableRegistry)).thenReturn([]); - }); - - async function setupFormatter( - formatter: BaseFormatter, - formattingSettings: IFormattingSettings, - ): Promise { - const { token } = new CancellationTokenSource(); - when(settings.formatting).thenReturn(formattingSettings); - when(pythonToolExecutionService.exec(anything(), anything(), anything())).thenResolve({ stdout: '' }); - - await formatter.formatDocument(document.object, options, token); - - const args = capture(pythonToolExecutionService.exec).first(); - return args[0]; - } - test('Ensure blackPath and args used to launch the formatter', async () => { - const formatter = new BlackFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithPath.blackPath); - assert.strictEqual(execInfo.moduleName, undefined); - assert.deepEqual( - execInfo.args, - formattingSettingsWithPath.blackArgs.concat(['--diff', '--quiet', docUri.fsPath]), - ); - }); - test('Ensure black modulename and args used to launch the formatter', async () => { - const formatter = new BlackFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithModuleName.blackPath); - assert.strictEqual(execInfo.moduleName, formattingSettingsWithModuleName.blackPath); - assert.deepEqual( - execInfo.args, - formattingSettingsWithPath.blackArgs.concat(['--diff', '--quiet', docUri.fsPath]), - ); - }); - test('Ensure autopep8path and args used to launch the formatter', async () => { - const formatter = new AutoPep8Formatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithPath.autopep8Path); - assert.strictEqual(execInfo.moduleName, undefined); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.autopep8Args.concat(['--diff', docUri.fsPath])); - }); - test('Ensure autpep8 modulename and args used to launch the formatter', async () => { - const formatter = new AutoPep8Formatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithModuleName.autopep8Path); - assert.strictEqual(execInfo.moduleName, formattingSettingsWithModuleName.autopep8Path); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.autopep8Args.concat(['--diff', docUri.fsPath])); - }); - test('Ensure yapfpath and args used to launch the formatter', async () => { - const formatter = new YapfFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithPath.yapfPath); - assert.strictEqual(execInfo.moduleName, undefined); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.yapfArgs.concat(['--diff', docUri.fsPath])); - }); - test('Ensure yapf modulename and args used to launch the formatter', async () => { - const formatter = new YapfFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.strictEqual(execInfo.execPath, formattingSettingsWithModuleName.yapfPath); - assert.strictEqual(execInfo.moduleName, formattingSettingsWithModuleName.yapfPath); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.yapfArgs.concat(['--diff', docUri.fsPath])); - }); -}); diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts index f89ee86c0b42..5c1cae31d158 100644 --- a/src/test/linters/lint.multiroot.test.ts +++ b/src/test/linters/lint.multiroot.test.ts @@ -3,11 +3,7 @@ import * as path from 'path'; import { CancellationTokenSource, ConfigurationTarget, Uri, workspace } from 'vscode'; import { LanguageServerType } from '../../client/activation/types'; import { PythonSettings } from '../../client/common/configSettings'; -import { - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../client/common/installer/productPath'; +import { LinterProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; import { ProductService } from '../../client/common/installer/productService'; import { IProductPathService, IProductService } from '../../client/common/installer/types'; import { IConfigurationService, Product, ProductType } from '../../client/common/types'; @@ -72,11 +68,6 @@ suite('Multiroot Linting', () => { ); ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); ioc.serviceManager.addSingleton( IProductPathService, LinterProductPathService, diff --git a/src/test/linters/lint.test.ts b/src/test/linters/lint.test.ts index d2eef3c9e321..837830f0c499 100644 --- a/src/test/linters/lint.test.ts +++ b/src/test/linters/lint.test.ts @@ -6,11 +6,7 @@ import * as assert from 'assert'; import { ConfigurationTarget } from 'vscode'; import { Product } from '../../client/common/installer/productInstaller'; -import { - FormatterProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../client/common/installer/productPath'; +import { LinterProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; import { ProductService } from '../../client/common/installer/productService'; import { IProductPathService, IProductService } from '../../client/common/installer/types'; import { IConfigurationService, ILintingSettings, ProductType } from '../../client/common/types'; @@ -50,11 +46,6 @@ suite('Linting Settings', () => { configService = ioc.serviceContainer.get(IConfigurationService); linterManager = new LinterManager(configService); ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - FormatterProductPathService, - ProductType.Formatter, - ); ioc.serviceManager.addSingleton( IProductPathService, LinterProductPathService, diff --git a/src/test/providers/prompt/installFormatterPrompt.unit.test.ts b/src/test/providers/prompt/installFormatterPrompt.unit.test.ts deleted file mode 100644 index fbd3a72d8cef..000000000000 --- a/src/test/providers/prompt/installFormatterPrompt.unit.test.ts +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { WorkspaceConfiguration } from 'vscode'; -import { IPersistentState } from '../../../client/common/types'; -import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; -import * as windowApis from '../../../client/common/vscodeApis/windowApis'; -import * as extensionsApi from '../../../client/common/vscodeApis/extensionsApi'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { InstallFormatterPrompt } from '../../../client/providers/prompts/installFormatterPrompt'; -import * as promptUtils from '../../../client/providers/prompts/promptUtils'; -import { AUTOPEP8_EXTENSION, BLACK_EXTENSION, IInstallFormatterPrompt } from '../../../client/providers/prompts/types'; -import { Common, ToolsExtensions } from '../../../client/common/utils/localize'; - -suite('Formatter Extension prompt tests', () => { - let inFormatterExtensionExperimentStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let prompt: IInstallFormatterPrompt; - let serviceContainer: TypeMoq.IMock; - let persistState: TypeMoq.IMock>; - let getConfigurationStub: sinon.SinonStub; - let isExtensionEnabledStub: sinon.SinonStub; - let pythonConfig: TypeMoq.IMock; - let editorConfig: TypeMoq.IMock; - let showInformationMessageStub: sinon.SinonStub; - let installFormatterExtensionStub: sinon.SinonStub; - let updateDefaultFormatterStub: sinon.SinonStub; - - setup(() => { - inFormatterExtensionExperimentStub = sinon.stub(promptUtils, 'inFormatterExtensionExperiment'); - inFormatterExtensionExperimentStub.returns(true); - - doNotShowPromptStateStub = sinon.stub(promptUtils, 'doNotShowPromptState'); - persistState = TypeMoq.Mock.ofType>(); - doNotShowPromptStateStub.returns(persistState.object); - - getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); - pythonConfig = TypeMoq.Mock.ofType(); - editorConfig = TypeMoq.Mock.ofType(); - getConfigurationStub.callsFake((section: string) => { - if (section === 'python') { - return pythonConfig.object; - } - return editorConfig.object; - }); - isExtensionEnabledStub = sinon.stub(extensionsApi, 'isExtensionEnabled'); - showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); - installFormatterExtensionStub = sinon.stub(promptUtils, 'installFormatterExtension'); - updateDefaultFormatterStub = sinon.stub(promptUtils, 'updateDefaultFormatter'); - - serviceContainer = TypeMoq.Mock.ofType(); - - prompt = new InstallFormatterPrompt(serviceContainer.object); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Not in experiment', async () => { - inFormatterExtensionExperimentStub.returns(false); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(doNotShowPromptStateStub.notCalled); - }); - - test('Do not show was set', async () => { - persistState.setup((p) => p.value).returns(() => true); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(getConfigurationStub.notCalled); - }); - - test('Formatting provider is set to none', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'none'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Formatting provider is set to yapf', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'yapf'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Formatting provider is set to autopep8, and autopep8 extension is set as default formatter', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => AUTOPEP8_EXTENSION); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Formatting provider is set to black, and black extension is set as default formatter', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => BLACK_EXTENSION); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue(isExtensionEnabledStub.notCalled); - }); - - test('Prompt: user selects do not show', async () => { - persistState.setup((p) => p.value).returns(() => false); - persistState - .setup((p) => p.updateValue(true)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.atLeastOnce()); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves(Common.doNotShowAgain); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - persistState.verifyAll(); - }); - - test('Prompt (autopep8): user selects Autopep8', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt (autopep8): user selects Black', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installAutopep8FormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(BLACK_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt (black): user selects Autopep8', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installBlackFormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt (black): user selects Black', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns(undefined); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.installBlackFormatterPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - installFormatterExtensionStub.calledWith(BLACK_EXTENSION, undefined), - 'installFormatterExtension should be called', - ); - }); - - test('Prompt: Black and Autopep8 installed user selects Black as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns({}); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectMultipleFormattersPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(BLACK_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); - - test('Prompt: Black and Autopep8 installed user selects Autopep8 as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.returns({}); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectMultipleFormattersPrompt, - 'Black', - 'Autopep8', - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); - - test('Prompt: Black installed user selects Black as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'black'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.callsFake((extensionId) => { - if (extensionId === BLACK_EXTENSION) { - return {}; - } - return undefined; - }); - - showInformationMessageStub.resolves('Black'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectBlackFormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(BLACK_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); - - test('Prompt: Autopep8 installed user selects Autopep8 as default', async () => { - persistState.setup((p) => p.value).returns(() => false); - pythonConfig.setup((p) => p.get('formatting.provider', TypeMoq.It.isAny())).returns(() => 'autopep8'); - editorConfig.setup((p) => p.get('defaultFormatter', TypeMoq.It.isAny())).returns(() => ''); - isExtensionEnabledStub.callsFake((extensionId) => { - if (extensionId === AUTOPEP8_EXTENSION) { - return {}; - } - return undefined; - }); - - showInformationMessageStub.resolves('Autopep8'); - - await prompt.showInstallFormatterPrompt(); - assert.isTrue( - showInformationMessageStub.calledWith( - ToolsExtensions.selectAutopep8FormatterPrompt, - Common.bannerLabelYes, - Common.doNotShowAgain, - ), - 'showInformationMessage should be called', - ); - assert.isTrue( - updateDefaultFormatterStub.calledWith(AUTOPEP8_EXTENSION, undefined), - 'updateDefaultFormatter should be called', - ); - }); -}); diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index c20a84b1e25a..e7b11d2b745b 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -33,7 +33,6 @@ import { ITestOutputChannel, } from '../client/common/types'; import { registerTypes as variableRegisterTypes } from '../client/common/variables/serviceRegistry'; -import { registerTypes as formattersRegisterTypes } from '../client/formatters/serviceRegistry'; import { EnvironmentActivationService } from '../client/interpreter/activation/service'; import { IEnvironmentActivationService } from '../client/interpreter/activation/types'; import { @@ -147,10 +146,6 @@ export class IocContainer { lintersRegisterTypes(this.serviceManager); } - public registerFormatterTypes(): void { - formattersRegisterTypes(this.serviceManager); - } - public registerPlatformTypes(): void { platformRegisterTypes(this.serviceManager); } From 1dd8a4bdb16d0a7e79082c75d8eb55a142a48fc2 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 11 Oct 2023 13:44:13 -0700 Subject: [PATCH 0241/1136] switch testing output to test result panel (#22039) closes https://github.com/microsoft/vscode-python/issues/21861 and related issues --------- Co-authored-by: Courtney Webster <60238438+cwebster-99@users.noreply.github.com> --- .../tests/pytestadapter/.data/test_logging.py | 35 ++++++++ .../expected_execution_test_output.py | 28 ++++++ pythonFiles/tests/pytestadapter/helpers.py | 1 + .../tests/pytestadapter/test_execution.py | 27 +++--- pythonFiles/unittestadapter/execution.py | 2 - .../vscode_pytest/run_pytest_script.py | 2 - .../testController/common/resultResolver.ts | 48 ++++------ .../testing/testController/common/server.ts | 79 +++++++++------- .../testing/testController/common/utils.ts | 10 +++ .../pytest/pytestDiscoveryAdapter.ts | 19 +++- .../pytest/pytestExecutionAdapter.ts | 10 ++- .../testing/common/testingAdapter.test.ts | 90 ++++++++++++++++++- .../loggingWorkspace/test_logging.py | 13 +++ .../smallWorkspace/test_simple.py | 17 +++- 14 files changed, 298 insertions(+), 83 deletions(-) create mode 100644 pythonFiles/tests/pytestadapter/.data/test_logging.py create mode 100644 src/testTestingRootWkspc/loggingWorkspace/test_logging.py diff --git a/pythonFiles/tests/pytestadapter/.data/test_logging.py b/pythonFiles/tests/pytestadapter/.data/test_logging.py new file mode 100644 index 000000000000..058ad8075718 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/test_logging.py @@ -0,0 +1,35 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging +import sys + + +def test_logging2(caplog): + logger = logging.getLogger(__name__) + caplog.set_level(logging.DEBUG) # Set minimum log level to capture + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") + + # Printing to stdout and stderr + print("This is a stdout message.") + print("This is a stderr message.", file=sys.stderr) + assert False + + +def test_logging(caplog): + logger = logging.getLogger(__name__) + caplog.set_level(logging.DEBUG) # Set minimum log level to capture + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") + + # Printing to stdout and stderr + print("This is a stdout message.") + print("This is a stderr message.", file=sys.stderr) diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index 76d21b3e2518..3fdb7b45a0c0 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -596,3 +596,31 @@ "subtest": None, } } + + +# This is the expected output for the test logging file. +# └── test_logging.py +# └── test_logging2: failure +# └── test_logging: success +test_logging_path = TEST_DATA_PATH / "test_logging.py" + +logging_test_expected_execution_output = { + get_absolute_test_id("test_logging.py::test_logging2", test_logging_path): { + "test": get_absolute_test_id( + "test_logging.py::test_logging2", test_logging_path + ), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_logging.py::test_logging", test_logging_path): { + "test": get_absolute_test_id( + "test_logging.py::test_logging", test_logging_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index b534e950945a..2d36da59956b 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -129,6 +129,7 @@ def runner_with_cwd( "pytest", "-p", "vscode_pytest", + "-s", ] + args listener: socket.socket = create_server() _, port = listener.getsockname() diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index 37a392f66d4b..98698d8cdd7c 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -215,23 +215,30 @@ def test_bad_id_error_execution(): ], expected_execution_test_output.doctest_pytest_expected_execution_output, ), + ( + ["test_logging.py::test_logging2", "test_logging.py::test_logging"], + expected_execution_test_output.logging_test_expected_execution_output, + ), ], ) def test_pytest_execution(test_ids, expected_const): """ Test that pytest discovery works as expected where run pytest is always successful but the actual test results are both successes and failures.: - 1. uf_execution_expected_output: unittest tests run on multiple files. - 2. uf_single_file_expected_output: test run on a single file. - 3. uf_single_method_execution_expected_output: test run on a single method in a file. - 4. uf_non_adjacent_tests_execution_expected_output: test run on unittests in two files with single selection in test explorer. - 5. unit_pytest_same_file_execution_expected_output: test run on a file with both unittest and pytest tests. - 6. dual_level_nested_folder_execution_expected_output: test run on a file with one test file + 1: skip_tests_execution_expected_output: test run on a file with skipped tests. + 2. error_raised_exception_execution_expected_output: test run on a file that raises an exception. + 3. uf_execution_expected_output: unittest tests run on multiple files. + 4. uf_single_file_expected_output: test run on a single file. + 5. uf_single_method_execution_expected_output: test run on a single method in a file. + 6. uf_non_adjacent_tests_execution_expected_output: test run on unittests in two files with single selection in test explorer. + 7. unit_pytest_same_file_execution_expected_output: test run on a file with both unittest and pytest tests. + 8. dual_level_nested_folder_execution_expected_output: test run on a file with one test file at the top level and one test file in a nested folder. - 7. double_nested_folder_expected_execution_output: test run on a double nested folder. - 8. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. - 9. single_parametrize_tests_expected_execution_output: test run on single parametrize test. - 10. doctest_pytest_expected_execution_output: test run on doctest file. + 9. double_nested_folder_expected_execution_output: test run on a double nested folder. + 10. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. + 11. single_parametrize_tests_expected_execution_output: test run on single parametrize test. + 12. doctest_pytest_expected_execution_output: test run on doctest file. + 13. logging_test_expected_execution_output: test run on a file with logging. Keyword arguments: diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index 5f46bda95328..e5758118b951 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -293,8 +293,6 @@ def post_response( ) # Clear the buffer as complete JSON object is received buffer = b"" - - # Process the JSON data break except json.JSONDecodeError: # JSON decoding error, the complete JSON object is not yet received diff --git a/pythonFiles/vscode_pytest/run_pytest_script.py b/pythonFiles/vscode_pytest/run_pytest_script.py index 0fca8208a406..c3720c8ab8d0 100644 --- a/pythonFiles/vscode_pytest/run_pytest_script.py +++ b/pythonFiles/vscode_pytest/run_pytest_script.py @@ -51,8 +51,6 @@ ) # Clear the buffer as complete JSON object is received buffer = b"" - - # Process the JSON data print("Received JSON data in run script") break except json.JSONDecodeError: diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index cf757d77243d..aaf82b143823 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -20,7 +20,7 @@ import { clearAllChildren, createErrorTestItem, getTestCaseNodes } from './testI import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { splitLines } from '../../../common/stringUtils'; -import { buildErrorNodeOptions, fixLogLines, populateTestTree, splitTestNameWithRegex } from './utils'; +import { buildErrorNodeOptions, populateTestTree, splitTestNameWithRegex } from './utils'; import { Deferred } from '../../../common/utils/async'; export class PythonResultResolver implements ITestResultResolver { @@ -151,15 +151,16 @@ export class PythonResultResolver implements ITestResultResolver { const tempArr: TestItem[] = getTestCaseNodes(i); testCases.push(...tempArr); }); + const testItem = rawTestExecData.result[keyTemp]; - if (rawTestExecData.result[keyTemp].outcome === 'error') { - const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; + if (testItem.outcome === 'error') { + const rawTraceback = testItem.traceback ?? ''; const traceback = splitLines(rawTraceback, { trim: false, removeEmptyEntries: true, }).join('\r\n'); - const text = `${rawTestExecData.result[keyTemp].test} failed with error: ${ - rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome + const text = `${testItem.test} failed with error: ${ + testItem.message ?? testItem.outcome }\r\n${traceback}\r\n`; const message = new TestMessage(text); @@ -170,23 +171,17 @@ export class PythonResultResolver implements ITestResultResolver { if (indiItem.uri && indiItem.range) { message.location = new Location(indiItem.uri, indiItem.range); runInstance.errored(indiItem, message); - runInstance.appendOutput(fixLogLines(text)); } } }); - } else if ( - rawTestExecData.result[keyTemp].outcome === 'failure' || - rawTestExecData.result[keyTemp].outcome === 'passed-unexpected' - ) { - const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? ''; + } else if (testItem.outcome === 'failure' || testItem.outcome === 'passed-unexpected') { + const rawTraceback = testItem.traceback ?? ''; const traceback = splitLines(rawTraceback, { trim: false, removeEmptyEntries: true, }).join('\r\n'); - const text = `${rawTestExecData.result[keyTemp].test} failed: ${ - rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome - }\r\n${traceback}\r\n`; + const text = `${testItem.test} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}\r\n`; const message = new TestMessage(text); // note that keyTemp is a runId for unittest library... @@ -197,14 +192,10 @@ export class PythonResultResolver implements ITestResultResolver { if (indiItem.uri && indiItem.range) { message.location = new Location(indiItem.uri, indiItem.range); runInstance.failed(indiItem, message); - runInstance.appendOutput(fixLogLines(text)); } } }); - } else if ( - rawTestExecData.result[keyTemp].outcome === 'success' || - rawTestExecData.result[keyTemp].outcome === 'expected-failure' - ) { + } else if (testItem.outcome === 'success' || testItem.outcome === 'expected-failure') { const grabTestItem = this.runIdToTestItem.get(keyTemp); const grabVSid = this.runIdToVSid.get(keyTemp); if (grabTestItem !== undefined) { @@ -216,7 +207,7 @@ export class PythonResultResolver implements ITestResultResolver { } }); } - } else if (rawTestExecData.result[keyTemp].outcome === 'skipped') { + } else if (testItem.outcome === 'skipped') { const grabTestItem = this.runIdToTestItem.get(keyTemp); const grabVSid = this.runIdToVSid.get(keyTemp); if (grabTestItem !== undefined) { @@ -228,11 +219,11 @@ export class PythonResultResolver implements ITestResultResolver { } }); } - } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') { + } else if (testItem.outcome === 'subtest-failure') { // split on [] or () based on how the subtest is setup. const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); - const data = rawTestExecData.result[keyTemp]; + const data = testItem; // find the subtest's parent test item if (parentTestItem) { const subtestStats = this.subTestStats.get(parentTestCaseId); @@ -243,20 +234,19 @@ export class PythonResultResolver implements ITestResultResolver { failed: 1, passed: 0, }); - runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); } const subTestItem = this.testController?.createTestItem(subtestId, subtestId); - runInstance.appendOutput(fixLogLines(`${subtestId} Failed\r\n`)); // create a new test item for the subtest if (subTestItem) { const traceback = data.traceback ?? ''; - const text = `${data.subtest} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`; - runInstance.appendOutput(fixLogLines(text)); + const text = `${data.subtest} failed: ${ + testItem.message ?? testItem.outcome + }\r\n${traceback}\r\n`; parentTestItem.children.add(subTestItem); runInstance.started(subTestItem); - const message = new TestMessage(rawTestExecData?.result[keyTemp].message ?? ''); + const message = new TestMessage(text); if (parentTestItem.uri && parentTestItem.range) { message.location = new Location(parentTestItem.uri, parentTestItem.range); } @@ -267,7 +257,7 @@ export class PythonResultResolver implements ITestResultResolver { } else { throw new Error('Parent test item not found'); } - } else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') { + } else if (testItem.outcome === 'subtest-success') { // split on [] or () based on how the subtest is setup. const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); @@ -279,7 +269,6 @@ export class PythonResultResolver implements ITestResultResolver { subtestStats.passed += 1; } else { this.subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); - runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`)); // clear since subtest items don't persist between runs clearAllChildren(parentTestItem); } @@ -289,7 +278,6 @@ export class PythonResultResolver implements ITestResultResolver { parentTestItem.children.add(subTestItem); runInstance.started(subTestItem); runInstance.passed(subTestItem); - runInstance.appendOutput(fixLogLines(`${subtestId} Passed\r\n`)); } else { throw new Error('Unable to create new child node for subtest'); } diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 50ae1f3f7536..e496860526e4 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -17,10 +17,12 @@ import { DataReceivedEvent, ITestServer, TestCommandOptions } from './types'; import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; import { + MESSAGE_ON_TESTING_OUTPUT_MOVE, createDiscoveryErrorPayload, createEOTPayload, createExecutionErrorPayload, extractJsonPayload, + fixLogLinesNoTrailing, } from './utils'; import { createDeferred } from '../../../common/utils/async'; import { EnvironmentVariables } from '../../../api/types'; @@ -173,7 +175,7 @@ export class PythonTestServer implements ITestServer, Disposable { callback?: () => void, ): Promise { const { uuid } = options; - // get and edit env vars + const isDiscovery = (testIds === undefined || testIds.length === 0) && runTestIdPort === undefined; const mutableEnv = { ...env }; const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; const pythonPathCommand = [options.cwd, ...pythonPathParts].join(path.delimiter); @@ -196,7 +198,6 @@ export class PythonTestServer implements ITestServer, Disposable { resource: options.workspaceFolder, }; const execService = await this.executionFactory.createActivatedEnvironment(creationOptions); - const args = [options.command.script].concat(options.command.args); if (options.outChannel) { @@ -244,23 +245,55 @@ export class PythonTestServer implements ITestServer, Disposable { const result = execService?.execObservable(args, spawnOptions); resultProc = result?.proc; - // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. - result?.proc?.stdout?.on('data', (data) => { - spawnOptions?.outputChannel?.append(data.toString()); - }); - result?.proc?.stderr?.on('data', (data) => { - spawnOptions?.outputChannel?.append(data.toString()); - }); - result?.proc?.on('exit', (code, signal) => { - if (code !== 0) { - traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}`); - } - }); + // TODO: after a release, remove discovery output from the "Python Test Log" channel and send it to the "Python" channel instead. + // TODO: after a release, remove run output from the "Python Test Log" channel and send it to the "Test Result" channel instead. + if (isDiscovery) { + result?.proc?.stdout?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + spawnOptions?.outputChannel?.append(`${out}`); + traceInfo(out); + }); + result?.proc?.stderr?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + spawnOptions?.outputChannel?.append(`${out}`); + traceError(out); + }); + } else { + result?.proc?.stdout?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + runInstance?.appendOutput(`${out}`); + spawnOptions?.outputChannel?.append(out); + }); + result?.proc?.stderr?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + runInstance?.appendOutput(`${out}`); + spawnOptions?.outputChannel?.append(out); + }); + } result?.proc?.on('exit', (code, signal) => { // if the child has testIds then this is a run request - if (code !== 0 && testIds && testIds?.length !== 0) { + spawnOptions?.outputChannel?.append(MESSAGE_ON_TESTING_OUTPUT_MOVE); + if (isDiscovery) { + if (code !== 0) { + // This occurs when we are running discovery + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, + ); + this._onDiscoveryDataReceived.fire({ + uuid, + data: JSON.stringify(createDiscoveryErrorPayload(code, signal, options.cwd)), + }); + // then send a EOT payload + this._onDiscoveryDataReceived.fire({ + uuid, + data: JSON.stringify(createEOTPayload(true)), + }); + } + } else if (code !== 0 && testIds) { + // This occurs when we are running the test and there is an error which occurs. + traceError( `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error execution payload`, ); @@ -274,22 +307,8 @@ export class PythonTestServer implements ITestServer, Disposable { uuid, data: JSON.stringify(createEOTPayload(true)), }); - } else if (code !== 0) { - // This occurs when we are running discovery - traceError( - `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, - ); - this._onDiscoveryDataReceived.fire({ - uuid, - data: JSON.stringify(createDiscoveryErrorPayload(code, signal, options.cwd)), - }); - // then send a EOT payload - this._onDiscoveryDataReceived.fire({ - uuid, - data: JSON.stringify(createEOTPayload(true)), - }); } - deferredTillExecClose.resolve({ stdout: '', stderr: '' }); + deferredTillExecClose.resolve(); }); await deferredTillExecClose.promise; } diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index f5f416529c42..5022fa5a44e6 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -23,6 +23,11 @@ export function fixLogLines(content: string): string { const lines = content.split(/\r?\n/g); return `${lines.join('\r\n')}\r\n`; } + +export function fixLogLinesNoTrailing(content: string): string { + const lines = content.split(/\r?\n/g); + return `${lines.join('\r\n')}`; +} export interface IJSONRPCData { extractedJSON: string; remainingRawData: string; @@ -42,6 +47,11 @@ export interface ExtractOutput { export const JSONRPC_UUID_HEADER = 'Request-uuid'; export const JSONRPC_CONTENT_LENGTH_HEADER = 'Content-Length'; export const JSONRPC_CONTENT_TYPE_HEADER = 'Content-Type'; +export const MESSAGE_ON_TESTING_OUTPUT_MOVE = + 'Starting now, all test run output will be sent to the Test Result panel,' + + ' while test discovery output will be sent to the "Python" output channel instead of the "Python Test Log" channel.' + + ' The "Python Test Log" channel will be deprecated within the next month.' + + 'See https://github.com/microsoft/vscode-python/wiki/New-Method-for-Output-Handling-in-Python-Testing for details.'; export function createTestingDeferred(): Deferred { return createDeferred(); diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 4ed2570ba7cc..92bd9f04834e 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -18,7 +18,13 @@ import { ITestResultResolver, ITestServer, } from '../common/types'; -import { createDiscoveryErrorPayload, createEOTPayload, createTestingDeferred } from '../common/utils'; +import { + MESSAGE_ON_TESTING_OUTPUT_MOVE, + createDiscoveryErrorPayload, + createEOTPayload, + createTestingDeferred, + fixLogLinesNoTrailing, +} from '../common/utils'; import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; /** @@ -95,13 +101,20 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + // TODO: after a release, remove discovery output from the "Python Test Log" channel and send it to the "Python" channel instead. + result?.proc?.stdout?.on('data', (data) => { - spawnOptions.outputChannel?.append(data.toString()); + const out = fixLogLinesNoTrailing(data.toString()); + traceInfo(out); + spawnOptions?.outputChannel?.append(`${out}`); }); result?.proc?.stderr?.on('data', (data) => { - spawnOptions.outputChannel?.append(data.toString()); + const out = fixLogLinesNoTrailing(data.toString()); + traceError(out); + spawnOptions?.outputChannel?.append(`${out}`); }); result?.proc?.on('exit', (code, signal) => { + this.outputChannel?.append(MESSAGE_ON_TESTING_OUTPUT_MOVE); if (code !== 0) { traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}.`); } diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index eb8e9b6f935a..5c04aabab845 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -190,13 +190,19 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + // TODO: after a release, remove run output from the "Python Test Log" channel and send it to the "Test Result" channel instead. result?.proc?.stdout?.on('data', (data) => { - this.outputChannel?.append(data.toString()); + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance?.appendOutput(out); + this.outputChannel?.append(out); }); result?.proc?.stderr?.on('data', (data) => { - this.outputChannel?.append(data.toString()); + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance?.appendOutput(out); + this.outputChannel?.append(out); }); result?.proc?.on('exit', (code, signal) => { + this.outputChannel?.append(utils.MESSAGE_ON_TESTING_OUTPUT_MOVE); if (code !== 0 && testIds) { traceError(`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}.`); } diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 3b5ef0062a98..519a60e3f0f7 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -275,7 +275,7 @@ suite('End to End Tests: test adapters', () => { assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); }); }); - test('unittest execution adapter small workspace', async () => { + test('unittest execution adapter small workspace with correct output', async () => { // result resolver and saved data for assertions resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); let callCount = 0; @@ -319,12 +319,34 @@ suite('End to End Tests: test adapters', () => { onCancellationRequested: () => undefined, } as any), ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); await executionAdapter .runTests(workspaceUri, ['test_simple.SimpleClass.test_simple_unit'], false, testRun.object) .finally(() => { // verify that the _resolveExecution was called once per test assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for stdout and stderr as well as unittest output + assert.ok( + collectedOutput.includes('expected printed output, stdout'), + 'The test string does not contain the expected stdout output.', + ); + assert.ok( + collectedOutput.includes('expected printed output, stderr'), + 'The test string does not contain the expected stderr output.', + ); + assert.ok( + collectedOutput.includes('Ran 1 test in'), + 'The test string does not contain the expected unittest output.', + ); }); }); test('unittest execution adapter large workspace', async () => { @@ -372,15 +394,33 @@ suite('End to End Tests: test adapters', () => { onCancellationRequested: () => undefined, } as any), ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); await executionAdapter .runTests(workspaceUri, ['test_parameterized_subtest.NumbersTest.test_even'], false, testRun.object) .then(() => { // verify that the _resolveExecution was called once per test assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output + assert.ok( + collectedOutput.includes('test_parameterized_subtest.py'), + 'The test string does not contain the correct test name which should be printed', + ); + assert.ok( + collectedOutput.includes('FAILED (failures=1000)'), + 'The test string does not contain the last of the unittest output', + ); }); }); - test('pytest execution adapter small workspace', async () => { + test('pytest execution adapter small workspace with correct output', async () => { // result resolver and saved data for assertions resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); let callCount = 0; @@ -423,6 +463,14 @@ suite('End to End Tests: test adapters', () => { onCancellationRequested: () => undefined, } as any), ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); await executionAdapter .runTests( workspaceUri, @@ -435,6 +483,30 @@ suite('End to End Tests: test adapters', () => { // verify that the _resolveExecution was called once per test assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for stdout and stderr as well as pytest output + assert.ok( + collectedOutput.includes('test session starts'), + 'The test string does not contain the expected stdout output.', + ); + assert.ok( + collectedOutput.includes('Captured log call'), + 'The test string does not contain the expected log section.', + ); + const searchStrings = [ + 'This is a warning message.', + 'This is an error message.', + 'This is a critical message.', + ]; + let searchString: string; + for (searchString of searchStrings) { + const count: number = (collectedOutput.match(new RegExp(searchString, 'g')) || []).length; + assert.strictEqual( + count, + 2, + `The test string does not contain two instances of ${searchString}. Should appear twice from logging output and stack trace`, + ); + } }); }); test('pytest execution adapter large workspace', async () => { @@ -488,10 +560,24 @@ suite('End to End Tests: test adapters', () => { onCancellationRequested: () => undefined, } as any), ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); await executionAdapter.runTests(workspaceUri, testIds, false, testRun.object, pythonExecFactory).then(() => { // verify that the _resolveExecution was called once per test assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for large repo + assert.ok( + collectedOutput.includes('test session starts'), + 'The test string does not contain the expected stdout output from pytest.', + ); }); }); test('unittest discovery adapter seg fault error handling', async () => { diff --git a/src/testTestingRootWkspc/loggingWorkspace/test_logging.py b/src/testTestingRootWkspc/loggingWorkspace/test_logging.py new file mode 100644 index 000000000000..a3e77f06ae78 --- /dev/null +++ b/src/testTestingRootWkspc/loggingWorkspace/test_logging.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +import logging + + +def test_logging(caplog): + logger = logging.getLogger(__name__) + caplog.set_level(logging.DEBUG) # Set minimum log level to capture + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") diff --git a/src/testTestingRootWkspc/smallWorkspace/test_simple.py b/src/testTestingRootWkspc/smallWorkspace/test_simple.py index 6b4f7bd2f8a6..f68a0d7d0d93 100644 --- a/src/testTestingRootWkspc/smallWorkspace/test_simple.py +++ b/src/testTestingRootWkspc/smallWorkspace/test_simple.py @@ -1,12 +1,25 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import unittest +import logging +import sys -def test_a(): - assert 1 == 1 +def test_a(caplog): + logger = logging.getLogger(__name__) + # caplog.set_level(logging.ERROR) # Set minimum log level to capture + logger.setLevel(logging.WARN) + + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.warning("This is a warning message.") + logger.error("This is an error message.") + logger.critical("This is a critical message.") + assert False class SimpleClass(unittest.TestCase): def test_simple_unit(self): + print("expected printed output, stdout") + print("expected printed output, stderr", file=sys.stderr) assert True From d1e4562b64e38045f549ca00025c4620a6a89567 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 12 Oct 2023 07:55:14 +1100 Subject: [PATCH 0242/1136] Move tensorboard support into a separate extension (#22197) * No need of experiments (if users install extension, then it works) * If tensorboard extension is installed the we rely on tensorboard extension to handle everything * For final deplayment we can decide whether to just remove this feature altogether or prompt users to install tensorboard extension or to go with an experiment, for now I wanted to keep this super simple (this shoudl not affect anyone as no one will have a tensorboard extension except us) * Simple private API for tensorboard extension, untill Python ext exposes a stable API * API is similar to Jupyter, scoped to Tensorboard ext --- package.json | 4 +- src/client/api.ts | 17 +++ src/client/common/constants.ts | 1 + .../nbextensionCodeLensProvider.ts | 4 + src/client/tensorBoard/serviceRegistry.ts | 2 + .../tensorBoard/tensorBoardFileWatcher.ts | 4 + .../tensorBoardImportCodeLensProvider.ts | 4 + src/client/tensorBoard/tensorBoardPrompt.ts | 2 +- src/client/tensorBoard/tensorBoardSession.ts | 9 +- .../tensorBoard/tensorBoardSessionProvider.ts | 7 +- .../tensorBoard/tensorBoardUsageTracker.ts | 4 + .../tensorBoard/tensorboarExperiment.ts | 8 ++ .../tensorboardDependencyChecker.ts | 60 +++++++++++ .../tensorBoard/tensorboardIntegration.ts | 102 ++++++++++++++++++ src/client/tensorBoard/terminalWatcher.ts | 4 + src/client/tensorBoard/types.ts | 7 +- .../tensorBoardUsageTracker.unit.test.ts | 7 ++ src/test/vscode-mock.ts | 43 ++++---- 18 files changed, 256 insertions(+), 33 deletions(-) create mode 100644 src/client/tensorBoard/tensorboarExperiment.ts create mode 100644 src/client/tensorBoard/tensorboardDependencyChecker.ts create mode 100644 src/client/tensorBoard/tensorboardIntegration.ts diff --git a/package.json b/package.json index 490e615f26f2..88578a687853 100644 --- a/package.json +++ b/package.json @@ -1836,7 +1836,7 @@ "category": "Python", "command": "python.launchTensorBoard", "title": "%python.command.python.launchTensorBoard.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && !python.tensorboardExtInstalled" }, { "category": "Python", @@ -1844,7 +1844,7 @@ "enablement": "python.hasActiveTensorBoardSession", "icon": "$(refresh)", "title": "%python.command.python.refreshTensorBoard.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && !python.tensorboardExtInstalled" }, { "category": "Python", diff --git a/src/client/api.ts b/src/client/api.ts index 23b2553c93d2..81a5f676cc22 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -21,6 +21,7 @@ import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { buildEnvironmentApi } from './environmentApi'; import { ApiForPylance } from './pylanceApi'; import { getTelemetryReporter } from './telemetry'; +import { TensorboardExtensionIntegration } from './tensorBoard/tensorboardIntegration'; export function buildApi( ready: Promise, @@ -31,7 +32,14 @@ export function buildApi( const configurationService = serviceContainer.get(IConfigurationService); const interpreterService = serviceContainer.get(IInterpreterService); serviceManager.addSingleton(JupyterExtensionIntegration, JupyterExtensionIntegration); + serviceManager.addSingleton( + TensorboardExtensionIntegration, + TensorboardExtensionIntegration, + ); const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); + const tensorboardIntegration = serviceContainer.get( + TensorboardExtensionIntegration, + ); const outputChannel = serviceContainer.get(ILanguageServerOutputChannel); const api: PythonExtension & { @@ -41,6 +49,12 @@ export function buildApi( jupyter: { registerHooks(): void; }; + /** + * Internal API just for Tensorboard, hence don't include in the official types. + */ + tensorboard: { + registerHooks(): void; + }; } & { /** * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an @@ -92,6 +106,9 @@ export function buildApi( jupyter: { registerHooks: () => jupyterIntegration.integrateWithJupyterExtension(), }, + tensorboard: { + registerHooks: () => tensorboardIntegration.integrateWithTensorboardExtension(), + }, debug: { async getRemoteLauncherCommand( host: string, diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index cd6d305f624a..6fc743fb8a0a 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -23,6 +23,7 @@ export const PYTHON_NOTEBOOKS = [ export const PVSC_EXTENSION_ID = 'ms-python.python'; export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance'; export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter'; +export const TENSORBOARD_EXTENSION_ID = 'ms-toolsai.tensorboard'; export const AppinsightsKey = '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255'; export type Channel = 'stable' | 'insiders'; diff --git a/src/client/tensorBoard/nbextensionCodeLensProvider.ts b/src/client/tensorBoard/nbextensionCodeLensProvider.ts index 6d4c844cd392..7b9a116ee144 100644 --- a/src/client/tensorBoard/nbextensionCodeLensProvider.ts +++ b/src/client/tensorBoard/nbextensionCodeLensProvider.ts @@ -12,6 +12,7 @@ import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { containsNotebookExtension } from './helpers'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; @injectable() export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleActivationService { @@ -27,6 +28,9 @@ export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleA constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } this.activateInternal().ignoreErrors(); } diff --git a/src/client/tensorBoard/serviceRegistry.ts b/src/client/tensorBoard/serviceRegistry.ts index 8d16766f70c5..dd193f528eea 100644 --- a/src/client/tensorBoard/serviceRegistry.ts +++ b/src/client/tensorBoard/serviceRegistry.ts @@ -10,6 +10,7 @@ import { TensorBoardPrompt } from './tensorBoardPrompt'; import { TensorBoardSessionProvider } from './tensorBoardSessionProvider'; import { TensorBoardNbextensionCodeLensProvider } from './nbextensionCodeLensProvider'; import { TerminalWatcher } from './terminalWatcher'; +import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(TensorBoardSessionProvider, TensorBoardSessionProvider); @@ -32,4 +33,5 @@ export function registerTypes(serviceManager: IServiceManager): void { ); serviceManager.addBinding(TensorBoardNbextensionCodeLensProvider, IExtensionSingleActivationService); serviceManager.addSingleton(IExtensionSingleActivationService, TerminalWatcher); + serviceManager.addSingleton(TensorboardDependencyChecker, TensorboardDependencyChecker); } diff --git a/src/client/tensorBoard/tensorBoardFileWatcher.ts b/src/client/tensorBoard/tensorBoardFileWatcher.ts index 81c62f1f8de3..dccdb95290ec 100644 --- a/src/client/tensorBoard/tensorBoardFileWatcher.ts +++ b/src/client/tensorBoard/tensorBoardFileWatcher.ts @@ -8,6 +8,7 @@ import { IWorkspaceService } from '../common/application/types'; import { IDisposableRegistry } from '../common/types'; import { TensorBoardEntrypointTrigger } from './constants'; import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; @injectable() export class TensorBoardFileWatcher implements IExtensionSingleActivationService { @@ -24,6 +25,9 @@ export class TensorBoardFileWatcher implements IExtensionSingleActivationService ) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } this.activateInternal().ignoreErrors(); } diff --git a/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts b/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts index cac29b1d7e7a..d6dc8d7e82e5 100644 --- a/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts +++ b/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts @@ -12,6 +12,7 @@ import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { containsTensorBoardImport } from './helpers'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; @injectable() export class TensorBoardImportCodeLensProvider implements IExtensionSingleActivationService { @@ -27,6 +28,9 @@ export class TensorBoardImportCodeLensProvider implements IExtensionSingleActiva constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } this.activateInternal().ignoreErrors(); } diff --git a/src/client/tensorBoard/tensorBoardPrompt.ts b/src/client/tensorBoard/tensorBoardPrompt.ts index 1c03a696dc1d..d42101cb51d6 100644 --- a/src/client/tensorBoard/tensorBoardPrompt.ts +++ b/src/client/tensorBoard/tensorBoardPrompt.ts @@ -84,7 +84,7 @@ export class TensorBoardPrompt { } } - private isPromptEnabled(): boolean { + public isPromptEnabled(): boolean { return this.state.value; } diff --git a/src/client/tensorBoard/tensorBoardSession.ts b/src/client/tensorBoard/tensorBoardSession.ts index 1d24e8c313f7..fb54ad6f32e6 100644 --- a/src/client/tensorBoard/tensorBoardSession.ts +++ b/src/client/tensorBoard/tensorBoardSession.ts @@ -100,7 +100,10 @@ export class TensorBoardSession { private readonly globalMemento: IPersistentState, private readonly multiStepFactory: IMultiStepInputFactory, private readonly configurationService: IConfigurationService, - ) {} + ) { + this.disposables.push(this.onDidChangeViewStateEventEmitter); + this.disposables.push(this.onDidDisposeEventEmitter); + } public get onDidDispose(): Event { return this.onDidDisposeEventEmitter.event; @@ -189,10 +192,10 @@ export class TensorBoardSession { // to start a TensorBoard session. If the user has a torch import in // any of their open documents, also try to install the torch-tb-plugin // package, but don't block if installing that fails. - private async ensurePrerequisitesAreInstalled() { + public async ensurePrerequisitesAreInstalled(resource?: Uri): Promise { traceVerbose('Ensuring TensorBoard package is installed into active interpreter'); const interpreter = - (await this.interpreterService.getActiveInterpreter()) || + (await this.interpreterService.getActiveInterpreter(resource)) || (await this.commandManager.executeCommand('python.setInterpreter')); if (!interpreter) { return false; diff --git a/src/client/tensorBoard/tensorBoardSessionProvider.ts b/src/client/tensorBoard/tensorBoardSessionProvider.ts index 53878bd543c2..c81059654075 100644 --- a/src/client/tensorBoard/tensorBoardSessionProvider.ts +++ b/src/client/tensorBoard/tensorBoardSessionProvider.ts @@ -22,8 +22,9 @@ import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { TensorBoardSession } from './tensorBoardSession'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; -const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup'; +export const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup'; @injectable() export class TensorBoardSessionProvider implements IExtensionSingleActivationService { @@ -58,6 +59,10 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer } public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } + this.disposables.push( this.commandManager.registerCommand( Commands.LaunchTensorBoard, diff --git a/src/client/tensorBoard/tensorBoardUsageTracker.ts b/src/client/tensorBoard/tensorBoardUsageTracker.ts index 99d82949dcfd..7c8ea7b00961 100644 --- a/src/client/tensorBoard/tensorBoardUsageTracker.ts +++ b/src/client/tensorBoard/tensorBoardUsageTracker.ts @@ -12,6 +12,7 @@ import { getDocumentLines } from '../telemetry/importTracker'; import { TensorBoardEntrypointTrigger } from './constants'; import { containsTensorBoardImport } from './helpers'; import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; const testExecution = isTestExecution(); @@ -28,6 +29,9 @@ export class TensorBoardUsageTracker implements IExtensionSingleActivationServic ) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } if (testExecution) { await this.activateInternal(); } else { diff --git a/src/client/tensorBoard/tensorboarExperiment.ts b/src/client/tensorBoard/tensorboarExperiment.ts new file mode 100644 index 000000000000..25eac8db71da --- /dev/null +++ b/src/client/tensorBoard/tensorboarExperiment.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { extensions } from 'vscode'; + +export function useNewTensorboardExtension(): boolean { + return !!extensions.getExtension('ms-toolsai.tensorboard'); +} diff --git a/src/client/tensorBoard/tensorboardDependencyChecker.ts b/src/client/tensorBoard/tensorboardDependencyChecker.ts new file mode 100644 index 000000000000..5c377e1d2455 --- /dev/null +++ b/src/client/tensorBoard/tensorboardDependencyChecker.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri, ViewColumn } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { IPythonExecutionFactory } from '../common/process/types'; +import { + IInstaller, + IPersistentState, + IPersistentStateFactory, + IConfigurationService, + IDisposable, +} from '../common/types'; +import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; +import { IInterpreterService } from '../interpreter/contracts'; +import { TensorBoardSession } from './tensorBoardSession'; +import { disposeAll } from '../common/utils/resourceLifecycle'; +import { PREFERRED_VIEWGROUP } from './tensorBoardSessionProvider'; + +@injectable() +export class TensorboardDependencyChecker { + private preferredViewGroupMemento: IPersistentState; + + constructor( + @inject(IInstaller) private readonly installer: IInstaller, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, + @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, + @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + ) { + this.preferredViewGroupMemento = this.stateFactory.createGlobalPersistentState( + PREFERRED_VIEWGROUP, + ViewColumn.Active, + ); + } + + public async ensureDependenciesAreInstalled(resource?: Uri): Promise { + const disposables: IDisposable[] = []; + const newSession = new TensorBoardSession( + this.installer, + this.interpreterService, + this.workspaceService, + this.pythonExecFactory, + this.commandManager, + disposables, + this.applicationShell, + this.preferredViewGroupMemento, + this.multiStepFactory, + this.configurationService, + ); + const result = await newSession.ensurePrerequisitesAreInstalled(resource); + disposeAll(disposables); + return result; + } +} diff --git a/src/client/tensorBoard/tensorboardIntegration.ts b/src/client/tensorBoard/tensorboardIntegration.ts new file mode 100644 index 000000000000..74f69afab84f --- /dev/null +++ b/src/client/tensorBoard/tensorboardIntegration.ts @@ -0,0 +1,102 @@ +/* eslint-disable comma-dangle */ + +/* eslint-disable implicit-arrow-linebreak */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Extension, Uri, commands } from 'vscode'; +import { IWorkspaceService } from '../common/application/types'; +import { TENSORBOARD_EXTENSION_ID } from '../common/constants'; +import { IDisposableRegistry, IExtensions, Resource } from '../common/types'; +import { IEnvironmentActivationService } from '../interpreter/activation/types'; +import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; + +type PythonApiForTensorboardExtension = { + /** + * Gets activated env vars for the active Python Environment for the given resource. + */ + getActivatedEnvironmentVariables(resource: Resource): Promise; + /** + * Ensures that the dependencies required for TensorBoard are installed in Active Environment for the given resource. + */ + ensureDependenciesAreInstalled(resource?: Uri): Promise; + /** + * Whether to allow displaying tensorboard prompt. + */ + isPromptEnabled(): boolean; +}; + +type TensorboardExtensionApi = { + /** + * Registers python extension specific parts with the tensorboard extension + */ + registerPythonApi(interpreterService: PythonApiForTensorboardExtension): void; +}; + +@injectable() +export class TensorboardExtensionIntegration { + private tensorboardExtension: Extension | undefined; + + constructor( + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(TensorboardDependencyChecker) private readonly dependencyChcker: TensorboardDependencyChecker, + @inject(TensorBoardPrompt) private readonly tensorBoardPrompt: TensorBoardPrompt, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + ) { + this.hideCommands(); + extensions.onDidChange(this.hideCommands, this, disposables); + } + + public registerApi(tensorboardExtensionApi: TensorboardExtensionApi): TensorboardExtensionApi | undefined { + this.hideCommands(); + if (!this.workspaceService.isTrusted) { + this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(tensorboardExtensionApi)); + return undefined; + } + tensorboardExtensionApi.registerPythonApi({ + getActivatedEnvironmentVariables: async (resource: Resource) => + this.envActivation.getActivatedEnvironmentVariables(resource, undefined, true), + ensureDependenciesAreInstalled: async (resource?: Uri): Promise => + this.dependencyChcker.ensureDependenciesAreInstalled(resource), + isPromptEnabled: () => this.tensorBoardPrompt.isPromptEnabled(), + }); + return undefined; + } + + public hideCommands(): void { + if (this.extensions.getExtension(TENSORBOARD_EXTENSION_ID)) { + console.error('TensorBoard extension is installed'); + void commands.executeCommand('setContext', 'python.tensorboardExtInstalled', true); + } else { + console.error('TensorBoard extension not installed'); + } + } + + public async integrateWithTensorboardExtension(): Promise { + const api = await this.getExtensionApi(); + if (api) { + this.registerApi(api); + } + } + + private async getExtensionApi(): Promise { + if (!this.tensorboardExtension) { + const extension = this.extensions.getExtension(TENSORBOARD_EXTENSION_ID); + if (!extension) { + return undefined; + } + await extension.activate(); + if (extension.isActive) { + this.tensorboardExtension = extension; + return this.tensorboardExtension.exports; + } + } else { + return this.tensorboardExtension.exports; + } + return undefined; + } +} diff --git a/src/client/tensorBoard/terminalWatcher.ts b/src/client/tensorBoard/terminalWatcher.ts index 5aadc12dc4c0..30ccf7e1726a 100644 --- a/src/client/tensorBoard/terminalWatcher.ts +++ b/src/client/tensorBoard/terminalWatcher.ts @@ -4,6 +4,7 @@ import { IExtensionSingleActivationService } from '../activation/types'; import { IDisposable, IDisposableRegistry } from '../common/types'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; // Every 5 min look, through active terminals to see if any are running `tensorboard` @injectable() @@ -15,6 +16,9 @@ export class TerminalWatcher implements IExtensionSingleActivationService, IDisp constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } const handle = setInterval(() => { // When user runs a command in VSCode terminal, the terminal's name // becomes the program that is currently running. Since tensorboard diff --git a/src/client/tensorBoard/types.ts b/src/client/tensorBoard/types.ts index 6e2c274d63f4..a11659015da8 100644 --- a/src/client/tensorBoard/types.ts +++ b/src/client/tensorBoard/types.ts @@ -1,9 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Event } from 'vscode'; +import { Event, Uri } from 'vscode'; export const ITensorBoardImportTracker = Symbol('ITensorBoardImportTracker'); export interface ITensorBoardImportTracker { onDidImportTensorBoard: Event; } + +export const ITensorboardDependencyChecker = Symbol('ITensorboardDependencyChecker'); +export interface ITensorboardDependencyChecker { + ensureDependenciesAreInstalled(resource?: Uri): Promise; +} diff --git a/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts b/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts index b6efad083a57..ff187dd2afc1 100644 --- a/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts +++ b/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts @@ -1,9 +1,11 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; +import { anything, reset, when } from 'ts-mockito'; import { TensorBoardUsageTracker } from '../../client/tensorBoard/tensorBoardUsageTracker'; import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; import { MockDocumentManager } from '../mocks/mockDocumentManager'; import { createTensorBoardPromptWithMocks } from './helpers'; +import { mockedVSCodeNamespaces } from '../vscode-mock'; suite('TensorBoard usage tracker', () => { let documentManager: MockDocumentManager; @@ -11,6 +13,11 @@ suite('TensorBoard usage tracker', () => { let prompt: TensorBoardPrompt; let showNativeTensorBoardPrompt: sinon.SinonSpy; + suiteSetup(() => { + reset(mockedVSCodeNamespaces.extensions); + when(mockedVSCodeNamespaces.extensions?.getExtension(anything())).thenReturn(undefined); + }); + suiteTeardown(() => reset(mockedVSCodeNamespaces.extensions)); setup(() => { documentManager = new MockDocumentManager(); prompt = createTensorBoardPromptWithMocks(); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index 44518e7575a7..ec44d302d063 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -3,21 +3,21 @@ 'use strict'; -import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; import * as vscodeMocks from './mocks/vsc'; import { vscMockTelemetryReporter } from './mocks/vsc/telemetryReporter'; +import { anything, instance, mock, when } from 'ts-mockito'; const Module = require('module'); type VSCode = typeof vscode; const mockedVSCode: Partial = {}; -export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: TypeMoq.IMock } = {}; +export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: VSCode[P] } = {}; const originalLoad = Module._load; function generateMock(name: K): void { - const mockedObj = TypeMoq.Mock.ofType(); - (mockedVSCode as any)[name] = mockedObj.object; + const mockedObj = mock(); + (mockedVSCode as any)[name] = instance(mockedObj); mockedVSCodeNamespaces[name] = mockedObj as any; } @@ -35,15 +35,26 @@ export function initialize() { generateMock('window'); generateMock('commands'); generateMock('languages'); + generateMock('extensions'); generateMock('env'); generateMock('debug'); generateMock('scm'); - generateNotebookMocks(); + generateMock('notebooks'); // Use mock clipboard fo testing purposes. const clipboard = new MockClipboard(); - mockedVSCodeNamespaces.env?.setup((e) => e.clipboard).returns(() => clipboard); - mockedVSCodeNamespaces.env?.setup((e) => e.appName).returns(() => 'Insider'); + when(mockedVSCodeNamespaces.env!.clipboard).thenReturn(clipboard); + when(mockedVSCodeNamespaces.env!.appName).thenReturn('Insider'); + + // This API is used in src/client/telemetry/telemetry.ts + const extension = mock>(); + const packageJson = mock(); + const contributes = mock(); + when(extension.packageJSON).thenReturn(instance(packageJson)); + when(packageJson.contributes).thenReturn(instance(contributes)); + when(contributes.debuggers).thenReturn([{ aiKey: '' }]); + when(mockedVSCodeNamespaces.extensions!.getExtension(anything())).thenReturn(instance(extension)); + when(mockedVSCodeNamespaces.extensions!.all).thenReturn([]); // When upgrading to npm 9-10, this might have to change, as we could have explicit imports (named imports). Module._load = function (request: any, _parent: any) { @@ -122,21 +133,3 @@ mockedVSCode.LogLevel = vscodeMocks.LogLevel; (mockedVSCode as any).ProtocolTypeHierarchyItem = vscodeMocks.vscMockExtHostedTypes.ProtocolTypeHierarchyItem; (mockedVSCode as any).CancellationError = vscodeMocks.vscMockExtHostedTypes.CancellationError; (mockedVSCode as any).LSPCancellationError = vscodeMocks.vscMockExtHostedTypes.LSPCancellationError; - -// This API is used in src/client/telemetry/telemetry.ts -const extensions = TypeMoq.Mock.ofType(); -extensions.setup((e) => e.all).returns(() => []); -const extension = TypeMoq.Mock.ofType>(); -const packageJson = TypeMoq.Mock.ofType(); -const contributes = TypeMoq.Mock.ofType(); -extension.setup((e) => e.packageJSON).returns(() => packageJson.object); -packageJson.setup((p) => p.contributes).returns(() => contributes.object); -contributes.setup((p) => p.debuggers).returns(() => [{ aiKey: '' }]); -extensions.setup((e) => e.getExtension(TypeMoq.It.isAny())).returns(() => extension.object); -mockedVSCode.extensions = extensions.object; - -function generateNotebookMocks() { - const mockedObj = TypeMoq.Mock.ofType<{}>(); - (mockedVSCode as any).notebook = mockedObj.object; - (mockedVSCodeNamespaces as any).notebook = mockedObj as any; -} From 65c8ac6e3f272d76c9775ad1163a18c61d473119 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 11 Oct 2023 21:43:50 -0700 Subject: [PATCH 0243/1136] Remove formatting settings (#22202) Closes https://github.com/microsoft/vscode-python/issues/22183 --- README.md | 9 +-- build/test-requirements.txt | 5 -- package.json | 71 ------------------- package.nls.json | 21 ------ resources/report_issue_user_settings.json | 9 --- src/client/common/configSettings.ts | 31 -------- src/client/common/types.ts | 9 --- src/client/common/utils/localize.ts | 20 ------ src/client/telemetry/constants.ts | 2 - src/client/telemetry/index.ts | 39 +--------- src/test/.vscode/settings.json | 1 - .../configSettings.unit.test.ts | 61 ---------------- src/test/common/productsToTest.ts | 16 +---- src/testMultiRootWkspc/multi.code-workspace | 1 - 14 files changed, 6 insertions(+), 289 deletions(-) diff --git a/README.md b/README.md index 0a8766f086af..8029aa096587 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Open the Command Palette (Command+Shift+P on macOS and Ctrl+Shift+P on Windows/L | `Python: Select Interpreter` | Switch between Python interpreters, versions, and environments. | | `Python: Start REPL` | Start an interactive Python REPL using the selected interpreter in the VS Code terminal. | | `Python: Run Python File in Terminal` | Runs the active Python file in the VS Code terminal. You can also run a Python file by right-clicking on the file and selecting `Run Python File in Terminal`. | -| `Format Document` | Formats code using the provided [formatter](https://code.visualstudio.com/docs/python/editing#_formatting) in the `settings.json` file. | +| `Format Document` | Formats code using the provided [formatter](https://code.visualstudio.com/docs/python/formatting) in the `settings.json` file. | | `Python: Configure Tests` | Select a test framework and configure it to display the Test Explorer. | To see all available Python commands, open the Command Palette and type `Python`. For Jupyter extension commands, just type `Jupyter`. @@ -71,16 +71,11 @@ Learn more about the rich features of the Python extension: - [IntelliSense](https://code.visualstudio.com/docs/python/editing#_autocomplete-and-intellisense): Edit your code with auto-completion, code navigation, syntax checking and more - [Linting](https://code.visualstudio.com/docs/python/linting): Get additional code analysis with Pylint, Flake8 and more -- [Code formatting](https://code.visualstudio.com/docs/python/editing#_formatting): Format your code with black, autopep or yapf - +- [Code formatting](https://code.visualstudio.com/docs/python/formatting): Format your code with black, autopep or yapf - [Debugging](https://code.visualstudio.com/docs/python/debugging): Debug your Python scripts, web apps, remote or multi-threaded processes - - [Testing](https://code.visualstudio.com/docs/python/unit-testing): Run and debug tests through the Test Explorer with unittest or pytest. - - [Jupyter Notebooks](https://code.visualstudio.com/docs/python/jupyter-support): Create and edit Jupyter Notebooks, add and run code cells, render plots, visualize variables through the variable explorer, visualize dataframes with the data viewer, and more - - [Environments](https://code.visualstudio.com/docs/python/environments): Automatically activate and switch between virtualenv, venv, pipenv, conda and pyenv environments - - [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring): Restructure your Python code with variable extraction and method extraction. Additionally, there is componentized support to enable additional refactoring, such as import sorting, through extensions including [isort](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) and [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff). diff --git a/build/test-requirements.txt b/build/test-requirements.txt index 433bd0f86682..0650e86fb3d3 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -1,13 +1,8 @@ # pin setoptconf to prevent issue with 'use_2to3' setoptconf==0.3.0 -# Install flake8 first, as both flake8 and autopep8 require pycodestyle, -# but flake8 has a tighter pinning. flake8 -autopep8 bandit -black -yapf pylint pycodestyle pydocstyle diff --git a/package.json b/package.json index 88578a687853..20401cc43762 100644 --- a/package.json +++ b/package.json @@ -577,77 +577,6 @@ "type": "array", "uniqueItems": true }, - "python.formatting.autopep8Args": { - "default": [], - "description": "%python.formatting.autopep8Args.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.formatting.autopep8Args.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.autopep8Args.deprecationMessage%" - }, - "python.formatting.autopep8Path": { - "default": "autopep8", - "description": "%python.formatting.autopep8Path.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.formatting.autopep8Path.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.autopep8Path.deprecationMessage%" - }, - "python.formatting.blackArgs": { - "default": [], - "description": "%python.formatting.blackArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.formatting.blackArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.blackArgs.deprecationMessage%" - }, - "python.formatting.blackPath": { - "default": "black", - "description": "%python.formatting.blackPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.formatting.blackPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.blackPath.deprecationMessage%" - }, - "python.formatting.provider": { - "default": "autopep8", - "description": "%python.formatting.provider.description%", - "enum": [ - "autopep8", - "black", - "none", - "yapf" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.formatting.provider.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.provider.deprecationMessage%" - }, - "python.formatting.yapfArgs": { - "default": [], - "description": "%python.formatting.yapfArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.formatting.yapfArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.yapfArgs.deprecationMessage%" - }, - "python.formatting.yapfPath": { - "default": "yapf", - "description": "%python.formatting.yapfPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.formatting.yapfPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.formatting.yapfPath.deprecationMessage%" - }, "python.globalModuleInstallation": { "default": false, "description": "%python.globalModuleInstallation.description%", diff --git a/package.nls.json b/package.nls.json index 5687e51ab9df..f843399e09c5 100644 --- a/package.nls.json +++ b/package.nls.json @@ -42,27 +42,6 @@ "python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.", "python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.", "python.experiments.pythonREPLSmartSend.description": "Denotes the Python REPL Smart Send experiment.", - "python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.autopep8Args.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.autopep8Args.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.autopep8Path.description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", - "python.formatting.autopep8Path.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.autopep8Path.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.blackArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.blackArgs.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.blackArgs.deprecationMessage": "This setting will soon be deprecated. Please use the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.blackPath.description": "Path to Black, you can use a custom version of Black by modifying this setting to include the full path.", - "python.formatting.blackPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Black Formatter extension](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.blackPath.deprecationMessage": "This setting will soon be deprecated. Please use the Black Formatter extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.provider.description": "Provider for formatting. Possible options include 'autopep8', 'black', and 'yapf'.", - "python.formatting.provider.markdownDeprecationMessage": "This setting will soon be deprecated. Please use a dedicated formatter extension.
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.provider.deprecationMessage": "This setting will soon be deprecated. Please use a dedicated formatter extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.yapfArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.formatting.yapfArgs.markdownDeprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.yapfArgs.deprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.formatting.yapfPath.description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", - "python.formatting.yapfPath.markdownDeprecationMessage": "Yapf support will soon be deprecated.
Learn more [here](https://aka.ms/AAlgvkb).", - "python.formatting.yapfPath.deprecationMessage": "Built-in Yapf support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.", "python.languageServer.description": "Defines type of the language server.", "python.languageServer.defaultDescription": "Automatically select a language server: Pylance if installed and available, otherwise fallback to Jedi.", diff --git a/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json index 677e58d83f21..eea4ca007da6 100644 --- a/resources/report_issue_user_settings.json +++ b/resources/report_issue_user_settings.json @@ -69,15 +69,6 @@ "memory": true, "symbolsHierarchyDepthLimit": false }, - "formatting": { - "autopep8Args": "placeholder", - "autopep8Path": "placeholder", - "provider": true, - "blackArgs": "placeholder", - "blackPath": "placeholder", - "yapfArgs": "placeholder", - "yapfPath": "placeholder" - }, "testing": { "cwd": "placeholder", "debugPort": true, diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 3e4b75b8b087..cadc1515f7e6 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -27,7 +27,6 @@ import { IAutoCompleteSettings, IDefaultLanguageServer, IExperiments, - IFormattingSettings, IInterpreterPathService, IInterpreterSettings, ILintingSettings, @@ -109,8 +108,6 @@ export class PythonSettings implements IPythonSettings { public linting!: ILintingSettings; - public formatting!: IFormattingSettings; - public autoComplete!: IAutoCompleteSettings; public tensorBoard: ITensorBoardSettings | undefined; @@ -395,34 +392,6 @@ export class PythonSettings implements IPythonSettings { this.linting.cwd = getAbsolutePath(systemVariables.resolveAny(this.linting.cwd), workspaceRoot); } - const formattingSettings = systemVariables.resolveAny(pythonSettings.get('formatting'))!; - if (this.formatting) { - Object.assign(this.formatting, formattingSettings); - } else { - this.formatting = formattingSettings; - } - // Support for travis. - this.formatting = this.formatting - ? this.formatting - : { - autopep8Args: [], - autopep8Path: 'autopep8', - provider: 'autopep8', - blackArgs: [], - blackPath: 'black', - yapfArgs: [], - yapfPath: 'yapf', - }; - this.formatting.autopep8Path = getAbsolutePath( - systemVariables.resolveAny(this.formatting.autopep8Path), - workspaceRoot, - ); - this.formatting.yapfPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.yapfPath), workspaceRoot); - this.formatting.blackPath = getAbsolutePath( - systemVariables.resolveAny(this.formatting.blackPath), - workspaceRoot, - ); - const testSettings = systemVariables.resolveAny(pythonSettings.get('testing'))!; if (this.testing) { Object.assign(this.testing, testSettings); diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 8b90443703c6..a33f437622fa 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -258,15 +258,6 @@ export interface ILintingSettings { banditArgs: string[]; banditPath: string; } -export interface IFormattingSettings { - readonly provider: string; - autopep8Path: string; - readonly autopep8Args: string[]; - blackPath: string; - readonly blackArgs: string[]; - yapfPath: string; - readonly yapfArgs: string[]; -} export interface ITerminalSettings { readonly executeInFileDir: boolean; diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index c6086071363f..bbb55a79ce40 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -516,24 +516,4 @@ export namespace ToolsExtensions { ); export const installPylintExtension = l10n.t('Install Pylint extension'); export const installFlake8Extension = l10n.t('Install Flake8 extension'); - - export const selectBlackFormatterPrompt = l10n.t( - 'You have the Black formatter extension installed, would you like to use that as the default formatter?', - ); - - export const selectAutopep8FormatterPrompt = l10n.t( - 'You have the Autopep8 formatter extension installed, would you like to use that as the default formatter?', - ); - - export const selectMultipleFormattersPrompt = l10n.t( - 'You have multiple formatters installed, would you like to select one as the default formatter?', - ); - - export const installBlackFormatterPrompt = l10n.t( - 'You triggered formatting with Black, would you like to install one of our new formatter extensions? This will also set it as the default formatter for Python.', - ); - - export const installAutopep8FormatterPrompt = l10n.t( - 'You triggered formatting with Autopep8, would you like to install one of our new formatter extension? This will also set it as the default formatter for Python.', - ); } diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index c680b91094cb..301502a0f6fa 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -4,8 +4,6 @@ 'use strict'; export enum EventName { - FORMAT_SORT_IMPORTS = 'FORMAT.SORT_IMPORTS', - FORMAT = 'FORMAT.FORMAT', FORMAT_ON_TYPE = 'FORMAT.FORMAT_ON_TYPE', EDITOR_LOAD = 'EDITOR.LOAD', LINTING = 'LINTING', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 95496c828018..f69da6046254 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -859,33 +859,7 @@ export interface IEventNamePropertyMapping { */ scope: 'file' | 'selection'; }; - /** - * Telemetry event sent with details when formatting a document - */ - /* __GDPR__ - "format.format" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "karthiknadig" }, - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, - "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "hascustomargs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "formatselection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } - } - */ - [EventName.FORMAT]: { - /** - * Tool being used to format - */ - tool: 'autopep8' | 'black' | 'yapf'; - /** - * If arguments for formatter is provided in resource settings - */ - hasCustomArgs: boolean; - /** - * Carries `true` when formatting a selection of text, `false` otherwise - */ - formatSelection: boolean; - }; + /** * Telemetry event sent with the value of setting 'Format on type' */ @@ -902,16 +876,6 @@ export interface IEventNamePropertyMapping { */ enabled: boolean; }; - /** - * Telemetry event sent when sorting imports using formatter - */ - /* __GDPR__ - "format.sort_imports" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "karthiknadig" }, - "originaleventname" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } - } - */ - [EventName.FORMAT_SORT_IMPORTS]: never | undefined; /** * Telemetry event sent with details when tracking imports @@ -921,7 +885,6 @@ export interface IEventNamePropertyMapping { "hashedname" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "luabud" } } */ - [EventName.HASHED_PACKAGE_NAME]: { /** * Hash of the package name diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index 771962b5a909..faeb48ffa29c 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -11,7 +11,6 @@ "python.linting.pylamaEnabled": false, "python.linting.mypyEnabled": false, "python.linting.banditEnabled": false, - "python.formatting.provider": "yapf", // Don't set this to `Pylance`, for CI we want to use the LS that ships with the extension. "python.languageServer": "Jedi", "python.pythonPath": "C:\\GIT\\s p\\vscode-python\\.venv\\Scripts\\python.exe" diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index 113770122fbc..e43ac7b7fbd8 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -19,7 +19,6 @@ import { PersistentStateFactory } from '../../../client/common/persistentState'; import { IAutoCompleteSettings, IExperiments, - IFormattingSettings, IInterpreterSettings, ILintingSettings, ITerminalSettings, @@ -117,7 +116,6 @@ suite('Python Settings', async () => { // complex settings config.setup((c) => c.get('interpreter')).returns(() => sourceSettings.interpreter); config.setup((c) => c.get('linting')).returns(() => sourceSettings.linting); - config.setup((c) => c.get('formatting')).returns(() => sourceSettings.formatting); config.setup((c) => c.get('autoComplete')).returns(() => sourceSettings.autoComplete); config.setup((c) => c.get('testing')).returns(() => sourceSettings.testing); config.setup((c) => c.get('terminal')).returns(() => sourceSettings.terminal); @@ -264,63 +262,4 @@ suite('Python Settings', async () => { test('Experiments (not enabled)', () => testExperiments(false)); test('Experiments (enabled)', () => testExperiments(true)); - - test('Formatter Paths and args', () => { - expected.pythonPath = 'python3'; - - expected.formatting = { - autopep8Args: ['1', '2'], - autopep8Path: 'one', - blackArgs: ['3', '4'], - blackPath: 'two', - yapfArgs: ['5', '6'], - yapfPath: 'three', - provider: '', - }; - expected.formatting.blackPath = 'spam'; - initializeConfig(expected); - config - .setup((c) => c.get('formatting')) - .returns(() => expected.formatting) - .verifiable(TypeMoq.Times.once()); - - settings.update(config.object); - - for (const key of Object.keys(expected.formatting)) { - expect((settings.formatting as any)[key]).to.be.deep.equal((expected.formatting as any)[key]); - } - config.verifyAll(); - }); - test('Formatter Paths (paths relative to home)', () => { - expected.pythonPath = 'python3'; - - expected.formatting = { - autopep8Args: [], - autopep8Path: path.join('~', 'one'), - blackArgs: [], - blackPath: path.join('~', 'two'), - yapfArgs: [], - yapfPath: path.join('~', 'three'), - provider: '', - }; - expected.formatting.blackPath = 'spam'; - initializeConfig(expected); - config - .setup((c) => c.get('formatting')) - .returns(() => expected.formatting) - .verifiable(TypeMoq.Times.once()); - - settings.update(config.object); - - for (const key of Object.keys(expected.formatting)) { - if (!key.endsWith('path')) { - continue; - } - - const expectedPath = untildify((expected.formatting as any)[key]); - - expect((settings.formatting as any)[key]).to.be.equal(expectedPath); - } - config.verifyAll(); - }); }); diff --git a/src/test/common/productsToTest.ts b/src/test/common/productsToTest.ts index 861bab898509..e82d12bbd9eb 100644 --- a/src/test/common/productsToTest.ts +++ b/src/test/common/productsToTest.ts @@ -7,18 +7,8 @@ import { getNamesAndValues } from '../../client/common/utils/enum'; export function getProductsForInstallerTests(): { name: string; value: Product }[] { return getNamesAndValues(Product).filter( (p) => - ![ - 'pylint', - 'flake8', - 'pycodestyle', - 'pylama', - 'prospector', - 'pydocstyle', - 'yapf', - 'autopep8', - 'mypy', - 'black', - 'bandit', - ].includes(p.name), + !['pylint', 'flake8', 'pycodestyle', 'pylama', 'prospector', 'pydocstyle', 'mypy', 'bandit'].includes( + p.name, + ), ); } diff --git a/src/testMultiRootWkspc/multi.code-workspace b/src/testMultiRootWkspc/multi.code-workspace index 9d5c8ac77475..51d218783041 100644 --- a/src/testMultiRootWkspc/multi.code-workspace +++ b/src/testMultiRootWkspc/multi.code-workspace @@ -37,7 +37,6 @@ "python.linting.pylintEnabled": true, "python.linting.pycodestyleEnabled": false, "python.linting.prospectorEnabled": false, - "python.formatting.provider": "yapf", "python.linting.lintOnSave": false, "python.linting.enabled": true, "python.pythonPath": "python" From 9b5f58afc0acacacd45c035c4e1a78622944407d Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 12 Oct 2023 08:53:10 -0700 Subject: [PATCH 0244/1136] Add logging for failure to retrieve environment variables, testing rewrite (#22203) --- pythonFiles/unittestadapter/discovery.py | 12 +++++++++--- pythonFiles/unittestadapter/execution.py | 19 ++++++++++++++++--- pythonFiles/vscode_pytest/__init__.py | 18 +++++++++++++++++- .../vscode_pytest/run_pytest_script.py | 2 ++ .../pytest/pytestDiscoveryAdapter.ts | 2 +- .../pytest/pytestExecutionAdapter.ts | 1 + 6 files changed, 46 insertions(+), 8 deletions(-) diff --git a/pythonFiles/unittestadapter/discovery.py b/pythonFiles/unittestadapter/discovery.py index 7e07e45d1202..7525f33cda61 100644 --- a/pythonFiles/unittestadapter/discovery.py +++ b/pythonFiles/unittestadapter/discovery.py @@ -19,7 +19,7 @@ # If I use from utils then there will be an import error in test_discovery.py. from unittestadapter.utils import TestNode, build_test_tree, parse_unittest_args -DEFAULT_PORT = "45454" +DEFAULT_PORT = 45454 class PayloadDict(TypedDict): @@ -121,12 +121,18 @@ def post_response( start_dir, pattern, top_level_dir = parse_unittest_args(argv[index + 1 :]) - # Perform test discovery. testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT)) testUuid = os.environ.get("TEST_UUID") - # Post this discovery payload. + if testPort is DEFAULT_PORT: + print( + "Error[vscode-unittest]: TEST_PORT is not set.", + " TEST_UUID = ", + testUuid, + ) if testUuid is not None: + # Perform test discovery. payload = discover_tests(start_dir, pattern, top_level_dir, testUuid) + # Post this discovery payload. post_response(payload, testPort, testUuid) # Post EOT token. eot_payload: EOTPayloadDict = {"command_type": "discovery", "eot": True} diff --git a/pythonFiles/unittestadapter/execution.py b/pythonFiles/unittestadapter/execution.py index e5758118b951..2a22bfff3486 100644 --- a/pythonFiles/unittestadapter/execution.py +++ b/pythonFiles/unittestadapter/execution.py @@ -21,14 +21,13 @@ from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict from unittestadapter.utils import parse_unittest_args -DEFAULT_PORT = "45454" - ErrorType = Union[ Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None] ] testPort = 0 testUuid = 0 START_DIR = "" +DEFAULT_PORT = 45454 class TestOutcomeEnum(str, enum.Enum): @@ -269,7 +268,8 @@ def post_response( run_test_ids_port_int = ( int(run_test_ids_port) if run_test_ids_port is not None else 0 ) - + if run_test_ids_port_int == 0: + print("Error[vscode-unittest]: RUN_TEST_IDS_PORT env var is not set.") # get data from socket test_ids_from_buffer = [] try: @@ -303,6 +303,19 @@ def post_response( testPort = int(os.environ.get("TEST_PORT", DEFAULT_PORT)) testUuid = os.environ.get("TEST_UUID") + if testPort is DEFAULT_PORT: + print( + "Error[vscode-unittest]: TEST_PORT is not set.", + " TEST_UUID = ", + testUuid, + ) + if testUuid is None: + print( + "Error[vscode-unittest]: TEST_UUID is not set.", + " TEST_PORT = ", + testPort, + ) + testUuid = "unknown" if test_ids_from_buffer: # Perform test execution. payload = run_tests( diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 2fab4d77c2f8..8349e1aa893d 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -20,6 +20,8 @@ from testing_tools import socket_manager from typing_extensions import Literal, TypedDict +DEFAULT_PORT = 45454 + class TestData(TypedDict): """A general class that all test objects inherit from.""" @@ -683,8 +685,22 @@ def send_post_request( payload -- the payload data to be sent. cls_encoder -- a custom encoder if needed. """ - testPort = os.getenv("TEST_PORT", 45454) + testPort = os.getenv("TEST_PORT") testUuid = os.getenv("TEST_UUID") + if testPort is None: + print( + "Error[vscode-pytest]: TEST_PORT is not set.", + " TEST_UUID = ", + testUuid, + ) + testPort = DEFAULT_PORT + if testUuid is None: + print( + "Error[vscode-pytest]: TEST_UUID is not set.", + " TEST_PORT = ", + testPort, + ) + testUuid = "unknown" addr = ("localhost", int(testPort)) global __socket diff --git a/pythonFiles/vscode_pytest/run_pytest_script.py b/pythonFiles/vscode_pytest/run_pytest_script.py index c3720c8ab8d0..e60ee91f096e 100644 --- a/pythonFiles/vscode_pytest/run_pytest_script.py +++ b/pythonFiles/vscode_pytest/run_pytest_script.py @@ -28,6 +28,8 @@ run_test_ids_port_int = ( int(run_test_ids_port) if run_test_ids_port is not None else 0 ) + if run_test_ids_port_int == 0: + print("Error[vscode-pytest]: RUN_TEST_IDS_PORT env var is not set.") test_ids_from_buffer = [] try: client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 92bd9f04834e..09ca36849000 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -78,7 +78,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { mutableEnv.PYTHONPATH = pythonPathCommand; mutableEnv.TEST_UUID = uuid.toString(); mutableEnv.TEST_PORT = this.testServer.getPort().toString(); - + traceInfo(`All environment variables set for pytest discovery: ${JSON.stringify(mutableEnv)}`); const spawnOptions: SpawnOptions = { cwd, throwOnStdErr: true, diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 5c04aabab845..fd61251d33fc 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -140,6 +140,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // add port with run test ids to env vars const pytestRunTestIdsPort = await utils.startTestIdServer(testIds); mutableEnv.RUN_TEST_IDS_PORT = pytestRunTestIdsPort.toString(); + traceInfo(`All environment variables set for pytest execution: ${JSON.stringify(mutableEnv)}`); const spawnOptions: SpawnOptions = { cwd, From ec001a0b1503555e685996baab8fda4f0648c454 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 12 Oct 2023 14:11:15 -0700 Subject: [PATCH 0245/1136] Fix for webpack warning with LSP types (#22211) --- build/webpack/webpack.extension.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/build/webpack/webpack.extension.config.js b/build/webpack/webpack.extension.config.js index f496aa32ee26..7003ffa277d2 100644 --- a/build/webpack/webpack.extension.config.js +++ b/build/webpack/webpack.extension.config.js @@ -69,6 +69,7 @@ const config = { resolve: { extensions: ['.ts', '.js'], plugins: [new tsconfig_paths_webpack_plugin.TsconfigPathsPlugin({ configFile: configFileName })], + conditionNames: ['require', 'node'], }, output: { filename: '[name].js', From 6c23e4335db10e900ea0ca2402e267322c3a2e69 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 12 Oct 2023 14:44:50 -0700 Subject: [PATCH 0246/1136] Handle white spaces for list along with dictionary (#22209) Legacy normalization script leaves unnecessary white spaces for dictionary as well as list. While there are multiple correct usage of it such as for after a function, for more intuitive REPL experience. We want to keep previous normalization style and white space format EXCEPT for dictionary and list case. Dictionary case is handled, but this is the PR to handle the elimination of extra white spaces for list as well. Closes: #22208 --- pythonFiles/normalizeSelection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonFiles/normalizeSelection.py b/pythonFiles/normalizeSelection.py index 0ac47ab5dc3b..7ace42daa901 100644 --- a/pythonFiles/normalizeSelection.py +++ b/pythonFiles/normalizeSelection.py @@ -119,7 +119,7 @@ def normalize_lines(selection): # Insert a newline between each top-level statement, and append a newline to the selection. source = "\n".join(statements) + "\n" - if selection[-2] == "}": + if selection[-2] == "}" or selection[-2] == "]": source = source[:-1] except Exception: # If there's a problem when parsing statements, From bdb8efb9dd20cefd56a11687ed7f7dfc89f9f15d Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 12 Oct 2023 17:18:49 -0700 Subject: [PATCH 0247/1136] Try using `import` in webpack condition names (#22212) --- build/webpack/webpack.extension.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/webpack/webpack.extension.config.js b/build/webpack/webpack.extension.config.js index 7003ffa277d2..a33508e5d96a 100644 --- a/build/webpack/webpack.extension.config.js +++ b/build/webpack/webpack.extension.config.js @@ -69,7 +69,7 @@ const config = { resolve: { extensions: ['.ts', '.js'], plugins: [new tsconfig_paths_webpack_plugin.TsconfigPathsPlugin({ configFile: configFileName })], - conditionNames: ['require', 'node'], + conditionNames: ['import', 'require', 'node'], }, output: { filename: '[name].js', From 76ae73a46bea7e722bf43e4d5550b3d895066d90 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 12 Oct 2023 21:52:38 -0700 Subject: [PATCH 0248/1136] Skip setting `PYTHONUTF8` when activating terminals (#22213) Closes https://github.com/microsoft/vscode-python/issues/22205 --- .../activation/terminalEnvVarCollectionService.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts index c11ec221d4d7..92e97c95e468 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/interpreter/activation/terminalEnvVarCollectionService.ts @@ -394,7 +394,12 @@ function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariabl } function shouldSkip(env: string) { - return ['_', 'SHLVL'].includes(env); + return [ + '_', + 'SHLVL', + // Even though this maybe returned, setting it can result in output encoding errors in terminal. + 'PYTHONUTF8', + ].includes(env); } function getPromptForEnv(interpreter: PythonEnvironment | undefined) { From eada0f1ab940729ae335389c34899d64b56edd35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 10:32:09 -0700 Subject: [PATCH 0249/1136] Bump microvenv from 2023.2.0 to 2023.3.post1 (#22204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [microvenv](https://github.com/brettcannon/microvenv) from 2023.2.0 to 2023.3.post1.
Release notes

Sourced from microvenv's releases.

2023.3.post1

What's Changed

⚠️ Breaking Changes

🎉 New Features

Full Changelog: https://github.com/brettcannon/microvenv/compare/v2023.2.0...v2023.3.post1

Commits
  • bf19f92 Update the docs due to Windows support
  • 0c5436d Fix docs.yml
  • 00d43b4 Update the version for release
  • 2e8d62e Add a release.yml workflow
  • 7b9ca8a Add support for Windows (except for create()) (#55)
  • 7085e42 Drop CI path requirements
  • f9dc600 Add mypy's stubtest to linting
  • 54e08f8 Clarify that _create.py is self-contained
  • 3145fcb Merge branch 'main' of github.com:brettcannon/microvenv
  • 6c0529e Drop the static HTML directory
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=microvenv&package-manager=pip&previous-version=2023.2.0&new-version=2023.3.post1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 205b9fc4804c..31765898ab59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,9 @@ importlib-metadata==6.7.0 \ --hash=sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4 \ --hash=sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5 # via -r requirements.in -microvenv==2023.2.0 \ - --hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \ - --hash=sha256:a07e88a8fb5ee90219b86dd90095cb5646462d45d30285ea3b1a3c7cf33616d3 +microvenv==2023.3.post1 \ + --hash=sha256:67f0a48511cf16d6a2a45137175d0ddc36a657b91459b598cfbe976ef2afd596 \ + --hash=sha256:6e8c80ccfe813b00b77ab9cc2e5af3fd44e2fe540df176509fda97123f8b8290 # via -r requirements.in packaging==23.2 \ --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ From ed155afa4bf6acdbd0341d093b5a00e13237985e Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 13 Oct 2023 16:24:39 -0700 Subject: [PATCH 0250/1136] remove asserts from catchable code for testing (#22210) some asserts were inside functions / mocking and with this then the extension code catches the exception and doesn't error out as the test. Bring the asserts out of the functions into the test so the asserts work as expected. --- .../testing/common/testingAdapter.test.ts | 50 ++++++++++++------- .../testing/common/testingPayloadsEot.test.ts | 14 ++++-- .../testController/server.unit.test.ts | 7 ++- .../testExecutionAdapter.unit.test.ts | 18 +++++-- 4 files changed, 62 insertions(+), 27 deletions(-) diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 519a60e3f0f7..a9ed25194fa9 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -653,17 +653,21 @@ suite('End to End Tests: test adapters', () => { if (data.error === undefined) { // Dereference a NULL pointer const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); - assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); - } else { - assert.ok(data.error, "Expected errors in 'error' field"); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = 'Expected test to have a null pointer'; + } + } else if (data.error.length === 0) { + failureOccurred = true; + failureMsg = "Expected errors in 'error' field"; } } else { const indexOfTest = JSON.stringify(data.tests).search('error'); - assert.notDeepEqual( - indexOfTest, - -1, - 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', - ); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.'; + } } } catch (err) { failureMsg = err ? (err as Error).toString() : ''; @@ -705,22 +709,32 @@ suite('End to End Tests: test adapters', () => { if (data.error === undefined) { // Dereference a NULL pointer const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); - assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); - } else { - assert.ok(data.error, "Expected errors in 'error' field"); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = 'Expected test to have a null pointer'; + } + } else if (data.error.length === 0) { + failureOccurred = true; + failureMsg = "Expected errors in 'error' field"; } } else { const indexOfTest = JSON.stringify(data.result).search('error'); - assert.notDeepEqual( - indexOfTest, - -1, - 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', - ); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.'; + } + } + if (data.result === undefined) { + failureOccurred = true; + failureMsg = 'Expected results to be present'; } - assert.ok(data.result, 'Expected results to be present'); // make sure the testID is found in the results const indexOfTest = JSON.stringify(data).search('test_seg_fault.TestSegmentationFault.test_segfault'); - assert.notDeepEqual(indexOfTest, -1, 'Expected testId to be present'); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = 'Expected testId to be present'; + } } catch (err) { failureMsg = err ? (err as Error).toString() : ''; failureOccurred = true; diff --git a/src/test/testing/common/testingPayloadsEot.test.ts b/src/test/testing/common/testingPayloadsEot.test.ts index a30b1efe288c..2b8b9c0667df 100644 --- a/src/test/testing/common/testingPayloadsEot.test.ts +++ b/src/test/testing/common/testingPayloadsEot.test.ts @@ -165,13 +165,20 @@ suite('EOT tests', () => { mockProc.emit('close', 0, null); client.end(); }); - + let errorBool = false; + let errorMessage = ''; resultResolver = new PythonResultResolver(testController, PYTEST_PROVIDER, workspaceUri); resultResolver._resolveExecution = async (payload, _token?) => { // the payloads that get to the _resolveExecution are all data and should be successful. actualCollectedResult = actualCollectedResult + JSON.stringify(payload.result); - assert.strictEqual(payload.status, 'success', "Expected status to be 'success'"); - assert.ok(payload.result, 'Expected results to be present'); + if (payload.status !== 'success') { + errorBool = true; + errorMessage = "Expected status to be 'success'"; + } + if (!payload.result) { + errorBool = true; + errorMessage = 'Expected results to be present'; + } return Promise.resolve(); }; @@ -208,6 +215,7 @@ suite('EOT tests', () => { actualCollectedResult, "Expected collected result to match 'data'", ); + assert.strictEqual(errorBool, false, errorMessage); }); }); }); diff --git a/src/test/testing/testController/server.unit.test.ts b/src/test/testing/testController/server.unit.test.ts index 742492b33ba8..62f5b8327219 100644 --- a/src/test/testing/testController/server.unit.test.ts +++ b/src/test/testing/testController/server.unit.test.ts @@ -117,13 +117,15 @@ suite('Python Test Server, DataWithPayloadChunks', () => { const dataWithPayloadChunks = testCaseDataObj; await server.serverReady(); - + let errorOccur = false; + let errorMessage = ''; server.onRunDataReceived(({ data }) => { try { const resultData = JSON.parse(data).result; eventData = eventData + JSON.stringify(resultData); } catch (e) { - assert(false, 'Error parsing data'); + errorOccur = true; + errorMessage = 'Error parsing data'; } deferred.resolve(); }); @@ -143,6 +145,7 @@ suite('Python Test Server, DataWithPayloadChunks', () => { await deferred.promise; const expectedResult = dataWithPayloadChunks.data; assert.deepStrictEqual(eventData, expectedResult); + assert.deepStrictEqual(errorOccur, false, errorMessage); }); }); }); diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index 4d4a8d0ebee4..e2903d353bbf 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -31,12 +31,16 @@ suite('Unittest test execution adapter', () => { test('runTests should send the run command to the test server', async () => { let options: TestCommandOptions | undefined; - + let errorBool = false; + let errorMessage = ''; const stubTestServer = ({ sendCommand(opt: TestCommandOptions, runTestIdPort?: string): Promise { delete opt.outChannel; options = opt; - assert(runTestIdPort !== undefined); + if (runTestIdPort === undefined) { + errorBool = true; + errorMessage = 'runTestIdPort is undefined'; + } return Promise.resolve(); }, onRunDataReceived: () => { @@ -60,6 +64,7 @@ suite('Unittest test execution adapter', () => { testIds, }; assert.deepStrictEqual(options, expectedOptions); + assert.equal(errorBool, false, errorMessage); }); }); test('runTests should respect settings.testing.cwd when present', async () => { @@ -69,12 +74,16 @@ suite('Unittest test execution adapter', () => { }), } as unknown) as IConfigurationService; let options: TestCommandOptions | undefined; - + let errorBool = false; + let errorMessage = ''; const stubTestServer = ({ sendCommand(opt: TestCommandOptions, runTestIdPort?: string): Promise { delete opt.outChannel; options = opt; - assert(runTestIdPort !== undefined); + if (runTestIdPort === undefined) { + errorBool = true; + errorMessage = 'runTestIdPort is undefined'; + } return Promise.resolve(); }, onRunDataReceived: () => { @@ -99,6 +108,7 @@ suite('Unittest test execution adapter', () => { testIds, }; assert.deepStrictEqual(options, expectedOptions); + assert.equal(errorBool, false, errorMessage); }); }); }); From 1310bd665d83bcd4e09903bff39ac841dafcad52 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 17 Oct 2023 00:00:05 -0700 Subject: [PATCH 0251/1136] Enable experiments for all tests (#22194) Closes: #22193 Enables to opt into experiments for tests such as single workspace, multi workspace, debugger, venv, etc. --- src/test/initialize.ts | 4 ++ src/test/smoke/smartSend.smoke.test.ts | 83 ++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/src/test/initialize.ts b/src/test/initialize.ts index add1d8624461..487860410bf0 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -31,6 +31,10 @@ export async function initializePython() { export async function initialize(): Promise { await initializePython(); + + const pythonConfig = vscode.workspace.getConfiguration('python'); + await pythonConfig.update('experiments.optInto', ['All'], vscode.ConfigurationTarget.Global); + await pythonConfig.update('experiments.optOutFrom', [], vscode.ConfigurationTarget.Global); const api = await activateExtension(); if (!IS_SMOKE_TEST) { // When running smoke tests, we won't have access to these. diff --git a/src/test/smoke/smartSend.smoke.test.ts b/src/test/smoke/smartSend.smoke.test.ts index e69de29bb2d1..20ec70af9b5b 100644 --- a/src/test/smoke/smartSend.smoke.test.ts +++ b/src/test/smoke/smartSend.smoke.test.ts @@ -0,0 +1,83 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { assert } from 'chai'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { openFile, waitForCondition } from '../common'; + +suite('Smoke Test: Run Smart Selection and Advance Cursor', () => { + suiteSetup(async function () { + if (!IS_SMOKE_TEST) { + return this.skip(); + } + await initialize(); + return undefined; + }); + + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + test('Smart Send', async () => { + const file = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'create_delete_file.py', + ); + const outputFile = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'smart_send_smoke.txt', + ); + + await fs.remove(outputFile); + + const textDocument = await openFile(file); + + if (vscode.window.activeTextEditor) { + const myPos = new vscode.Position(0, 0); + vscode.window.activeTextEditor!.selections = [new vscode.Selection(myPos, myPos)]; + } + await vscode.commands + .executeCommand('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + + const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); + await waitForCondition(checkIfFileHasBeenCreated, 10_000, `"${outputFile}" file not created`); + + await vscode.commands + .executeCommand('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + await vscode.commands + .executeCommand('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + + async function wait() { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 10000); + }); + } + + await wait(); + + const deletedFile = !(await fs.pathExists(outputFile)); + if (deletedFile) { + assert.ok(true, `"${outputFile}" file has been deleted`); + } else { + assert.fail(`"${outputFile}" file still exists`); + } + }); +}); From f43826256703a40f76e1a93e677e72e5963689bc Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 17 Oct 2023 19:38:08 +1100 Subject: [PATCH 0252/1136] Add support for a tensorboard experiment (#22215) --- package.json | 6 +- package.nls.json | 1 + src/client/common/experiments/groups.ts | 5 + .../nbextensionCodeLensProvider.ts | 22 +- src/client/tensorBoard/serviceRegistry.ts | 2 + .../tensorBoard/tensorBoardFileWatcher.ts | 22 +- .../tensorBoardImportCodeLensProvider.ts | 22 +- .../tensorBoard/tensorBoardSessionProvider.ts | 28 ++- .../tensorBoard/tensorBoardUsageTracker.ts | 16 +- .../tensorBoard/tensorboarExperiment.ts | 65 +++++- .../tensorBoard/tensorboardIntegration.ts | 3 - src/client/tensorBoard/terminalWatcher.ts | 12 +- .../nbextensionCodeLensProvider.unit.test.ts | 85 ++++---- ...orBoardImportCodeLensProvider.unit.test.ts | 106 +++++----- .../tensorBoardPrompt.unit.test.ts | 2 +- .../tensorBoardUsageTracker.unit.test.ts | 191 ++++++++++-------- 16 files changed, 380 insertions(+), 208 deletions(-) diff --git a/package.json b/package.json index 20401cc43762..5b13f9eae0a3 100644 --- a/package.json +++ b/package.json @@ -537,7 +537,8 @@ "pythonPromptNewToolsExt", "pythonTerminalEnvVarActivation", "pythonTestAdapter", - "pythonREPLSmartSend" + "pythonREPLSmartSend", + "pythonRecommendTensorboardExt" ], "enumDescriptions": [ "%python.experiments.All.description%", @@ -545,7 +546,8 @@ "%python.experiments.pythonPromptNewToolsExt.description%", "%python.experiments.pythonTerminalEnvVarActivation.description%", "%python.experiments.pythonTestAdapter.description%", - "%python.experiments.pythonREPLSmartSend.description%" + "%python.experiments.pythonREPLSmartSend.description%", + "%python.experiments.pythonRecommendTensorboardExt.description%" ] }, "scope": "machine", diff --git a/package.nls.json b/package.nls.json index f843399e09c5..87692fb7c1a8 100644 --- a/package.nls.json +++ b/package.nls.json @@ -42,6 +42,7 @@ "python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.", "python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.", "python.experiments.pythonREPLSmartSend.description": "Denotes the Python REPL Smart Send experiment.", + "python.experiments.pythonRecommendTensorboardExt.description": "Denotes the Tensorboard Extension recommendation experiment.", "python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.", "python.languageServer.description": "Defines type of the language server.", "python.languageServer.defaultDescription": "Automatically select a language server: Pylance if installed and available, otherwise fallback to Jedi.", diff --git a/src/client/common/experiments/groups.ts b/src/client/common/experiments/groups.ts index b7a598e0a08a..29035bbc57fe 100644 --- a/src/client/common/experiments/groups.ts +++ b/src/client/common/experiments/groups.ts @@ -22,3 +22,8 @@ export enum EnableTestAdapterRewrite { export enum EnableREPLSmartSend { experiment = 'pythonREPLSmartSend', } + +// Experiment to recommend installing the tensorboard extension. +export enum RecommendTensobardExtension { + experiment = 'pythonRecommendTensorboardExt', +} diff --git a/src/client/tensorBoard/nbextensionCodeLensProvider.ts b/src/client/tensorBoard/nbextensionCodeLensProvider.ts index 7b9a116ee144..afaaf116851a 100644 --- a/src/client/tensorBoard/nbextensionCodeLensProvider.ts +++ b/src/client/tensorBoard/nbextensionCodeLensProvider.ts @@ -3,21 +3,23 @@ import { inject, injectable } from 'inversify'; import { once } from 'lodash'; -import { CancellationToken, CodeLens, Command, languages, Position, Range, TextDocument } from 'vscode'; +import { CancellationToken, CodeLens, Command, Disposable, languages, Position, Range, TextDocument } from 'vscode'; import { IExtensionSingleActivationService } from '../activation/types'; import { Commands, NotebookCellScheme, PYTHON_LANGUAGE } from '../common/constants'; -import { IDisposableRegistry } from '../common/types'; +import { IDisposable, IDisposableRegistry } from '../common/types'; import { TensorBoard } from '../common/utils/localize'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { containsNotebookExtension } from './helpers'; -import { useNewTensorboardExtension } from './tensorboarExperiment'; +import { TensorboardExperiment } from './tensorboarExperiment'; @injectable() export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + private readonly disposables: IDisposable[] = []; + private sendTelemetryOnce = once( sendTelemetryEvent.bind(this, EventName.TENSORBOARD_ENTRYPOINT_SHOWN, undefined, { trigger: TensorBoardEntrypointTrigger.nbextension, @@ -25,12 +27,22 @@ export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleA }), ); - constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} + constructor( + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, + ) { + disposables.push(this); + } + + public dispose(): void { + Disposable.from(...this.disposables).dispose(); + } public async activate(): Promise { - if (useNewTensorboardExtension()) { + if (TensorboardExperiment.isTensorboardExtensionInstalled) { return; } + this.experiment.disposeOnInstallingTensorboard(this); this.activateInternal().ignoreErrors(); } diff --git a/src/client/tensorBoard/serviceRegistry.ts b/src/client/tensorBoard/serviceRegistry.ts index dd193f528eea..5fedb7b6abf5 100644 --- a/src/client/tensorBoard/serviceRegistry.ts +++ b/src/client/tensorBoard/serviceRegistry.ts @@ -11,6 +11,7 @@ import { TensorBoardSessionProvider } from './tensorBoardSessionProvider'; import { TensorBoardNbextensionCodeLensProvider } from './nbextensionCodeLensProvider'; import { TerminalWatcher } from './terminalWatcher'; import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; +import { TensorboardExperiment } from './tensorboarExperiment'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(TensorBoardSessionProvider, TensorBoardSessionProvider); @@ -34,4 +35,5 @@ export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addBinding(TensorBoardNbextensionCodeLensProvider, IExtensionSingleActivationService); serviceManager.addSingleton(IExtensionSingleActivationService, TerminalWatcher); serviceManager.addSingleton(TensorboardDependencyChecker, TensorboardDependencyChecker); + serviceManager.addSingleton(TensorboardExperiment, TensorboardExperiment); } diff --git a/src/client/tensorBoard/tensorBoardFileWatcher.ts b/src/client/tensorBoard/tensorBoardFileWatcher.ts index dccdb95290ec..f2f9344d7365 100644 --- a/src/client/tensorBoard/tensorBoardFileWatcher.ts +++ b/src/client/tensorBoard/tensorBoardFileWatcher.ts @@ -2,13 +2,13 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { FileSystemWatcher, RelativePattern, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; +import { Disposable, FileSystemWatcher, RelativePattern, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; import { IExtensionSingleActivationService } from '../activation/types'; import { IWorkspaceService } from '../common/application/types'; -import { IDisposableRegistry } from '../common/types'; +import { IDisposable, IDisposableRegistry } from '../common/types'; import { TensorBoardEntrypointTrigger } from './constants'; import { TensorBoardPrompt } from './tensorBoardPrompt'; -import { useNewTensorboardExtension } from './tensorboarExperiment'; +import { TensorboardExperiment } from './tensorboarExperiment'; @injectable() export class TensorBoardFileWatcher implements IExtensionSingleActivationService { @@ -18,16 +18,26 @@ export class TensorBoardFileWatcher implements IExtensionSingleActivationService private globPatterns = ['*tfevents*', '*/*tfevents*', '*/*/*tfevents*']; + private readonly disposables: IDisposable[] = []; + constructor( @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(TensorBoardPrompt) private tensorBoardPrompt: TensorBoardPrompt, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, - ) {} + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, + ) { + disposables.push(this); + } + + public dispose(): void { + Disposable.from(...this.disposables).dispose(); + } public async activate(): Promise { - if (useNewTensorboardExtension()) { + if (TensorboardExperiment.isTensorboardExtensionInstalled) { return; } + this.experiment.disposeOnInstallingTensorboard(this); this.activateInternal().ignoreErrors(); } diff --git a/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts b/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts index d6dc8d7e82e5..585b9151922a 100644 --- a/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts +++ b/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts @@ -3,16 +3,16 @@ import { inject, injectable } from 'inversify'; import { once } from 'lodash'; -import { CancellationToken, CodeLens, Command, languages, Position, Range, TextDocument } from 'vscode'; +import { CancellationToken, CodeLens, Command, Disposable, languages, Position, Range, TextDocument } from 'vscode'; import { IExtensionSingleActivationService } from '../activation/types'; import { Commands, PYTHON } from '../common/constants'; -import { IDisposableRegistry } from '../common/types'; +import { IDisposable, IDisposableRegistry } from '../common/types'; import { TensorBoard } from '../common/utils/localize'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { containsTensorBoardImport } from './helpers'; -import { useNewTensorboardExtension } from './tensorboarExperiment'; +import { TensorboardExperiment } from './tensorboarExperiment'; @injectable() export class TensorBoardImportCodeLensProvider implements IExtensionSingleActivationService { @@ -25,12 +25,24 @@ export class TensorBoardImportCodeLensProvider implements IExtensionSingleActiva }), ); - constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} + private readonly disposables: IDisposable[] = []; + + constructor( + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, + ) { + disposables.push(this); + } + + public dispose(): void { + Disposable.from(...this.disposables).dispose(); + } public async activate(): Promise { - if (useNewTensorboardExtension()) { + if (TensorboardExperiment.isTensorboardExtensionInstalled) { return; } + this.experiment.disposeOnInstallingTensorboard(this); this.activateInternal().ignoreErrors(); } diff --git a/src/client/tensorBoard/tensorBoardSessionProvider.ts b/src/client/tensorBoard/tensorBoardSessionProvider.ts index c81059654075..ec52b9ef94dc 100644 --- a/src/client/tensorBoard/tensorBoardSessionProvider.ts +++ b/src/client/tensorBoard/tensorBoardSessionProvider.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { l10n, ViewColumn } from 'vscode'; +import { Disposable, l10n, ViewColumn } from 'vscode'; import { IExtensionSingleActivationService } from '../activation/types'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; import { Commands } from '../common/constants'; @@ -14,6 +14,7 @@ import { IPersistentState, IPersistentStateFactory, IConfigurationService, + IDisposable, } from '../common/types'; import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; import { IInterpreterService } from '../interpreter/contracts'; @@ -22,7 +23,7 @@ import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { TensorBoardSession } from './tensorBoardSession'; -import { useNewTensorboardExtension } from './tensorboarExperiment'; +import { TensorboardExperiment } from './tensorboarExperiment'; export const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup'; @@ -36,18 +37,22 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer private hasActiveTensorBoardSessionContext: ContextKey; + private readonly disposables: IDisposable[] = []; + constructor( @inject(IInstaller) private readonly installer: IInstaller, @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, ) { + disposables.push(this); this.preferredViewGroupMemento = this.stateFactory.createGlobalPersistentState( PREFERRED_VIEWGROUP, ViewColumn.Active, @@ -58,10 +63,15 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer ); } + public dispose(): void { + Disposable.from(...this.disposables).dispose(); + } + public async activate(): Promise { - if (useNewTensorboardExtension()) { + if (TensorboardExperiment.isTensorboardExtensionInstalled) { return; } + this.experiment.disposeOnInstallingTensorboard(this); this.disposables.push( this.commandManager.registerCommand( @@ -69,16 +79,20 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer ( entrypoint: TensorBoardEntrypoint = TensorBoardEntrypoint.palette, trigger: TensorBoardEntrypointTrigger = TensorBoardEntrypointTrigger.palette, - ) => { + ): void => { sendTelemetryEvent(EventName.TENSORBOARD_SESSION_LAUNCH, undefined, { trigger, entrypoint, }); - return this.createNewSession(); + if (this.experiment.recommendAndUseNewExtension() === 'continueWithPythonExtension') { + void this.createNewSession(); + } }, ), this.commandManager.registerCommand(Commands.RefreshTensorBoard, () => - this.knownSessions.map((w) => w.refresh()), + this.experiment.recommendAndUseNewExtension() === 'continueWithPythonExtension' + ? this.knownSessions.map((w) => w.refresh()) + : undefined, ), ); } diff --git a/src/client/tensorBoard/tensorBoardUsageTracker.ts b/src/client/tensorBoard/tensorBoardUsageTracker.ts index 7c8ea7b00961..b88e416a113f 100644 --- a/src/client/tensorBoard/tensorBoardUsageTracker.ts +++ b/src/client/tensorBoard/tensorBoardUsageTracker.ts @@ -3,7 +3,7 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { TextEditor } from 'vscode'; +import { Disposable, TextEditor } from 'vscode'; import { IExtensionSingleActivationService } from '../activation/types'; import { IDocumentManager } from '../common/application/types'; import { isTestExecution } from '../common/constants'; @@ -12,7 +12,7 @@ import { getDocumentLines } from '../telemetry/importTracker'; import { TensorBoardEntrypointTrigger } from './constants'; import { containsTensorBoardImport } from './helpers'; import { TensorBoardPrompt } from './tensorBoardPrompt'; -import { useNewTensorboardExtension } from './tensorboarExperiment'; +import { TensorboardExperiment } from './tensorboarExperiment'; const testExecution = isTestExecution(); @@ -26,12 +26,20 @@ export class TensorBoardUsageTracker implements IExtensionSingleActivationServic @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(IDisposableRegistry) private disposables: IDisposableRegistry, @inject(TensorBoardPrompt) private prompt: TensorBoardPrompt, - ) {} + @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, + ) { + disposables.push(this); + } + + public dispose(): void { + Disposable.from(...this.disposables).dispose(); + } public async activate(): Promise { - if (useNewTensorboardExtension()) { + if (TensorboardExperiment.isTensorboardExtensionInstalled) { return; } + this.experiment.disposeOnInstallingTensorboard(this); if (testExecution) { await this.activateInternal(); } else { diff --git a/src/client/tensorBoard/tensorboarExperiment.ts b/src/client/tensorBoard/tensorboarExperiment.ts index 25eac8db71da..3cf4cb3c779a 100644 --- a/src/client/tensorBoard/tensorboarExperiment.ts +++ b/src/client/tensorBoard/tensorboarExperiment.ts @@ -1,8 +1,67 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { extensions } from 'vscode'; +import { Disposable, EventEmitter, commands, extensions, l10n, window } from 'vscode'; +import { inject, injectable } from 'inversify'; +import { IDisposable, IDisposableRegistry, IExperimentService } from '../common/types'; +import { RecommendTensobardExtension } from '../common/experiments/groups'; +import { TENSORBOARD_EXTENSION_ID } from '../common/constants'; -export function useNewTensorboardExtension(): boolean { - return !!extensions.getExtension('ms-toolsai.tensorboard'); +@injectable() +export class TensorboardExperiment { + private readonly _onDidChange = new EventEmitter(); + + public readonly onDidChange = this._onDidChange.event; + + private readonly toDisposeWhenTensobardIsInstalled: IDisposable[] = []; + + public static get isTensorboardExtensionInstalled(): boolean { + return !!extensions.getExtension(TENSORBOARD_EXTENSION_ID); + } + + private readonly isExperimentEnabled: boolean; + + constructor( + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(IExperimentService) experiments: IExperimentService, + ) { + this.isExperimentEnabled = experiments.inExperimentSync(RecommendTensobardExtension.experiment); + disposables.push(this._onDidChange); + extensions.onDidChange( + () => + TensorboardExperiment.isTensorboardExtensionInstalled + ? Disposable.from(...this.toDisposeWhenTensobardIsInstalled).dispose() + : undefined, + this, + disposables, + ); + } + + public recommendAndUseNewExtension(): 'continueWithPythonExtension' | 'usingTensorboardExtension' { + if (!this.isExperimentEnabled) { + return 'continueWithPythonExtension'; + } + if (TensorboardExperiment.isTensorboardExtensionInstalled) { + return 'usingTensorboardExtension'; + } + const install = l10n.t('Install Tensorboard Extension'); + window + .showInformationMessage( + l10n.t( + 'Install the TensorBoard extension to use the this functionality. Once installed, select the command `Launch Tensorboard`.', + ), + { modal: true }, + install, + ) + .then((result): void => { + if (result === install) { + void commands.executeCommand('workbench.extensions.installExtension', TENSORBOARD_EXTENSION_ID); + } + }); + return 'usingTensorboardExtension'; + } + + public disposeOnInstallingTensorboard(disposabe: IDisposable): void { + this.toDisposeWhenTensobardIsInstalled.push(disposabe); + } } diff --git a/src/client/tensorBoard/tensorboardIntegration.ts b/src/client/tensorBoard/tensorboardIntegration.ts index 74f69afab84f..22d590d6ee65 100644 --- a/src/client/tensorBoard/tensorboardIntegration.ts +++ b/src/client/tensorBoard/tensorboardIntegration.ts @@ -69,10 +69,7 @@ export class TensorboardExtensionIntegration { public hideCommands(): void { if (this.extensions.getExtension(TENSORBOARD_EXTENSION_ID)) { - console.error('TensorBoard extension is installed'); void commands.executeCommand('setContext', 'python.tensorboardExtInstalled', true); - } else { - console.error('TensorBoard extension not installed'); } } diff --git a/src/client/tensorBoard/terminalWatcher.ts b/src/client/tensorBoard/terminalWatcher.ts index 30ccf7e1726a..5f48def54e43 100644 --- a/src/client/tensorBoard/terminalWatcher.ts +++ b/src/client/tensorBoard/terminalWatcher.ts @@ -4,7 +4,7 @@ import { IExtensionSingleActivationService } from '../activation/types'; import { IDisposable, IDisposableRegistry } from '../common/types'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; -import { useNewTensorboardExtension } from './tensorboarExperiment'; +import { TensorboardExperiment } from './tensorboarExperiment'; // Every 5 min look, through active terminals to see if any are running `tensorboard` @injectable() @@ -13,12 +13,18 @@ export class TerminalWatcher implements IExtensionSingleActivationService, IDisp private handle: NodeJS.Timeout | undefined; - constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} + constructor( + @inject(IDisposableRegistry) private disposables: IDisposableRegistry, + @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, + ) { + disposables.push(this); + } public async activate(): Promise { - if (useNewTensorboardExtension()) { + if (TensorboardExperiment.isTensorboardExtensionInstalled) { return; } + this.experiment.disposeOnInstallingTensorboard(this); const handle = setInterval(() => { // When user runs a command in VSCode terminal, the terminal's name // becomes the program that is currently running. Since tensorboard diff --git a/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts b/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts index 9a46d92c1422..aef90d14eacf 100644 --- a/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts +++ b/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts @@ -1,49 +1,60 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as sinon from 'sinon'; import { assert } from 'chai'; import { CancellationTokenSource } from 'vscode'; +import { instance, mock } from 'ts-mockito'; import { TensorBoardNbextensionCodeLensProvider } from '../../client/tensorBoard/nbextensionCodeLensProvider'; import { MockDocument } from '../mocks/mockDocument'; +import { TensorboardExperiment } from '../../client/tensorBoard/tensorboarExperiment'; -suite('TensorBoard nbextension code lens provider', () => { - let codeLensProvider: TensorBoardNbextensionCodeLensProvider; - let cancelTokenSource: CancellationTokenSource; +[true, false].forEach((tbExtensionInstalled) => { + suite(`Tensorboard Extension is ${tbExtensionInstalled ? 'installed' : 'not installed'}`, () => { + suite.only('TensorBoard nbextension code lens provider', () => { + let experiment: TensorboardExperiment; + let codeLensProvider: TensorBoardNbextensionCodeLensProvider; + let cancelTokenSource: CancellationTokenSource; - setup(() => { - codeLensProvider = new TensorBoardNbextensionCodeLensProvider([]); - cancelTokenSource = new CancellationTokenSource(); - }); - teardown(() => { - cancelTokenSource.dispose(); - }); + setup(() => { + sinon.stub(TensorboardExperiment, 'isTensorboardExtensionInstalled').returns(tbExtensionInstalled); + experiment = mock(); + codeLensProvider = new TensorBoardNbextensionCodeLensProvider([], instance(experiment)); + cancelTokenSource = new CancellationTokenSource(); + }); + teardown(() => { + sinon.restore(); + cancelTokenSource.dispose(); + }); - test('Provide code lens for Python notebook loading tensorboard nbextension', async () => { - const document = new MockDocument('a=1\n%load_ext tensorboard', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length > 0, 'Failed to provide code lens for file loading tensorboard nbextension'); - }); - test('Provide code lens for Python notebook launching tensorboard nbextension', async () => { - const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length > 0, 'Failed to provide code lens for file loading tensorboard nbextension'); - }); - test('Fails when cancellation is signaled', () => { - const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.ipynb', async () => true); - cancelTokenSource.cancel(); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided codelens even after cancellation was requested'); + test('Provide code lens for Python notebook loading tensorboard nbextension', async () => { + const document = new MockDocument('a=1\n%load_ext tensorboard', 'foo.ipynb', async () => true); + const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); + assert.ok(codeLens.length > 0, 'Failed to provide code lens for file loading tensorboard nbextension'); + }); + test('Provide code lens for Python notebook launching tensorboard nbextension', async () => { + const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.ipynb', async () => true); + const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); + assert.ok(codeLens.length > 0, 'Failed to provide code lens for file loading tensorboard nbextension'); + }); + test('Fails when cancellation is signaled', () => { + const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.ipynb', async () => true); + cancelTokenSource.cancel(); + const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); + assert.ok(codeLens.length === 0, 'Provided codelens even after cancellation was requested'); + }); + // Can't verify these cases without running in vscode as we depend on vscode to not call us + // based on the DocumentSelector we provided. See nbExtensionCodeLensProvider.test.ts for that. + // test('Does not provide code lens for Python file loading tensorboard nbextension', async () => { + // const document = new MockDocument('a=1\n%load_ext tensorboard', 'foo.py', async () => true); + // const codeLens = codeLensProvider.provideCodeLenses(document); + // assert.ok(codeLens.length === 0, 'Provided code lens for Python file loading tensorboard nbextension'); + // }); + // test('Does not provide code lens for Python file launching tensorboard nbextension', async () => { + // const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.py', async () => true); + // const codeLens = codeLensProvider.provideCodeLenses(document); + // assert.ok(codeLens.length === 0, 'Provided code lens for Python file loading tensorboard nbextension'); + // }); + }); }); - // Can't verify these cases without running in vscode as we depend on vscode to not call us - // based on the DocumentSelector we provided. See nbExtensionCodeLensProvider.test.ts for that. - // test('Does not provide code lens for Python file loading tensorboard nbextension', async () => { - // const document = new MockDocument('a=1\n%load_ext tensorboard', 'foo.py', async () => true); - // const codeLens = codeLensProvider.provideCodeLenses(document); - // assert.ok(codeLens.length === 0, 'Provided code lens for Python file loading tensorboard nbextension'); - // }); - // test('Does not provide code lens for Python file launching tensorboard nbextension', async () => { - // const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.py', async () => true); - // const codeLens = codeLensProvider.provideCodeLenses(document); - // assert.ok(codeLens.length === 0, 'Provided code lens for Python file loading tensorboard nbextension'); - // }); }); diff --git a/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts b/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts index 9b691c9af17c..07bcce035a7c 100644 --- a/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts +++ b/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts @@ -1,58 +1,72 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as sinon from 'sinon'; import { assert } from 'chai'; import { CancellationTokenSource } from 'vscode'; +import { instance, mock } from 'ts-mockito'; import { TensorBoardImportCodeLensProvider } from '../../client/tensorBoard/tensorBoardImportCodeLensProvider'; import { MockDocument } from '../mocks/mockDocument'; +import { TensorboardExperiment } from '../../client/tensorBoard/tensorboarExperiment'; -suite('TensorBoard import code lens provider', () => { - let codeLensProvider: TensorBoardImportCodeLensProvider; - let cancelTokenSource: CancellationTokenSource; +[true, false].forEach((tbExtensionInstalled) => { + suite(`Tensorboard Extension is ${tbExtensionInstalled ? 'installed' : 'not installed'}`, () => { + suite.only('TensorBoard import code lens provider', () => { + let experiment: TensorboardExperiment; + let codeLensProvider: TensorBoardImportCodeLensProvider; + let cancelTokenSource: CancellationTokenSource; - setup(() => { - codeLensProvider = new TensorBoardImportCodeLensProvider([]); - cancelTokenSource = new CancellationTokenSource(); - }); - teardown(() => { - cancelTokenSource.dispose(); - }); - [ - 'import tensorboard', - 'import foo, tensorboard', - 'import foo, tensorboard, bar', - 'import tensorboardX', - 'import tensorboardX, bar', - 'import torch.profiler', - 'import foo, torch.profiler', - 'from torch.utils import tensorboard', - 'from torch.utils import foo, tensorboard', - 'import torch.utils.tensorboard, foo', - 'from torch import profiler', - ].forEach((importStatement) => { - test(`Provides code lens for Python files containing ${importStatement}`, () => { - const document = new MockDocument(importStatement, 'foo.py', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length > 0, `Failed to provide code lens for file containing ${importStatement} import`); - }); - test(`Provides code lens for Python ipynbs containing ${importStatement}`, () => { - const document = new MockDocument(importStatement, 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok( - codeLens.length > 0, - `Failed to provide code lens for ipynb containing ${importStatement} import`, - ); + setup(() => { + sinon.stub(TensorboardExperiment, 'isTensorboardExtensionInstalled').returns(tbExtensionInstalled); + experiment = mock(); + codeLensProvider = new TensorBoardImportCodeLensProvider([], instance(experiment)); + cancelTokenSource = new CancellationTokenSource(); + }); + teardown(() => { + sinon.restore(); + cancelTokenSource.dispose(); + }); + [ + 'import tensorboard', + 'import foo, tensorboard', + 'import foo, tensorboard, bar', + 'import tensorboardX', + 'import tensorboardX, bar', + 'import torch.profiler', + 'import foo, torch.profiler', + 'from torch.utils import tensorboard', + 'from torch.utils import foo, tensorboard', + 'import torch.utils.tensorboard, foo', + 'from torch import profiler', + ].forEach((importStatement) => { + test(`Provides code lens for Python files containing ${importStatement}`, () => { + const document = new MockDocument(importStatement, 'foo.py', async () => true); + const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); + assert.ok( + codeLens.length > 0, + `Failed to provide code lens for file containing ${importStatement} import`, + ); + }); + test(`Provides code lens for Python ipynbs containing ${importStatement}`, () => { + const document = new MockDocument(importStatement, 'foo.ipynb', async () => true); + const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); + assert.ok( + codeLens.length > 0, + `Failed to provide code lens for ipynb containing ${importStatement} import`, + ); + }); + test('Fails when cancellation is signaled', () => { + const document = new MockDocument(importStatement, 'foo.py', async () => true); + cancelTokenSource.cancel(); + const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); + assert.ok(codeLens.length === 0, 'Provided codelens even after cancellation was requested'); + }); + }); + test('Does not provide code lens if no matching import', () => { + const document = new MockDocument('import foo', 'foo.ipynb', async () => true); + const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); + assert.ok(codeLens.length === 0, 'Provided code lens for file without tensorboard import'); + }); }); - test('Fails when cancellation is signaled', () => { - const document = new MockDocument(importStatement, 'foo.py', async () => true); - cancelTokenSource.cancel(); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided codelens even after cancellation was requested'); - }); - }); - test('Does not provide code lens if no matching import', () => { - const document = new MockDocument('import foo', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided code lens for file without tensorboard import'); }); }); diff --git a/src/test/tensorBoard/tensorBoardPrompt.unit.test.ts b/src/test/tensorBoard/tensorBoardPrompt.unit.test.ts index 6f096e560d70..d94b0d6c5f23 100644 --- a/src/test/tensorBoard/tensorBoardPrompt.unit.test.ts +++ b/src/test/tensorBoard/tensorBoardPrompt.unit.test.ts @@ -7,7 +7,7 @@ import { Common } from '../../client/common/utils/localize'; import { TensorBoardEntrypointTrigger } from '../../client/tensorBoard/constants'; import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; -suite('TensorBoard prompt', () => { +suite.only('TensorBoard prompt', () => { let applicationShell: ApplicationShell; let commandManager: CommandManager; let persistentState: PersistentState; diff --git a/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts b/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts index ff187dd2afc1..54771ab4b6b6 100644 --- a/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts +++ b/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts @@ -1,98 +1,117 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; -import { anything, reset, when } from 'ts-mockito'; +import { anything, instance, mock, reset, when } from 'ts-mockito'; import { TensorBoardUsageTracker } from '../../client/tensorBoard/tensorBoardUsageTracker'; import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; import { MockDocumentManager } from '../mocks/mockDocumentManager'; import { createTensorBoardPromptWithMocks } from './helpers'; import { mockedVSCodeNamespaces } from '../vscode-mock'; +import { TensorboardExperiment } from '../../client/tensorBoard/tensorboarExperiment'; -suite('TensorBoard usage tracker', () => { - let documentManager: MockDocumentManager; - let tensorBoardImportTracker: TensorBoardUsageTracker; - let prompt: TensorBoardPrompt; - let showNativeTensorBoardPrompt: sinon.SinonSpy; +[true, false].forEach((tbExtensionInstalled) => { + suite(`Tensorboard Extension is ${tbExtensionInstalled ? 'installed' : 'not installed'}`, () => { + suite.only('TensorBoard usage tracker', () => { + let experiment: TensorboardExperiment; + let documentManager: MockDocumentManager; + let tensorBoardImportTracker: TensorBoardUsageTracker; + let prompt: TensorBoardPrompt; + let showNativeTensorBoardPrompt: sinon.SinonSpy; - suiteSetup(() => { - reset(mockedVSCodeNamespaces.extensions); - when(mockedVSCodeNamespaces.extensions?.getExtension(anything())).thenReturn(undefined); - }); - suiteTeardown(() => reset(mockedVSCodeNamespaces.extensions)); - setup(() => { - documentManager = new MockDocumentManager(); - prompt = createTensorBoardPromptWithMocks(); - showNativeTensorBoardPrompt = sinon.spy(prompt, 'showNativeTensorBoardPrompt'); - tensorBoardImportTracker = new TensorBoardUsageTracker(documentManager, [], prompt); - }); + suiteSetup(() => { + reset(mockedVSCodeNamespaces.extensions); + when(mockedVSCodeNamespaces.extensions?.getExtension(anything())).thenReturn(undefined); + }); + suiteTeardown(() => reset(mockedVSCodeNamespaces.extensions)); + setup(() => { + sinon.stub(TensorboardExperiment, 'isTensorboardExtensionInstalled').returns(tbExtensionInstalled); + experiment = mock(); + documentManager = new MockDocumentManager(); + prompt = createTensorBoardPromptWithMocks(); + showNativeTensorBoardPrompt = sinon.spy(prompt, 'showNativeTensorBoardPrompt'); + tensorBoardImportTracker = new TensorBoardUsageTracker( + documentManager, + [], + prompt, + instance(experiment), + ); + }); - test('Simple tensorboard import in Python file', async () => { - const document = documentManager.addDocument('import tensorboard', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Simple tensorboardX import in Python file', async () => { - const document = documentManager.addDocument('import tensorboardX', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Simple tensorboard import in Python ipynb', async () => { - const document = documentManager.addDocument('import tensorboard', 'foo.ipynb'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from x.y.tensorboard import z` import', async () => { - const document = documentManager.addDocument('from torch.utils.tensorboard import SummaryWriter', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from x.y import tensorboard` import', async () => { - const document = documentManager.addDocument('from torch.utils import tensorboard', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from tensorboardX import x` import', async () => { - const document = documentManager.addDocument('from tensorboardX import SummaryWriter', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`import x, y` import', async () => { - const document = documentManager.addDocument('import tensorboard, tensorflow', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`import pkg as _` import', async () => { - const document = documentManager.addDocument('import tensorboard as tb', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Show prompt on changed text editor', async () => { - await tensorBoardImportTracker.activate(); - const document = documentManager.addDocument('import tensorboard as tb', 'foo.py'); - await documentManager.showTextDocument(document); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Do not show prompt if no tensorboard import', async () => { - const document = documentManager.addDocument('import tensorflow as tf\nfrom torch.utils import foo', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.notCalled); - }); - test('Do not show prompt if language is not Python', async () => { - const document = documentManager.addDocument( - 'import tensorflow as tf\nfrom torch.utils import foo', - 'foo.cpp', - 'cpp', - ); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.notCalled); + test('Simple tensorboard import in Python file', async () => { + const document = documentManager.addDocument('import tensorboard', 'foo.py'); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('Simple tensorboardX import in Python file', async () => { + const document = documentManager.addDocument('import tensorboardX', 'foo.py'); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('Simple tensorboard import in Python ipynb', async () => { + const document = documentManager.addDocument('import tensorboard', 'foo.ipynb'); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('`from x.y.tensorboard import z` import', async () => { + const document = documentManager.addDocument( + 'from torch.utils.tensorboard import SummaryWriter', + 'foo.py', + ); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('`from x.y import tensorboard` import', async () => { + const document = documentManager.addDocument('from torch.utils import tensorboard', 'foo.py'); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('`from tensorboardX import x` import', async () => { + const document = documentManager.addDocument('from tensorboardX import SummaryWriter', 'foo.py'); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('`import x, y` import', async () => { + const document = documentManager.addDocument('import tensorboard, tensorflow', 'foo.py'); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('`import pkg as _` import', async () => { + const document = documentManager.addDocument('import tensorboard as tb', 'foo.py'); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('Show prompt on changed text editor', async () => { + await tensorBoardImportTracker.activate(); + const document = documentManager.addDocument('import tensorboard as tb', 'foo.py'); + await documentManager.showTextDocument(document); + assert.ok(showNativeTensorBoardPrompt.calledOnce); + }); + test('Do not show prompt if no tensorboard import', async () => { + const document = documentManager.addDocument( + 'import tensorflow as tf\nfrom torch.utils import foo', + 'foo.py', + ); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.notCalled); + }); + test('Do not show prompt if language is not Python', async () => { + const document = documentManager.addDocument( + 'import tensorflow as tf\nfrom torch.utils import foo', + 'foo.cpp', + 'cpp', + ); + await documentManager.showTextDocument(document); + await tensorBoardImportTracker.activate(); + assert.ok(showNativeTensorBoardPrompt.notCalled); + }); + }); }); }); From 10b98d34b51b501531ac21027fcab59b12c8e182 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 17 Oct 2023 20:29:54 +1100 Subject: [PATCH 0253/1136] Deprecate the log directory setting (#22236) --- package.json | 4 +++- package.nls.json | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b13f9eae0a3..c89ccf624a27 100644 --- a/package.json +++ b/package.json @@ -1100,7 +1100,9 @@ "default": "", "description": "%python.tensorBoard.logDirectory.description%", "scope": "resource", - "type": "string" + "type": "string", + "markdownDeprecationMessage": "%python.tensorBoard.logDirectory.markdownDeprecationMessage%", + "deprecationMessage": "%python.tensorBoard.logDirectory.deprecationMessage%" }, "python.terminal.activateEnvInCurrentTerminal": { "default": false, diff --git a/package.nls.json b/package.nls.json index 87692fb7c1a8..c738b3692daf 100644 --- a/package.nls.json +++ b/package.nls.json @@ -182,6 +182,8 @@ "python.pipenvPath.description": "Path to the pipenv executable to use for activation.", "python.poetryPath.description": "Path to the poetry executable.", "python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.", + "python.tensorBoard.logDirectory.markdownDeprecationMessage": "Tensorboard support has been moved to the extension [Tensorboard extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.tensorboard). Instead use the setting `tensorBoard.logDirectory`.", + "python.tensorBoard.logDirectory.deprecationMessage": "Tensorboard support has been moved to the extension Tensorboard extension. Instead use the setting `tensorBoard.logDirectory`.", "python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.", "python.terminal.activateEnvironment.description": "Activate Python Environment in all Terminals created.", "python.terminal.executeInFileDir.description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.", From ebaf8fe0d587cfbc190bd89ad4d584c35ff57bd1 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 17 Oct 2023 13:29:27 -0700 Subject: [PATCH 0254/1136] Fix experiment telemetry related to optInto/optOutFrom settings (#22241) cc/ @luabud --- src/client/common/experiments/service.ts | 6 ++++-- src/client/telemetry/index.ts | 8 ++++---- src/test/common/experiments/service.unit.test.ts | 16 +++++++++++----- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/client/common/experiments/service.ts b/src/client/common/experiments/service.ts index 270f91512809..3d85b99a26ff 100644 --- a/src/client/common/experiments/service.ts +++ b/src/client/common/experiments/service.ts @@ -257,8 +257,10 @@ function sendOptInOptOutTelemetry(optedIn: string[], optedOut: string[], package const sanitizedOptedIn = optedIn.filter((exp) => optedInEnumValues.includes(exp)); const sanitizedOptedOut = optedOut.filter((exp) => optedOutEnumValues.includes(exp)); + JSON.stringify(sanitizedOptedIn.sort()); + sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS, undefined, { - optedInto: sanitizedOptedIn, - optedOutFrom: sanitizedOptedOut, + optedInto: JSON.stringify(sanitizedOptedIn.sort()), + optedOutFrom: JSON.stringify(sanitizedOptedOut.sort()), }); } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index f69da6046254..ba65c4d1913f 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1405,14 +1405,14 @@ export interface IEventNamePropertyMapping { [EventName.PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS]: { /** * List of valid experiments in the python.experiments.optInto setting - * @type {string[]} + * @type {string} */ - optedInto: string[]; + optedInto: string; /** * List of valid experiments in the python.experiments.optOutFrom setting - * @type {string[]} + * @type {string} */ - optedOutFrom: string[]; + optedOutFrom: string; }; /** * Telemetry event sent when LS is started for workspace (workspace folder in case of multi-root) diff --git a/src/test/common/experiments/service.unit.test.ts b/src/test/common/experiments/service.unit.test.ts index 1d96f2e0bd70..ab05db6da5a1 100644 --- a/src/test/common/experiments/service.unit.test.ts +++ b/src/test/common/experiments/service.unit.test.ts @@ -491,7 +491,10 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[1]; - assert.deepStrictEqual(properties, { optedInto: ['foo'], optedOutFrom: ['bar'] }); + assert.deepStrictEqual(properties, { + optedInto: JSON.stringify(['foo']), + optedOutFrom: JSON.stringify(['bar']), + }); }); test('Set telemetry properties to empty arrays if no experiments have been opted into or out from', async () => { @@ -523,7 +526,7 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[1]; - assert.deepStrictEqual(properties, { optedInto: [], optedOutFrom: [] }); + assert.deepStrictEqual(properties, { optedInto: '[]', optedOutFrom: '[]' }); }); test('If the entered value for a setting contains "All", do not expand it to be a list of all experiments, and pass it as-is', async () => { @@ -555,7 +558,10 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[0]; - assert.deepStrictEqual(properties, { optedInto: ['All'], optedOutFrom: ['All'] }); + assert.deepStrictEqual(properties, { + optedInto: JSON.stringify(['All']), + optedOutFrom: JSON.stringify(['All']), + }); }); // This is an unlikely scenario. @@ -577,7 +583,7 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[1]; - assert.deepStrictEqual(properties, { optedInto: [], optedOutFrom: [] }); + assert.deepStrictEqual(properties, { optedInto: '[]', optedOutFrom: '[]' }); }); // This is also an unlikely scenario. @@ -608,7 +614,7 @@ suite('Experimentation service', () => { await experimentService.activate(); const { properties } = telemetryEvents[1]; - assert.deepStrictEqual(properties, { optedInto: [], optedOutFrom: [] }); + assert.deepStrictEqual(properties, { optedInto: '[]', optedOutFrom: '[]' }); }); }); }); From 754f8effa482d2e37a8dfba588da4d51374e2a63 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 17 Oct 2023 13:50:28 -0700 Subject: [PATCH 0255/1136] remove node deletion for error tolerant discovery (#22207) helps with a part of https://github.com/microsoft/vscode-python/issues/21757 --- pythonFiles/vscode_pytest/__init__.py | 12 ++-- .../testController/common/resultResolver.ts | 8 --- .../pytest/pytestDiscoveryAdapter.ts | 5 +- .../resultResolver.unit.test.ts | 63 +++++++++++++++++++ 4 files changed, 72 insertions(+), 16 deletions(-) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 8349e1aa893d..300f145b6f75 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -302,12 +302,12 @@ def pytest_sessionfinish(session, exitstatus): session -- the pytest session object. exitstatus -- the status code of the session. - 0: All tests passed successfully. - 1: One or more tests failed. - 2: Pytest was unable to start or run any tests due to issues with test discovery or test collection. - 3: Pytest was interrupted by the user, for example by pressing Ctrl+C during test execution. - 4: Pytest encountered an internal error or exception during test execution. - 5: Pytest was unable to find any tests to run. + Exit code 0: All tests were collected and passed successfully + Exit code 1: Tests were collected and run but some of the tests failed + Exit code 2: Test execution was interrupted by the user + Exit code 3: Internal error happened while executing tests + Exit code 4: pytest command line usage error + Exit code 5: No tests were collected """ cwd = pathlib.Path.cwd() if IS_DISCOVERY: diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index aaf82b143823..22a13090e1b1 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -103,14 +103,6 @@ export class PythonResultResolver implements ITestResultResolver { // If the test root for this folder exists: Workspace refresh, update its children. // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. populateTestTree(this.testController, rawTestData.tests, undefined, this, token); - } else { - // Delete everything from the test controller. - const errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`); - this.testController.items.replace([]); - // Add back the error node if it exists. - if (errorNode !== undefined) { - this.testController.items.add(errorNode); - } } sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 09ca36849000..daaaec04ee1c 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -120,9 +120,10 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } }); result?.proc?.on('close', (code, signal) => { - if (code !== 0) { + // pytest exits with code of 5 when 0 tests are found- this is not a failure for discovery. + if (code !== 0 && code !== 5) { traceError( - `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`, + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}. Creating and sending error discovery payload`, ); // if the child process exited with a non-zero exit code, then we need to send the error payload. this.testServer.triggerDiscoveryDataReceivedEvent({ diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts index 2078c72e8cf6..5ecf75987b3c 100644 --- a/src/test/testing/testController/resultResolver.unit.test.ts +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -195,6 +195,69 @@ suite('Result Resolver tests', () => { cancelationToken, // token ); }); + test('resolveDiscovery should create error and not clear test items to allow for error tolerant discovery', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const errorMessage = 'error msg A'; + const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`; + + // create test result node + const tests: DiscoveredTestNode = { + path: 'path', + name: 'name', + type_: 'folder', + id_: 'id', + children: [], + }; + // stub out return values of functions called in resolveDiscovery + const errorPayload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: [errorMessage], + }; + const regPayload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + error: [errorMessage], + tests, + }; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + + // stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + + // stub out functionality of populateTestTreeStub which is called in resolveDiscovery + sinon.stub(util, 'populateTestTree').returns(); + // add spies to insure these aren't called + const deleteSpy = sinon.spy(testController.items, 'delete'); + const replaceSpy = sinon.spy(testController.items, 'replace'); + // call resolve discovery + let deferredTillEOT: Deferred = createDeferred(); + resultResolver.resolveDiscovery(regPayload, deferredTillEOT, cancelationToken); + deferredTillEOT = createDeferred(); + resultResolver.resolveDiscovery(errorPayload, deferredTillEOT, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // builds an error node root + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider); + // builds an error item + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + + if (!deleteSpy.calledOnce) { + throw new Error("The delete method was called, but it shouldn't have been."); + } + if (replaceSpy.called) { + throw new Error("The replace method was called, but it shouldn't have been."); + } + }); }); suite('Test execution result resolver', () => { let resultResolver: ResultResolver.PythonResultResolver; From 44053a22aafaa4ae1d661f867b4735b237308a14 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 17 Oct 2023 13:50:59 -0700 Subject: [PATCH 0256/1136] add hookwrappers to pytest plugin to ensure run (#22240) fixes https://github.com/microsoft/vscode-python/issues/22232. From [this discussion](https://github.com/pytest-dev/pytest/discussions/11509), learned that some pytest hooks are meant to be unique and only one will be called per run. If multiple plugins are at play then another plugin the user has might override our plugin. Added the hookwrapper so our is always run. --- pythonFiles/vscode_pytest/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 300f145b6f75..1718d435bb23 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -183,6 +183,7 @@ class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): tests: Dict[str, TestOutcome] +@pytest.hookimpl(hookwrapper=True, trylast=True) def pytest_report_teststatus(report, config): """ A pytest hook that is called when a test is called. It is called 3 times per test, @@ -223,6 +224,7 @@ def pytest_report_teststatus(report, config): "success", collected_test if collected_test else None, ) + yield ERROR_MESSAGE_CONST = { From 4caa20735b4fa7832e3d62e884cbc04b482f2ad8 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 17 Oct 2023 15:55:24 -0700 Subject: [PATCH 0257/1136] add wrapper hook for pytest_runtest_protocol (#22243) fixes https://github.com/microsoft/vscode-python/issues/22232. From https://github.com/pytest-dev/pytest/discussions/11509, learned that some pytest hooks are meant to be unique and only one will be called per run. If multiple plugins are at play then another plugin the user has might override our plugin. Added the hookwrapper so our is always run. Same as https://github.com/microsoft/vscode-python/pull/22240 --- pythonFiles/vscode_pytest/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 1718d435bb23..0767b85c5249 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -235,6 +235,7 @@ def pytest_report_teststatus(report, config): } +@pytest.hookimpl(hookwrapper=True, trylast=True) def pytest_runtest_protocol(item, nextitem): map_id_to_path[item.nodeid] = get_node_path(item) skipped = check_skipped_wrapper(item) @@ -257,6 +258,7 @@ def pytest_runtest_protocol(item, nextitem): "success", collected_test if collected_test else None, ) + yield def check_skipped_wrapper(item): From 01c7665e37f4674a6a574d38f4f7af9344ec0485 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 17 Oct 2023 16:45:44 -0700 Subject: [PATCH 0258/1136] add correct retrieval of workspace adapter for test discovery in multiroot context (#22246) fixes: https://github.com/microsoft/vscode-python/issues/22218 --- .../testing/testController/controller.ts | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index a87017a26a51..329326d84af9 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -269,13 +269,20 @@ export class PythonTestController implements ITestController, IExtensionSingleAc if (settings.testing.pytestEnabled) { if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { traceInfo(`Running discovery for pytest using the new test adapter.`); - const testAdapter = - this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); - testAdapter.discoverTests( - this.testController, - this.refreshCancellation.token, - this.pythonExecFactory, - ); + if (workspace && workspace.uri) { + const testAdapter = this.testAdapters.get(workspace.uri); + if (testAdapter) { + testAdapter.discoverTests( + this.testController, + this.refreshCancellation.token, + this.pythonExecFactory, + ); + } else { + traceError('Unable to find test adapter for workspace.'); + } + } else { + traceError('Unable to find workspace for given file'); + } } else { // else use OLD test discovery mechanism await this.pytest.refreshTestData(this.testController, uri, this.refreshCancellation.token); @@ -283,13 +290,21 @@ export class PythonTestController implements ITestController, IExtensionSingleAc } else if (settings.testing.unittestEnabled) { if (pythonTestAdapterRewriteEnabled(this.serviceContainer)) { traceInfo(`Running discovery for unittest using the new test adapter.`); - const testAdapter = - this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); - testAdapter.discoverTests( - this.testController, - this.refreshCancellation.token, - this.pythonExecFactory, - ); + traceInfo(`Running discovery for pytest using the new test adapter.`); + if (workspace && workspace.uri) { + const testAdapter = this.testAdapters.get(workspace.uri); + if (testAdapter) { + testAdapter.discoverTests( + this.testController, + this.refreshCancellation.token, + this.pythonExecFactory, + ); + } else { + traceError('Unable to find test adapter for workspace.'); + } + } else { + traceError('Unable to find workspace for given file'); + } } else { // else use OLD test discovery mechanism await this.unittest.refreshTestData(this.testController, uri, this.refreshCancellation.token); From 7cb3593c1f998d109721f783a0b80ae878dd0164 Mon Sep 17 00:00:00 2001 From: Bolton Bailey Date: Wed, 18 Oct 2023 13:29:57 -0500 Subject: [PATCH 0259/1136] Remove unmatched parenthesis from error message (#22254) Remove unmatched parenthesis from error message closes: https://github.com/microsoft/vscode-python/issues/22253 --- src/client/terminals/codeExecution/helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index c560de9c17b7..058c78e332a3 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -152,7 +152,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { return undefined; } if (activeEditor.document.languageId !== PYTHON_LANGUAGE) { - this.applicationShell.showErrorMessage(l10n.t('The active file is not a Python source file)')); + this.applicationShell.showErrorMessage(l10n.t('The active file is not a Python source file')); return undefined; } if (activeEditor.document.isDirty) { From 8becc7654d3765520b99b973fb61e696748daa66 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 18 Oct 2023 14:44:47 -0700 Subject: [PATCH 0260/1136] Remove unused text edit code (#22244) This is part of removing the formatting support from the extension. --- ThirdPartyNotices-Repository.txt | 42 +- build/webpack/common.js | 1 - package-lock.json | 315 ++++++++------- package.json | 2 - src/client/common/editor.ts | 400 ------------------- src/client/common/experiments/groups.ts | 3 - src/client/common/serviceRegistry.ts | 3 - src/client/common/types.ts | 6 - src/test/common/installer.test.ts | 3 - src/test/common/moduleInstaller.test.ts | 3 - src/test/common/serviceRegistry.unit.test.ts | 3 - 11 files changed, 180 insertions(+), 601 deletions(-) delete mode 100644 src/client/common/editor.ts diff --git a/ThirdPartyNotices-Repository.txt b/ThirdPartyNotices-Repository.txt index c8854a208e5a..9e7e822af1bb 100644 --- a/ThirdPartyNotices-Repository.txt +++ b/ThirdPartyNotices-Repository.txt @@ -6,18 +6,17 @@ Microsoft Python extension for Visual Studio Code incorporates third party mater 1. Go for Visual Studio Code (https://github.com/Microsoft/vscode-go) 2. Files from the Python Project (https://www.python.org/) -3. Google Diff Match and Patch (https://github.com/GerHobbelt/google-diff-match-patch) -4. omnisharp-vscode (https://github.com/OmniSharp/omnisharp-vscode) -5. PTVS (https://github.com/Microsoft/PTVS) -6. Python documentation (https://docs.python.org/) -7. python-functools32 (https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py) -8. pythonVSCode (https://github.com/DonJayamanne/pythonVSCode) -9. Sphinx (http://sphinx-doc.org/) -10. nteract (https://github.com/nteract/nteract) -11. less-plugin-inline-urls (https://github.com/less/less-plugin-inline-urls/) -12. vscode-cpptools (https://github.com/microsoft/vscode-cpptools) -13. mocha (https://github.com/mochajs/mocha) -14. get-pip (https://github.com/pypa/get-pip) +3. omnisharp-vscode (https://github.com/OmniSharp/omnisharp-vscode) +4. PTVS (https://github.com/Microsoft/PTVS) +5. Python documentation (https://docs.python.org/) +6. python-functools32 (https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py) +7. pythonVSCode (https://github.com/DonJayamanne/pythonVSCode) +8. Sphinx (http://sphinx-doc.org/) +9. nteract (https://github.com/nteract/nteract) +10. less-plugin-inline-urls (https://github.com/less/less-plugin-inline-urls/) +11. vscode-cpptools (https://github.com/microsoft/vscode-cpptools) +12. mocha (https://github.com/mochajs/mocha) +13. get-pip (https://github.com/pypa/get-pip) %% Go for Visual Studio Code NOTICES, INFORMATION, AND LICENSE BEGIN HERE @@ -244,25 +243,6 @@ OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ========================================= END OF Files from the Python Project NOTICES, INFORMATION, AND LICENSE -%% Google Diff Match and Patch NOTICES, INFORMATION, AND LICENSE BEGIN HERE -========================================= - * Copyright 2006 Google Inc. - * http://code.google.com/p/google-diff-match-patch/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -========================================= -END OF Google Diff Match and Patch NOTICES, INFORMATION, AND LICENSE - %% omnisharp-vscode NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= Copyright (c) Microsoft Corporation diff --git a/build/webpack/common.js b/build/webpack/common.js index d5235db54967..c7f7460adf86 100644 --- a/build/webpack/common.js +++ b/build/webpack/common.js @@ -21,7 +21,6 @@ exports.nodeModulesToExternalize = [ 'unicode/category/Nd', 'unicode/category/Pc', 'source-map-support', - 'diff-match-patch', 'sudo-prompt', 'node-stream-zip', 'xml2js', diff --git a/package-lock.json b/package-lock.json index c4e177468706..61c379ee6953 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@vscode/extension-telemetry": "^0.8.4", "@vscode/jupyter-lsp-middleware": "^0.2.50", "arch": "^2.1.0", - "diff-match-patch": "^1.0.0", "fs-extra": "^10.0.1", "glob": "^7.2.0", "hash.js": "^1.1.7", @@ -52,7 +51,6 @@ "@types/chai": "^4.1.2", "@types/chai-arrays": "^2.0.0", "@types/chai-as-promised": "^7.1.0", - "@types/diff-match-patch": "^1.0.32", "@types/download": "^8.0.1", "@types/fs-extra": "^9.0.13", "@types/glob": "^7.2.0", @@ -397,12 +395,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", - "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -446,22 +444,22 @@ "dev": true }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -544,9 +542,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -576,13 +574,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -590,9 +588,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.6.tgz", - "integrity": "sha512-EIQu22vNkceq3LbjAq7knDf/UmtI2qbcNI8GRBlijez6TpQLvSodJPYfydQmNA5buwkxxxa/PVI44jjYZ+/cLw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -624,45 +622,46 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template/node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.6.tgz", - "integrity": "sha512-53CijMvKlLIDlOTrdWiHileRddlIiwUIyCKqYa7lYnnPldXCG5dUSN38uT0cA6i7rHWNKJLH0VU/Kxdr1GzB3w==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.6", - "@babel/types": "^7.22.5", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -671,12 +670,13 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -700,13 +700,13 @@ } }, "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1422,28 +1422,22 @@ "dev": true }, "node_modules/@types/decompress": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.4.tgz", - "integrity": "sha512-/C8kTMRTNiNuWGl5nEyKbPiMv6HA+0RbEXzFhFBEzASM6+oa4tJro9b8nj7eRlOFfuLdzUU+DS/GPDlvvzMOhA==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.5.tgz", + "integrity": "sha512-LdL+kbcKGs9TzvB/K+OBGzPfDoP6gwwTsykYjodlzUJUUYp/43c1p1jE5YTtz3z4Ml90iruvBXbJ6+kDvb3WSQ==", "dev": true, "dependencies": { "@types/node": "*" } }, - "node_modules/@types/diff-match-patch": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.32.tgz", - "integrity": "sha512-bPYT5ECFiblzsVzyURaNhljBH2Gh1t9LowgUwciMrNAhFewLkHT2H0Mto07Y4/3KCOGZHRQll3CTtQZ0X11D/A==", - "dev": true - }, "node_modules/@types/download": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@types/download/-/download-8.0.1.tgz", - "integrity": "sha512-t5DjMD6Y1DxjXtEHl7Kt+nQn9rOmVLYD8p4Swrcc5QpgyqyqR2gXTIK6RwwMnNeFJ+ZIiIW789fQKzCrK7AOFA==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@types/download/-/download-8.0.3.tgz", + "integrity": "sha512-IDwXjU7zCtuFVvI0Plnb02TpXyj3RA4YeOKQvEfsjdJeWxZ9hTl6lxeNsU2bLWn0aeAS7fyMl74w/TbdOlS2KQ==", "dev": true, "dependencies": { "@types/decompress": "*", - "@types/got": "^8", + "@types/got": "^9", "@types/node": "*" } }, @@ -1499,12 +1493,28 @@ } }, "node_modules/@types/got": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/@types/got/-/got-8.3.6.tgz", - "integrity": "sha512-nvLlj+831dhdm4LR2Ly+HTpdLyBaMynoOr6wpIxS19d/bPeHQxFU5XQ6Gp6ohBpxvCWZM1uHQIC2+ySRH1rGrQ==", + "version": "9.6.12", + "resolved": "https://registry.npmjs.org/@types/got/-/got-9.6.12.tgz", + "integrity": "sha512-X4pj/HGHbXVLqTpKjA2ahI4rV/nNBc9mGO2I/0CgAra+F2dKgMXnENv2SRpemScBzBAI4vMelIVYViQxlSE6xA==", "dev": true, "dependencies": { - "@types/node": "*" + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/got/node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" } }, "node_modules/@types/json-schema": { @@ -1602,6 +1612,12 @@ "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", "dev": true }, + "node_modules/@types/tough-cookie": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", + "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==", + "dev": true + }, "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -4865,11 +4881,6 @@ "node": ">=0.3.1" } }, - "node_modules/diff-match-patch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", - "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" - }, "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -15621,12 +15632,12 @@ } }, "@babel/generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", - "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "requires": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -15663,19 +15674,19 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { @@ -15737,9 +15748,9 @@ "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helper-validator-option": { @@ -15760,20 +15771,20 @@ } }, "@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.6.tgz", - "integrity": "sha512-EIQu22vNkceq3LbjAq7knDf/UmtI2qbcNI8GRBlijez6TpQLvSodJPYfydQmNA5buwkxxxa/PVI44jjYZ+/cLw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true }, "@babel/runtime": { @@ -15796,52 +15807,54 @@ } }, "@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "dependencies": { "@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "requires": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } } } }, "@babel/traverse": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.6.tgz", - "integrity": "sha512-53CijMvKlLIDlOTrdWiHileRddlIiwUIyCKqYa7lYnnPldXCG5dUSN38uT0cA6i7rHWNKJLH0VU/Kxdr1GzB3w==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.6", - "@babel/types": "^7.22.5", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, "dependencies": { "@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "requires": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "debug": { @@ -15856,13 +15869,13 @@ } }, "@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "requires": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -16443,28 +16456,22 @@ "dev": true }, "@types/decompress": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.4.tgz", - "integrity": "sha512-/C8kTMRTNiNuWGl5nEyKbPiMv6HA+0RbEXzFhFBEzASM6+oa4tJro9b8nj7eRlOFfuLdzUU+DS/GPDlvvzMOhA==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.5.tgz", + "integrity": "sha512-LdL+kbcKGs9TzvB/K+OBGzPfDoP6gwwTsykYjodlzUJUUYp/43c1p1jE5YTtz3z4Ml90iruvBXbJ6+kDvb3WSQ==", "dev": true, "requires": { "@types/node": "*" } }, - "@types/diff-match-patch": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.32.tgz", - "integrity": "sha512-bPYT5ECFiblzsVzyURaNhljBH2Gh1t9LowgUwciMrNAhFewLkHT2H0Mto07Y4/3KCOGZHRQll3CTtQZ0X11D/A==", - "dev": true - }, "@types/download": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@types/download/-/download-8.0.1.tgz", - "integrity": "sha512-t5DjMD6Y1DxjXtEHl7Kt+nQn9rOmVLYD8p4Swrcc5QpgyqyqR2gXTIK6RwwMnNeFJ+ZIiIW789fQKzCrK7AOFA==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@types/download/-/download-8.0.3.tgz", + "integrity": "sha512-IDwXjU7zCtuFVvI0Plnb02TpXyj3RA4YeOKQvEfsjdJeWxZ9hTl6lxeNsU2bLWn0aeAS7fyMl74w/TbdOlS2KQ==", "dev": true, "requires": { "@types/decompress": "*", - "@types/got": "^8", + "@types/got": "^9", "@types/node": "*" } }, @@ -16520,12 +16527,27 @@ } }, "@types/got": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/@types/got/-/got-8.3.6.tgz", - "integrity": "sha512-nvLlj+831dhdm4LR2Ly+HTpdLyBaMynoOr6wpIxS19d/bPeHQxFU5XQ6Gp6ohBpxvCWZM1uHQIC2+ySRH1rGrQ==", + "version": "9.6.12", + "resolved": "https://registry.npmjs.org/@types/got/-/got-9.6.12.tgz", + "integrity": "sha512-X4pj/HGHbXVLqTpKjA2ahI4rV/nNBc9mGO2I/0CgAra+F2dKgMXnENv2SRpemScBzBAI4vMelIVYViQxlSE6xA==", "dev": true, "requires": { - "@types/node": "*" + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + }, + "dependencies": { + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } } }, "@types/json-schema": { @@ -16623,6 +16645,12 @@ "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", "dev": true }, + "@types/tough-cookie": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", + "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==", + "dev": true + }, "@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -19200,11 +19228,6 @@ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, - "diff-match-patch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", - "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" - }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", diff --git a/package.json b/package.json index c89ccf624a27..716335f73767 100644 --- a/package.json +++ b/package.json @@ -1990,7 +1990,6 @@ "@vscode/extension-telemetry": "^0.8.4", "@vscode/jupyter-lsp-middleware": "^0.2.50", "arch": "^2.1.0", - "diff-match-patch": "^1.0.0", "fs-extra": "^10.0.1", "glob": "^7.2.0", "hash.js": "^1.1.7", @@ -2029,7 +2028,6 @@ "@types/chai": "^4.1.2", "@types/chai-arrays": "^2.0.0", "@types/chai-as-promised": "^7.1.0", - "@types/diff-match-patch": "^1.0.32", "@types/download": "^8.0.1", "@types/fs-extra": "^9.0.13", "@types/glob": "^7.2.0", diff --git a/src/client/common/editor.ts b/src/client/common/editor.ts deleted file mode 100644 index f08d73194d41..000000000000 --- a/src/client/common/editor.ts +++ /dev/null @@ -1,400 +0,0 @@ -import { Diff, diff_match_patch } from 'diff-match-patch'; -import { injectable } from 'inversify'; -import * as md5 from 'md5'; -import { EOL } from 'os'; -import * as path from 'path'; -import { Position, Range, TextDocument, TextEdit, Uri, WorkspaceEdit } from 'vscode'; -import { IFileSystem } from '../common/platform/types'; -import { traceError } from '../logging'; -import { WrappedError } from './errors/errorUtils'; -import { IEditorUtils } from './types'; -import { isNotebookCell } from './utils/misc'; - -// Code borrowed from goFormat.ts (Go Extension for VS Code) -enum EditAction { - Delete, - Insert, - Replace, -} - -const NEW_LINE_LENGTH = EOL.length; - -class Patch { - public diffs!: Diff[]; - public start1!: number; - public start2!: number; - public length1!: number; - public length2!: number; -} - -class Edit { - public action: EditAction; - public start: Position; - public end!: Position; - public text: string; - - constructor(action: number, start: Position) { - this.action = action; - this.start = start; - this.text = ''; - } - - public apply(): TextEdit { - switch (this.action) { - case EditAction.Insert: - return TextEdit.insert(this.start, this.text); - case EditAction.Delete: - return TextEdit.delete(new Range(this.start, this.end)); - case EditAction.Replace: - return TextEdit.replace(new Range(this.start, this.end), this.text); - default: - return new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), ''); - } - } -} - -export function getTextEditsFromPatch(before: string, patch: string): TextEdit[] { - if (patch.startsWith('---')) { - // Strip the first two lines - patch = patch.substring(patch.indexOf('@@')); - } - if (patch.length === 0) { - return []; - } - // Remove the text added by unified_diff - // # Work around missing newline (http://bugs.python.org/issue2142). - patch = patch.replace(/\\ No newline at end of file[\r\n]/, ''); - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - const d = new dmp.diff_match_patch(); - const patches = patch_fromText.call(d, patch); - if (!Array.isArray(patches) || patches.length === 0) { - throw new Error('Unable to parse Patch string'); - } - const textEdits: TextEdit[] = []; - - // Add line feeds and build the text edits - patches.forEach((p) => { - p.diffs.forEach((diff) => { - diff[1] += EOL; - }); - getTextEditsInternal(before, p.diffs, p.start1).forEach((edit) => textEdits.push(edit.apply())); - }); - - return textEdits; -} -export function getWorkspaceEditsFromPatch( - filePatches: string[], - workspaceRoot: string | undefined, - fs: IFileSystem, -): WorkspaceEdit { - const workspaceEdit = new WorkspaceEdit(); - filePatches.forEach((patch) => { - const indexOfAtAt = patch.indexOf('@@'); - if (indexOfAtAt === -1) { - return; - } - const fileNameLines = patch - .substring(0, indexOfAtAt) - .split(/\r?\n/g) - .map((line) => line.trim()) - .filter((line) => line.length > 0 && line.toLowerCase().endsWith('.py') && line.indexOf(' a') > 0); - - if (patch.startsWith('---')) { - // Strip the first two lines - patch = patch.substring(indexOfAtAt); - } - if (patch.length === 0) { - return; - } - // We can't find the find name - if (fileNameLines.length === 0) { - return; - } - - let fileName = fileNameLines[0].substring(fileNameLines[0].indexOf(' a') + 3).trim(); - fileName = workspaceRoot && !path.isAbsolute(fileName) ? path.resolve(workspaceRoot, fileName) : fileName; - if (!fs.fileExistsSync(fileName)) { - return; - } - - // Remove the text added by unified_diff - // # Work around missing newline (http://bugs.python.org/issue2142). - patch = patch.replace(/\\ No newline at end of file[\r\n]/, ''); - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - const d = new dmp.diff_match_patch(); - const patches = patch_fromText.call(d, patch); - if (!Array.isArray(patches) || patches.length === 0) { - throw new Error('Unable to parse Patch string'); - } - - const fileSource = fs.readFileSync(fileName); - const fileUri = Uri.file(fileName); - - // Add line feeds and build the text edits - patches.forEach((p) => { - p.diffs.forEach((diff) => { - diff[1] += EOL; - }); - - getTextEditsInternal(fileSource, p.diffs, p.start1).forEach((edit) => { - switch (edit.action) { - case EditAction.Delete: - workspaceEdit.delete(fileUri, new Range(edit.start, edit.end)); - break; - case EditAction.Insert: - workspaceEdit.insert(fileUri, edit.start, edit.text); - break; - case EditAction.Replace: - workspaceEdit.replace(fileUri, new Range(edit.start, edit.end), edit.text); - break; - default: - break; - } - }); - }); - }); - - return workspaceEdit; -} - -function getTextEditsInternal(before: string, diffs: [number, string][], startLine: number = 0): Edit[] { - let line = startLine; - let character = 0; - const beforeLines = before.split(/\r?\n/g); - if (line > 0) { - beforeLines.filter((_l, i) => i < line).forEach((l) => (character += l.length + NEW_LINE_LENGTH)); - } - const edits: Edit[] = []; - let edit: Edit | null = null; - let end: Position; - - for (let i = 0; i < diffs.length; i += 1) { - let start = new Position(line, character); - // Compute the line/character after the diff is applied. - - for (let curr = 0; curr < diffs[i][1].length; curr += 1) { - if (diffs[i][1][curr] !== '\n') { - character += 1; - } else { - character = 0; - line += 1; - } - } - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - - switch (diffs[i][0]) { - case dmp.DIFF_DELETE: - if ( - beforeLines[line - 1].length === 0 && - beforeLines[start.line - 1] && - beforeLines[start.line - 1].length === 0 - ) { - // We're asked to delete an empty line which only contains `/\r?\n/g`. The last line is also empty. - // Delete the `\n` from the last line instead of deleting `\n` from the current line - // This change ensures that the last line in the file, which won't contain `\n` is deleted - start = new Position(start.line - 1, 0); - end = new Position(line - 1, 0); - } else { - end = new Position(line, character); - } - if (edit === null) { - edit = new Edit(EditAction.Delete, start); - } else if (edit.action !== EditAction.Delete) { - throw new Error('cannot format due to an internal error.'); - } - edit.end = end; - break; - - case dmp.DIFF_INSERT: - if (edit === null) { - edit = new Edit(EditAction.Insert, start); - } else if (edit.action === EditAction.Delete) { - edit.action = EditAction.Replace; - } - // insert and replace edits are all relative to the original state - // of the document, so inserts should reset the current line/character - // position to the start. - line = start.line; - character = start.character; - edit.text += diffs[i][1]; - break; - - case dmp.DIFF_EQUAL: - if (edit !== null) { - edits.push(edit); - edit = null; - } - break; - } - } - - if (edit !== null) { - edits.push(edit); - } - - return edits; -} - -export async function getTempFileWithDocumentContents(document: TextDocument, fs: IFileSystem): Promise { - // Don't create file in temp folder since external utilities - // look into configuration files in the workspace and are not - // to find custom rules if file is saved in a random disk location. - // This means temp file has to be created in the same folder - // as the original one and then removed. - // Use a .tmp file extension (instead of the original extension) - // because the language server is watching the file system for Python - // file add/delete/change and we don't want this temp file to trigger it. - - let fileName = `${document.uri.fsPath}.${md5(document.uri.fsPath + document.uri.fragment)}.tmp`; - try { - // When dealing with untitled notebooks, there's no original physical file, hence create a temp file. - if (isNotebookCell(document.uri) && !(await fs.fileExists(document.uri.fsPath))) { - fileName = ( - await fs.createTemporaryFile(`${path.basename(document.uri.fsPath)}-${document.uri.fragment}.tmp`) - ).filePath; - } - await fs.writeFile(fileName, document.getText()); - } catch (ex) { - traceError('Failed to create a temporary file', ex); - const exception = ex as Error; - throw new WrappedError(`Failed to create a temporary file, ${exception.message}`, exception); - } - return fileName; -} - -/** - * Parse a textual representation of patches and return a list of Patch objects. - * @param {string} textline Text representation of patches. - * @return {!Array.} Array of Patch objects. - * @throws {!Error} If invalid input. - */ -function patch_fromText(textline: string): Patch[] { - const patches: Patch[] = []; - if (!textline) { - return patches; - } - // Start Modification by Don Jayamanne 24/06/2016 Support for CRLF - const text = textline.split(/[\r\n]/); - // End Modification - let textPointer = 0; - const patchHeader = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/; - while (textPointer < text.length) { - const m = text[textPointer].match(patchHeader); - if (!m) { - throw new Error(`Invalid patch string: ${text[textPointer]}`); - } - - const patch = new (diff_match_patch).patch_obj(); - patches.push(patch); - patch.start1 = parseInt(m[1], 10); - if (m[2] === '') { - patch.start1 -= 1; - patch.length1 = 1; - } else if (m[2] === '0') { - patch.length1 = 0; - } else { - patch.start1 -= 1; - patch.length1 = parseInt(m[2], 10); - } - - patch.start2 = parseInt(m[3], 10); - if (m[4] === '') { - patch.start2 -= 1; - patch.length2 = 1; - } else if (m[4] === '0') { - patch.length2 = 0; - } else { - patch.start2 -= 1; - patch.length2 = parseInt(m[4], 10); - } - textPointer += 1; - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - - while (textPointer < text.length) { - const sign = text[textPointer].charAt(0); - let line: string; - try { - //var line = decodeURI(text[textPointer].substring(1)); - // For some reason the patch generated by python files don't encode any characters - // And this patch module (code from Google) is expecting the text to be encoded!! - // Temporary solution, disable decoding - // Issue #188 - line = text[textPointer].substring(1); - } catch (ex) { - // Malformed URI sequence. - throw new Error('Illegal escape in patch_fromText'); - } - if (sign === '-') { - // Deletion. - patch.diffs.push([dmp.DIFF_DELETE, line]); - } else if (sign === '+') { - // Insertion. - patch.diffs.push([dmp.DIFF_INSERT, line]); - } else if (sign === ' ') { - // Minor equality. - patch.diffs.push([dmp.DIFF_EQUAL, line]); - } else if (sign === '@') { - // Start of next patch. - break; - } else if (sign === '') { - // Blank line? Whatever. - } else { - throw new Error(`Invalid patch mode '${sign}' in: ${line}`); - } - textPointer += 1; - } - } - return patches; -} - -@injectable() -export class EditorUtils implements IEditorUtils { - public getWorkspaceEditsFromPatch(originalContents: string, patch: string, uri: Uri): WorkspaceEdit { - const workspaceEdit = new WorkspaceEdit(); - if (patch.startsWith('---')) { - // Strip the first two lines - patch = patch.substring(patch.indexOf('@@')); - } - if (patch.length === 0) { - return workspaceEdit; - } - // Remove the text added by unified_diff - // # Work around missing newline (http://bugs.python.org/issue2142). - patch = patch.replace(/\\ No newline at end of file[\r\n]/, ''); - - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - const d = new dmp.diff_match_patch(); - const patches = patch_fromText.call(d, patch); - if (!Array.isArray(patches) || patches.length === 0) { - throw new Error('Unable to parse Patch string'); - } - - // Add line feeds and build the text edits - patches.forEach((p) => { - p.diffs.forEach((diff) => { - diff[1] += EOL; - }); - getTextEditsInternal(originalContents, p.diffs, p.start1).forEach((edit) => { - switch (edit.action) { - case EditAction.Delete: - workspaceEdit.delete(uri, new Range(edit.start, edit.end)); - break; - case EditAction.Insert: - workspaceEdit.insert(uri, edit.start, edit.text); - break; - case EditAction.Replace: - workspaceEdit.replace(uri, new Range(edit.start, edit.end), edit.text); - break; - default: - break; - } - }); - }); - - return workspaceEdit; - } -} diff --git a/src/client/common/experiments/groups.ts b/src/client/common/experiments/groups.ts index 29035bbc57fe..8f8ecc631caf 100644 --- a/src/client/common/experiments/groups.ts +++ b/src/client/common/experiments/groups.ts @@ -11,9 +11,6 @@ export enum TerminalEnvVarActivation { experiment = 'pythonTerminalEnvVarActivation', } -export enum ShowFormatterExtensionPrompt { - experiment = 'pythonPromptNewFormatterExt', -} // Experiment to enable the new testing rewrite. export enum EnableTestAdapterRewrite { experiment = 'pythonTestAdapter', diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index be0559496ace..8c872c3113ba 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -5,7 +5,6 @@ import { IBrowserService, IConfigurationService, ICurrentProcess, - IEditorUtils, IExperimentService, IExtensions, IInstaller, @@ -51,7 +50,6 @@ import { import { WorkspaceService } from './application/workspace'; import { ConfigurationService } from './configuration/service'; import { PipEnvExecutionPath } from './configuration/executionSettings/pipEnvExecution'; -import { EditorUtils } from './editor'; import { ExperimentService } from './experiments/service'; import { ProductInstaller } from './installer/productInstaller'; import { InterpreterPathService } from './interpreterPathService'; @@ -130,7 +128,6 @@ export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); serviceManager.addSingleton(ILanguageService, LanguageService); serviceManager.addSingleton(IBrowserService, BrowserService); - serviceManager.addSingleton(IEditorUtils, EditorUtils); serviceManager.addSingleton(ITerminalActivator, TerminalActivator); serviceManager.addSingleton( ITerminalActivationHandler, diff --git a/src/client/common/types.ts b/src/client/common/types.ts index a33f437622fa..05a8a985a5ff 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -17,7 +17,6 @@ import { Memento, LogOutputChannel, Uri, - WorkspaceEdit, OutputChannel, } from 'vscode'; import { LanguageServerType } from '../activation/types'; @@ -381,11 +380,6 @@ export interface IBrowserService { launch(url: string): void; } -export const IEditorUtils = Symbol('IEditorUtils'); -export interface IEditorUtils { - getWorkspaceEditsFromPatch(originalContents: string, patch: string, uri: Uri): WorkspaceEdit; -} - /** * Stores hash formats */ diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 15c745cbd64f..9523572ccfe2 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -24,7 +24,6 @@ import { } from '../../client/common/application/types'; import { WorkspaceService } from '../../client/common/application/workspace'; import { ConfigurationService } from '../../client/common/configuration/service'; -import { EditorUtils } from '../../client/common/editor'; import { ExperimentService } from '../../client/common/experiments/service'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; @@ -70,7 +69,6 @@ import { IBrowserService, IConfigurationService, ICurrentProcess, - IEditorUtils, IExperimentService, IExtensions, IInstaller, @@ -186,7 +184,6 @@ suite('Installer', () => { ioc.serviceManager.addSingleton(IDebugService, DebugService); ioc.serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); ioc.serviceManager.addSingleton(IBrowserService, BrowserService); - ioc.serviceManager.addSingleton(IEditorUtils, EditorUtils); ioc.serviceManager.addSingleton(ITerminalActivator, TerminalActivator); ioc.serviceManager.addSingleton( ITerminalActivationHandler, diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index 2f73bc520307..d91c32fc7350 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -29,7 +29,6 @@ import { } from '../../client/common/application/types'; import { WorkspaceService } from '../../client/common/application/workspace'; import { ConfigurationService } from '../../client/common/configuration/service'; -import { EditorUtils } from '../../client/common/editor'; import { ExperimentService } from '../../client/common/experiments/service'; import { CondaInstaller } from '../../client/common/installer/condaInstaller'; import { PipEnvInstaller } from '../../client/common/installer/pipEnvInstaller'; @@ -73,7 +72,6 @@ import { IBrowserService, IConfigurationService, ICurrentProcess, - IEditorUtils, IExperimentService, IExtensions, IInstaller, @@ -207,7 +205,6 @@ suite('Module Installer', () => { JupyterExtensionDependencyManager, ); ioc.serviceManager.addSingleton(IBrowserService, BrowserService); - ioc.serviceManager.addSingleton(IEditorUtils, EditorUtils); ioc.serviceManager.addSingleton(ITerminalActivator, TerminalActivator); ioc.serviceManager.addSingleton( ITerminalActivationHandler, diff --git a/src/test/common/serviceRegistry.unit.test.ts b/src/test/common/serviceRegistry.unit.test.ts index 2964455ada37..8ba7b7faaa90 100644 --- a/src/test/common/serviceRegistry.unit.test.ts +++ b/src/test/common/serviceRegistry.unit.test.ts @@ -28,7 +28,6 @@ import { import { WorkspaceService } from '../../client/common/application/workspace'; import { ConfigurationService } from '../../client/common/configuration/service'; import { PipEnvExecutionPath } from '../../client/common/configuration/executionSettings/pipEnvExecution'; -import { EditorUtils } from '../../client/common/editor'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; import { InterpreterPathService } from '../../client/common/interpreterPathService'; import { BrowserService } from '../../client/common/net/browser'; @@ -63,7 +62,6 @@ import { IBrowserService, IConfigurationService, ICurrentProcess, - IEditorUtils, IExtensions, IInstaller, IInterpreterPathService, @@ -103,7 +101,6 @@ suite('Common - Service Registry', () => { [IApplicationEnvironment, ApplicationEnvironment], [ILanguageService, LanguageService], [IBrowserService, BrowserService], - [IEditorUtils, EditorUtils], [ITerminalActivator, TerminalActivator], [ITerminalActivationHandler, PowershellTerminalActivationFailedHandler], [ITerminalHelper, TerminalHelper], From 0ffce1999c5c611668c0dcc00eab6397a9f1f137 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 11:25:09 -0700 Subject: [PATCH 0261/1136] Bump microvenv from 2023.3.post1 to 2023.5 (#22259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [microvenv](https://github.com/brettcannon/microvenv) from 2023.3.post1 to 2023.5.
Release notes

Sourced from microvenv's releases.

2023.5

What's Changed

⚠️ Breaking Changes

🎉 New Features

Full Changelog: https://github.com/brettcannon/microvenv/compare/v2023.4...v2023.5

2023.4

What's Changed

🪲 Bug Fixes

Full Changelog: https://github.com/brettcannon/microvenv/compare/v2023.3.post1...v2023.4

Commits
  • 7cdcf90 Get mypy passing under Windows (#58)
  • d32ca9d Fix .github/workflows/docs.yml syntax
  • ee3b599 Prevent __main__.py from attempting to execute on Windows. (#57)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=microvenv&package-manager=pip&previous-version=2023.3.post1&new-version=2023.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 31765898ab59..52981a3ced8f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,9 @@ importlib-metadata==6.7.0 \ --hash=sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4 \ --hash=sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5 # via -r requirements.in -microvenv==2023.3.post1 \ - --hash=sha256:67f0a48511cf16d6a2a45137175d0ddc36a657b91459b598cfbe976ef2afd596 \ - --hash=sha256:6e8c80ccfe813b00b77ab9cc2e5af3fd44e2fe540df176509fda97123f8b8290 +microvenv==2023.5 \ + --hash=sha256:128c0c8ab46e3bbd7b4c902c8a5d6333b694f9ebf871f123b473425cb6fbe19f \ + --hash=sha256:270977691d207d70308c4239221d2ffbbfd595fa1819d09680c75e8808b21254 # via -r requirements.in packaging==23.2 \ --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ From c82702e584c01c9891007d792e55d0b48e8ea38a Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 19 Oct 2023 13:05:00 -0700 Subject: [PATCH 0262/1136] Add extra logging to PythonTestServer data received before parsed as json (#22265) gives additional insight into cases where the data returned to the extension occurs but tests are still not populating the UI. --- src/client/testing/testController/common/server.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index e496860526e4..cff23f67c97f 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -44,6 +44,7 @@ export class PythonTestServer implements ITestServer, Disposable { this.server = net.createServer((socket: net.Socket) => { let buffer: Buffer = Buffer.alloc(0); // Buffer to accumulate received data socket.on('data', (data: Buffer) => { + traceVerbose('data received from python server: ', data.toString()); buffer = Buffer.concat([buffer, data]); // get the new data and add it to the buffer while (buffer.length > 0) { try { @@ -92,6 +93,10 @@ export class PythonTestServer implements ITestServer, Disposable { // if a full json was found in the buffer, fire the data received event then keep cycling with the remaining raw data. traceVerbose(`Firing data received event, ${extractedJsonPayload.cleanedJsonData}`); this._fireDataReceived(extractedJsonPayload.uuid, extractedJsonPayload.cleanedJsonData); + } else { + traceVerbose( + `extract json payload incomplete, uuid= ${extractedJsonPayload.uuid} and cleanedJsonData= ${extractedJsonPayload.cleanedJsonData}`, + ); } buffer = Buffer.from(extractedJsonPayload.remainingRawData); if (buffer.length === 0) { From 5d7eb6546b2c1e03c9c321410b79fed32f859624 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 19 Oct 2023 15:17:02 -0700 Subject: [PATCH 0263/1136] Remove linting support (#22266) --- package.json | 436 --------- package.nls.json | 123 --- src/client/common/configSettings.ts | 90 -- .../common/installer/moduleInstaller.ts | 16 - src/client/common/installer/productNames.ts | 8 - src/client/common/installer/productPath.ts | 12 - src/client/common/installer/productService.ts | 8 - .../common/installer/serviceRegistry.ts | 7 +- src/client/common/types.ts | 69 -- src/client/common/utils/localize.ts | 11 - src/client/extensionActivation.ts | 2 - src/client/linters/bandit.ts | 36 - src/client/linters/baseLinter.ts | 229 ----- src/client/linters/constants.ts | 19 - .../linters/errorHandlers/baseErrorHandler.ts | 27 - .../linters/errorHandlers/errorHandler.ts | 18 - src/client/linters/errorHandlers/standard.ts | 55 -- src/client/linters/flake8.ts | 40 - src/client/linters/linterInfo.ts | 94 -- src/client/linters/linterManager.ts | 134 --- src/client/linters/lintingEngine.ts | 209 ---- src/client/linters/mypy.ts | 29 - src/client/linters/prompts/common.ts | 52 - src/client/linters/prompts/flake8Prompt.ts | 73 -- src/client/linters/prompts/pylintPrompt.ts | 86 -- src/client/linters/prompts/types.ts | 6 - src/client/linters/prospector.ts | 69 -- src/client/linters/pycodestyle.ts | 25 - src/client/linters/pydocstyle.ts | 89 -- src/client/linters/pylama.ts | 34 - src/client/linters/pylint.ts | 96 -- src/client/linters/serviceRegistry.ts | 17 - src/client/linters/types.ts | 80 -- src/client/providers/linterProvider.ts | 123 --- src/client/telemetry/constants.ts | 8 - src/client/telemetry/index.ts | 134 +-- src/client/telemetry/types.ts | 4 - src/test/common.ts | 11 - .../configSettings.unit.test.ts | 2 - src/test/common/installer.test.ts | 331 ------- .../installer.invalidPath.unit.test.ts | 128 --- .../common/installer/installer.unit.test.ts | 621 ------------ .../installer/moduleInstaller.unit.test.ts | 119 --- .../common/installer/productPath.unit.test.ts | 181 ---- .../installer/serviceRegistry.unit.test.ts | 12 +- src/test/common/moduleInstaller.test.ts | 15 +- src/test/common/productsToTest.ts | 14 - .../install/channelManager.channels.test.ts | 2 +- .../install/channelManager.messages.test.ts | 2 +- src/test/linters/bandit.unit.test.ts | 87 -- src/test/linters/common.ts | 405 -------- src/test/linters/lint.args.test.ts | 201 ---- src/test/linters/lint.functional.test.ts | 889 ------------------ src/test/linters/lint.multiroot.test.ts | 170 ---- src/test/linters/lint.provider.test.ts | 217 ----- src/test/linters/lint.test.ts | 110 --- src/test/linters/lint.unit.test.ts | 854 ----------------- src/test/linters/lintengine.test.ts | 178 ---- src/test/linters/linterManager.unit.test.ts | 178 ---- src/test/linters/mypy.unit.test.ts | 99 -- .../linters/prompts/flake8Prompt.unit.test.ts | 152 --- .../linters/prompts/pylintPrompt.unit.test.ts | 142 --- src/test/linters/pylint.test.ts | 163 ---- src/test/linters/pylint.unit.test.ts | 289 ------ src/test/linters/serviceRegistry.unit.test.ts | 31 - src/test/mockClasses.ts | 43 - src/test/serviceRegistry.ts | 5 - 67 files changed, 7 insertions(+), 8212 deletions(-) delete mode 100644 src/client/linters/bandit.ts delete mode 100644 src/client/linters/baseLinter.ts delete mode 100644 src/client/linters/constants.ts delete mode 100644 src/client/linters/errorHandlers/baseErrorHandler.ts delete mode 100644 src/client/linters/errorHandlers/errorHandler.ts delete mode 100644 src/client/linters/errorHandlers/standard.ts delete mode 100644 src/client/linters/flake8.ts delete mode 100644 src/client/linters/linterInfo.ts delete mode 100644 src/client/linters/linterManager.ts delete mode 100644 src/client/linters/lintingEngine.ts delete mode 100644 src/client/linters/mypy.ts delete mode 100644 src/client/linters/prompts/common.ts delete mode 100644 src/client/linters/prompts/flake8Prompt.ts delete mode 100644 src/client/linters/prompts/pylintPrompt.ts delete mode 100644 src/client/linters/prompts/types.ts delete mode 100644 src/client/linters/prospector.ts delete mode 100644 src/client/linters/pycodestyle.ts delete mode 100644 src/client/linters/pydocstyle.ts delete mode 100644 src/client/linters/pylama.ts delete mode 100644 src/client/linters/pylint.ts delete mode 100644 src/client/linters/serviceRegistry.ts delete mode 100644 src/client/linters/types.ts delete mode 100644 src/client/providers/linterProvider.ts delete mode 100644 src/test/common/installer.test.ts delete mode 100644 src/test/common/installer/installer.invalidPath.unit.test.ts delete mode 100644 src/test/common/installer/installer.unit.test.ts delete mode 100644 src/test/common/installer/productPath.unit.test.ts delete mode 100644 src/test/common/productsToTest.ts delete mode 100644 src/test/linters/bandit.unit.test.ts delete mode 100644 src/test/linters/common.ts delete mode 100644 src/test/linters/lint.args.test.ts delete mode 100644 src/test/linters/lint.functional.test.ts delete mode 100644 src/test/linters/lint.multiroot.test.ts delete mode 100644 src/test/linters/lint.provider.test.ts delete mode 100644 src/test/linters/lint.test.ts delete mode 100644 src/test/linters/lint.unit.test.ts delete mode 100644 src/test/linters/lintengine.test.ts delete mode 100644 src/test/linters/linterManager.unit.test.ts delete mode 100644 src/test/linters/mypy.unit.test.ts delete mode 100644 src/test/linters/prompts/flake8Prompt.unit.test.ts delete mode 100644 src/test/linters/prompts/pylintPrompt.unit.test.ts delete mode 100644 src/test/linters/pylint.test.ts delete mode 100644 src/test/linters/pylint.unit.test.ts delete mode 100644 src/test/linters/serviceRegistry.unit.test.ts diff --git a/package.json b/package.json index 716335f73767..6342e1327205 100644 --- a/package.json +++ b/package.json @@ -603,88 +603,6 @@ "scope": "window", "type": "string" }, - "python.linting.banditArgs": { - "default": [], - "description": "%python.linting.banditArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.banditArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.banditArgs.deprecationMessage%" - }, - "python.linting.banditEnabled": { - "default": false, - "description": "%python.linting.banditEnabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.banditArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.banditArgs.deprecationMessage%" - }, - "python.linting.banditPath": { - "default": "bandit", - "description": "%python.linting.banditPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.banditPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.banditPath.deprecationMessage%" - }, - "python.linting.cwd": { - "default": null, - "description": "%python.linting.cwd.description%", - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.cwd.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.cwd.deprecationMessage%" - }, - "python.linting.enabled": { - "default": true, - "description": "%python.linting.enabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.enabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.enabled.deprecationMessage%" - }, - "python.linting.flake8Args": { - "default": [], - "description": "%python.linting.flake8Args.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.flake8Args.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.flake8Args.deprecationMessage%" - }, - "python.linting.flake8CategorySeverity.E": { - "default": "Error", - "description": "%python.linting.flake8CategorySeverity.E.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.flake8CategorySeverity.E.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.flake8CategorySeverity.E.deprecationMessage%" - }, - "python.linting.flake8CategorySeverity.F": { - "default": "Error", - "description": "%python.linting.flake8CategorySeverity.F.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.flake8CategorySeverity.F.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.flake8CategorySeverity.F.deprecationMessage%" - }, "python.interpreter.infoVisibility": { "default": "onPythonRelated", "description": "%python.interpreter.infoVisibility.description%", @@ -701,360 +619,6 @@ "scope": "machine", "type": "string" }, - "python.linting.flake8CategorySeverity.W": { - "default": "Warning", - "description": "%python.linting.flake8CategorySeverity.W.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.flake8CategorySeverity.W.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.flake8CategorySeverity.W.deprecationMessage%" - }, - "python.linting.flake8Enabled": { - "default": false, - "description": "%python.linting.flake8Enabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.flake8Enabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.flake8Enabled.deprecationMessage%" - }, - "python.linting.flake8Path": { - "default": "flake8", - "description": "%python.linting.flake8Path.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.flake8Path.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.flake8Path.deprecationMessage%" - }, - "python.linting.ignorePatterns": { - "default": [ - "**/site-packages/**/*.py", - ".vscode/*.py" - ], - "description": "%python.linting.ignorePatterns.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "uniqueItems": true, - "markdownDeprecationMessage": "%python.linting.ignorePatterns.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.ignorePatterns.deprecationMessage%" - }, - "python.linting.lintOnSave": { - "default": true, - "description": "%python.linting.lintOnSave.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.lintOnSave.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.lintOnSave.deprecationMessage%" - }, - "python.linting.maxNumberOfProblems": { - "default": 100, - "description": "%python.linting.maxNumberOfProblems.description%", - "scope": "resource", - "type": "number", - "markdownDeprecationMessage": "%python.linting.maxNumberOfProblems.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.maxNumberOfProblems.deprecationMessage%" - }, - "python.linting.mypyArgs": { - "default": [ - "--follow-imports=silent", - "--ignore-missing-imports", - "--show-column-numbers", - "--no-pretty" - ], - "description": "%python.linting.mypyArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.mypyArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.mypyArgs.deprecationMessage%" - }, - "python.linting.mypyCategorySeverity.error": { - "default": "Error", - "description": "%python.linting.mypyCategorySeverity.error.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.mypyCategorySeverity.error.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.mypyCategorySeverity.error.deprecationMessage%" - }, - "python.linting.mypyCategorySeverity.note": { - "default": "Information", - "description": "%python.linting.mypyCategorySeverity.note.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.mypyCategorySeverity.note.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.mypyCategorySeverity.note.deprecationMessage%" - }, - "python.linting.mypyEnabled": { - "default": false, - "description": "%python.linting.mypyEnabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.mypyEnabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.mypyEnabled.deprecationMessage%" - }, - "python.linting.mypyPath": { - "default": "mypy", - "description": "%python.linting.mypyPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.mypyPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.mypyPath.deprecationMessage%" - }, - "python.linting.prospectorArgs": { - "default": [], - "description": "%python.linting.prospectorArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.prospectorArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.prospectorArgs.deprecationMessage%" - }, - "python.linting.prospectorEnabled": { - "default": false, - "description": "%python.linting.prospectorEnabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.prospectorEnabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.prospectorEnabled.deprecationMessage%" - }, - "python.linting.prospectorPath": { - "default": "prospector", - "description": "%python.linting.prospectorPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.prospectorPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.prospectorPath.deprecationMessage%" - }, - "python.linting.pycodestyleArgs": { - "default": [], - "description": "%python.linting.pycodestyleArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.pycodestyleArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pycodestyleArgs.deprecationMessage%" - }, - "python.linting.pycodestyleCategorySeverity.E": { - "default": "Error", - "description": "%python.linting.pycodestyleCategorySeverity.E.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pycodestyleCategorySeverity.E.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pycodestyleCategorySeverity.E.deprecationMessage%" - }, - "python.linting.pycodestyleCategorySeverity.W": { - "default": "Warning", - "description": "%python.linting.pycodestyleCategorySeverity.W.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pycodestyleCategorySeverity.W.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pycodestyleCategorySeverity.W.deprecationMessage%" - }, - "python.linting.pycodestyleEnabled": { - "default": false, - "description": "%python.linting.pycodestyleEnabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.pycodestyleEnabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pycodestyleEnabled.deprecationMessage%" - }, - "python.linting.pycodestylePath": { - "default": "pycodestyle", - "description": "%python.linting.pycodestylePath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pycodestylePath.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pycodestylePath.deprecationMessage%" - }, - "python.linting.pydocstyleArgs": { - "default": [], - "description": "%python.linting.pydocstyleArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.pydocstyleArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pydocstyleArgs.deprecationMessage%" - }, - "python.linting.pydocstyleEnabled": { - "default": false, - "description": "%python.linting.pydocstyleEnabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.pydocstyleEnabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pydocstyleEnabled.deprecationMessage%" - }, - "python.linting.pydocstylePath": { - "default": "pydocstyle", - "description": "%python.linting.pydocstylePath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pydocstylePath.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pydocstylePath.deprecationMessage%" - }, - "python.linting.pylamaArgs": { - "default": [], - "description": "%python.linting.pylamaArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.pylamaArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylamaArgs.deprecationMessage%" - }, - "python.linting.pylamaEnabled": { - "default": false, - "description": "%python.linting.pylamaEnabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.pylamaEnabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylamaEnabled.deprecationMessage%" - }, - "python.linting.pylamaPath": { - "default": "pylama", - "description": "%python.linting.pylamaPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pylamaPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylamaPath.deprecationMessage%" - }, - "python.linting.pylintArgs": { - "default": [], - "description": "%python.linting.pylintArgs.description%", - "items": { - "type": "string" - }, - "scope": "resource", - "type": "array", - "markdownDeprecationMessage": "%python.linting.pylintArgs.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintArgs.deprecationMessage%" - }, - "python.linting.pylintCategorySeverity.convention": { - "default": "Information", - "description": "%python.linting.pylintCategorySeverity.convention.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.convention.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintCategorySeverity.convention.deprecationMessage%" - }, - "python.linting.pylintCategorySeverity.error": { - "default": "Error", - "description": "%python.linting.pylintCategorySeverity.error.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.error.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintCategorySeverity.error.deprecationMessage%" - }, - "python.linting.pylintCategorySeverity.fatal": { - "default": "Error", - "description": "%python.linting.pylintCategorySeverity.fatal.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.fatal.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintCategorySeverity.fatal.deprecationMessage%" - }, - "python.linting.pylintCategorySeverity.refactor": { - "default": "Hint", - "description": "%python.linting.pylintCategorySeverity.refactor.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.refactor.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintCategorySeverity.refactor.deprecationMessage%" - }, - "python.linting.pylintCategorySeverity.warning": { - "default": "Warning", - "description": "%python.linting.pylintCategorySeverity.warning.description%", - "enum": [ - "Error", - "Hint", - "Information", - "Warning" - ], - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pylintCategorySeverity.warning.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintCategorySeverity.warning.deprecationMessage%" - }, - "python.linting.pylintEnabled": { - "default": false, - "description": "%python.linting.pylintEnabled.description%", - "scope": "resource", - "type": "boolean", - "markdownDeprecationMessage": "%python.linting.pylintEnabled.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintEnabled.deprecationMessage%" - }, - "python.linting.pylintPath": { - "default": "pylint", - "description": "%python.linting.pylintPath.description%", - "scope": "machine-overridable", - "type": "string", - "markdownDeprecationMessage": "%python.linting.pylintPath.markdownDeprecationMessage%", - "deprecationMessage": "%python.linting.pylintPath.deprecationMessage%" - }, "python.logging.level": { "default": "error", "deprecationMessage": "%python.logging.level.deprecation%", diff --git a/package.nls.json b/package.nls.json index c738b3692daf..f328ee613ba9 100644 --- a/package.nls.json +++ b/package.nls.json @@ -49,133 +49,10 @@ "python.languageServer.jediDescription": "Use Jedi behind the Language Server Protocol (LSP) as a language server.", "python.languageServer.pylanceDescription": "Use Pylance as a language server.", "python.languageServer.noneDescription": "Disable language server capabilities.", - "python.linting.banditArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.banditArgs.markdownDeprecationMessage": "Bandit support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.banditArgs.deprecationMessage": "Bandit support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.banditEnabled.description": "Whether to lint Python files using bandit.", - "python.linting.banditEnabled.markdownDeprecationMessage": "Bandit support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.banditEnabled.deprecationMessage": "Bandit support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.banditPath.description": "Path to bandit, you can use a custom version of bandit by modifying this setting to include the full path.", - "python.linting.banditPath.markdownDeprecationMessage": "Bandit support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.banditPath.deprecationMessage": "Bandit support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.cwd.description": "Optional working directory for linters.", - "python.linting.cwd.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.cwd.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.enabled.description": "Whether to lint Python files.", - "python.linting.enabled.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.enabled.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.flake8Args.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.flake8Args.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.flake8Args.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.flake8CategorySeverity.E.description": "Severity of Flake8 message type 'E'.", - "python.linting.flake8CategorySeverity.E.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.flake8CategorySeverity.E.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.flake8CategorySeverity.F.description": "Severity of Flake8 message type 'F'.", - "python.linting.flake8CategorySeverity.F.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.flake8CategorySeverity.F.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.flake8CategorySeverity.W.description": "Severity of Flake8 message type 'W'.", - "python.linting.flake8CategorySeverity.W.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.flake8CategorySeverity.W.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.flake8Enabled.description": "Whether to lint Python files using flake8.", - "python.linting.flake8Enabled.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.flake8Enabled.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.flake8Path.description": "Path to flake8, you can use a custom version of flake8 by modifying this setting to include the full path.", - "python.linting.flake8Path.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Flake8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.flake8Path.deprecationMessage": "This setting will soon be deprecated. Please use the Flake8 extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.ignorePatterns.description": "Patterns used to exclude files or folders from being linted.", - "python.linting.ignorePatterns.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.ignorePatterns.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", "python.interpreter.infoVisibility.description": "Controls when to display information of selected interpreter in the status bar.", "python.interpreter.infoVisibility.never.description": "Never display information.", "python.interpreter.infoVisibility.onPythonRelated.description": "Only display information if Python-related files are opened.", "python.interpreter.infoVisibility.always.description": "Always display information.", - "python.linting.lintOnSave.description": "Whether to lint Python files when saved.", - "python.linting.lintOnSave.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.lintOnSave.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.maxNumberOfProblems.description": "Controls the maximum number of problems produced by the server.", - "python.linting.maxNumberOfProblems.markdownDeprecationMessage": "This setting will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.maxNumberOfProblems.deprecationMessage": "This setting will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.mypyArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.mypyArgs.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.mypyArgs.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.mypyCategorySeverity.error.description": "Severity of Mypy message type 'Error'.", - "python.linting.mypyCategorySeverity.error.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.mypyCategorySeverity.error.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.mypyCategorySeverity.note.description": "Severity of Mypy message type 'Note'.", - "python.linting.mypyCategorySeverity.note.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.mypyCategorySeverity.note.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.mypyEnabled.description": "Whether to lint Python files using mypy.", - "python.linting.mypyEnabled.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.mypyEnabled.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.mypyPath.description": "Path to mypy, you can use a custom version of mypy by modifying this setting to include the full path.", - "python.linting.mypyPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Mypy Type Checker extension](https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.mypyPath.deprecationMessage": "This setting will soon be deprecated. Please use the Mypy Type Checker extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.prospectorArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.prospectorArgs.markdownDeprecationMessage": "Prospector support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.prospectorArgs.deprecationMessage": "Prospector support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.prospectorEnabled.description": "Whether to lint Python files using prospector.", - "python.linting.prospectorEnabled.markdownDeprecationMessage": "Prospector support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.prospectorEnabled.deprecationMessage": "Prospector support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.prospectorPath.description": "Path to Prospector, you can use a custom version of prospector by modifying this setting to include the full path.", - "python.linting.prospectorPath.markdownDeprecationMessage": "Prospector support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.prospectorPath.deprecationMessage": "Prospector support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pycodestyleArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.pycodestyleArgs.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pycodestyleArgs.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pycodestyleCategorySeverity.E.description": "Severity of pycodestyle message type 'E'.", - "python.linting.pycodestyleCategorySeverity.E.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pycodestyleCategorySeverity.E.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pycodestyleCategorySeverity.W.description": "Severity of pycodestyle message type 'W'.", - "python.linting.pycodestyleCategorySeverity.W.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pycodestyleCategorySeverity.W.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pycodestyleEnabled.description": "Whether to lint Python files using pycodestyle.", - "python.linting.pycodestyleEnabled.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pycodestyleEnabled.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pycodestylePath.description": "Path to pycodestyle, you can use a custom version of pycodestyle by modifying this setting to include the full path.", - "python.linting.pycodestylePath.markdownDeprecationMessage": "Pycodestyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pycodestylePath.deprecationMessage": "Pycodestyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pydocstyleArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.pydocstyleArgs.markdownDeprecationMessage": "Pydocstyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pydocstyleArgs.deprecationMessage": "Pydocstyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pydocstyleEnabled.description": "Whether to lint Python files using pydocstyle.", - "python.linting.pydocstyleEnabled.markdownDeprecationMessage": "Pydocstyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pydocstyleEnabled.deprecationMessage": "Pydocstyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pydocstylePath.description": "Path to pydocstyle, you can use a custom version of pydocstyle by modifying this setting to include the full path.", - "python.linting.pydocstylePath.markdownDeprecationMessage": "Pydocstyle support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pydocstylePath.deprecationMessage": "Pydocstyle support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylamaArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.pylamaArgs.markdownDeprecationMessage": "Pylama support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylamaArgs.deprecationMessage": "Pylama support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylamaEnabled.description": "Whether to lint Python files using pylama.", - "python.linting.pylamaEnabled.markdownDeprecationMessage": "Pylama support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylamaEnabled.deprecationMessage": "Pylama support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylamaPath.description": "Path to pylama, you can use a custom version of pylama by modifying this setting to include the full path.", - "python.linting.pylamaPath.markdownDeprecationMessage": "Pylama support will soon be deprecated. Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylamaPath.deprecationMessage": "Pylama support will soon be deprecated. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintArgs.description": "Arguments passed in. Each argument is a separate item in the array.", - "python.linting.pylintArgs.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintArgs.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintCategorySeverity.convention.description": "Severity of Pylint message type 'Convention/C'.", - "python.linting.pylintCategorySeverity.convention.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintCategorySeverity.convention.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintCategorySeverity.error.description": "Severity of Pylint message type 'Error/E'.", - "python.linting.pylintCategorySeverity.error.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintCategorySeverity.error.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintCategorySeverity.fatal.description": "Severity of Pylint message type 'Error/F'.", - "python.linting.pylintCategorySeverity.fatal.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintCategorySeverity.fatal.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintCategorySeverity.refactor.description": "Severity of Pylint message type 'Refactor/R'.", - "python.linting.pylintCategorySeverity.refactor.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintCategorySeverity.refactor.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintCategorySeverity.warning.description": "Severity of Pylint message type 'Warning/W'.", - "python.linting.pylintCategorySeverity.warning.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintCategorySeverity.warning.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintEnabled.description": "Whether to lint Python files using pylint.", - "python.linting.pylintEnabled.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintEnabled.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", - "python.linting.pylintPath.description": "Path to Pylint, you can use a custom version of pylint by modifying this setting to include the full path.", - "python.linting.pylintPath.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Pylint extension](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint).
Learn more [here](https://aka.ms/AAlgvkb).", - "python.linting.pylintPath.deprecationMessage": "This setting will soon be deprecated. Please use the Pylint extension. Learn more here: https://aka.ms/AAlgvkb.", "python.logging.level.description": "The logging level the extension logs at, defaults to 'error'", "python.logging.level.deprecation": "This setting is deprecated. Please use command `Developer: Set Log Level...` to set logging level.", "python.missingPackage.severity.description": "Set severity of missing packages in requirements.txt or pyproject.toml", diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index cadc1515f7e6..f9c56d4992fe 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -6,7 +6,6 @@ import * as fs from 'fs'; import { ConfigurationChangeEvent, ConfigurationTarget, - DiagnosticSeverity, Disposable, Event, EventEmitter, @@ -29,7 +28,6 @@ import { IExperiments, IInterpreterPathService, IInterpreterSettings, - ILintingSettings, IPythonSettings, ITensorBoardSettings, ITerminalSettings, @@ -106,8 +104,6 @@ export class PythonSettings implements IPythonSettings { public devOptions: string[] = []; - public linting!: ILintingSettings; - public autoComplete!: IAutoCompleteSettings; public tensorBoard: ITensorBoardSettings | undefined; @@ -304,94 +300,8 @@ export class PythonSettings implements IPythonSettings { this.devOptions = systemVariables.resolveAny(pythonSettings.get('devOptions'))!; this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; - const lintingSettings = systemVariables.resolveAny(pythonSettings.get('linting'))!; - if (this.linting) { - Object.assign(this.linting, lintingSettings); - } else { - this.linting = lintingSettings; - } - this.globalModuleInstallation = pythonSettings.get('globalModuleInstallation') === true; - // Support for travis. - this.linting = this.linting - ? this.linting - : { - enabled: false, - cwd: undefined, - ignorePatterns: [], - flake8Args: [], - flake8Enabled: false, - flake8Path: 'flake8', - lintOnSave: false, - maxNumberOfProblems: 100, - mypyArgs: [], - mypyEnabled: false, - mypyPath: 'mypy', - banditArgs: [], - banditEnabled: false, - banditPath: 'bandit', - pycodestyleArgs: [], - pycodestyleEnabled: false, - pycodestylePath: 'pycodestyle', - pylamaArgs: [], - pylamaEnabled: false, - pylamaPath: 'pylama', - prospectorArgs: [], - prospectorEnabled: false, - prospectorPath: 'prospector', - pydocstyleArgs: [], - pydocstyleEnabled: false, - pydocstylePath: 'pydocstyle', - pylintArgs: [], - pylintEnabled: false, - pylintPath: 'pylint', - pylintCategorySeverity: { - convention: DiagnosticSeverity.Hint, - error: DiagnosticSeverity.Error, - fatal: DiagnosticSeverity.Error, - refactor: DiagnosticSeverity.Hint, - warning: DiagnosticSeverity.Warning, - }, - pycodestyleCategorySeverity: { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning, - }, - flake8CategorySeverity: { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning, - // Per http://flake8.pycqa.org/en/latest/glossary.html#term-error-code - // 'F' does not mean 'fatal as in PyLint but rather 'pyflakes' such as - // unused imports, variables, etc. - F: DiagnosticSeverity.Warning, - }, - mypyCategorySeverity: { - error: DiagnosticSeverity.Error, - note: DiagnosticSeverity.Hint, - }, - }; - this.linting.pylintPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylintPath), workspaceRoot); - this.linting.flake8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.flake8Path), workspaceRoot); - this.linting.pycodestylePath = getAbsolutePath( - systemVariables.resolveAny(this.linting.pycodestylePath), - workspaceRoot, - ); - this.linting.pylamaPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylamaPath), workspaceRoot); - this.linting.prospectorPath = getAbsolutePath( - systemVariables.resolveAny(this.linting.prospectorPath), - workspaceRoot, - ); - this.linting.pydocstylePath = getAbsolutePath( - systemVariables.resolveAny(this.linting.pydocstylePath), - workspaceRoot, - ); - this.linting.mypyPath = getAbsolutePath(systemVariables.resolveAny(this.linting.mypyPath), workspaceRoot); - this.linting.banditPath = getAbsolutePath(systemVariables.resolveAny(this.linting.banditPath), workspaceRoot); - - if (this.linting.cwd) { - this.linting.cwd = getAbsolutePath(systemVariables.resolveAny(this.linting.cwd), workspaceRoot); - } - const testSettings = systemVariables.resolveAny(pythonSettings.get('testing'))!; if (this.testing) { Object.assign(this.testing, testSettings); diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index 5a4f245900ea..4cc4cd0c6a2f 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -238,26 +238,10 @@ export abstract class ModuleInstaller implements IModuleInstaller { export function translateProductToModule(product: Product): string { switch (product) { - case Product.mypy: - return 'mypy'; - case Product.pylama: - return 'pylama'; - case Product.prospector: - return 'prospector'; - case Product.pylint: - return 'pylint'; case Product.pytest: return 'pytest'; - case Product.pycodestyle: - return 'pycodestyle'; - case Product.pydocstyle: - return 'pydocstyle'; - case Product.flake8: - return 'flake8'; case Product.unittest: return 'unittest'; - case Product.bandit: - return 'bandit'; case Product.tensorboard: return 'tensorboard'; case Product.torchProfilerInstallName: diff --git a/src/client/common/installer/productNames.ts b/src/client/common/installer/productNames.ts index 9b917d2f1d76..00b19ce77ac3 100644 --- a/src/client/common/installer/productNames.ts +++ b/src/client/common/installer/productNames.ts @@ -4,14 +4,6 @@ import { Product } from '../types'; export const ProductNames = new Map(); -ProductNames.set(Product.bandit, 'bandit'); -ProductNames.set(Product.flake8, 'flake8'); -ProductNames.set(Product.mypy, 'mypy'); -ProductNames.set(Product.pycodestyle, 'pycodestyle'); -ProductNames.set(Product.pylama, 'pylama'); -ProductNames.set(Product.prospector, 'prospector'); -ProductNames.set(Product.pydocstyle, 'pydocstyle'); -ProductNames.set(Product.pylint, 'pylint'); ProductNames.set(Product.pytest, 'pytest'); ProductNames.set(Product.tensorboard, 'tensorboard'); ProductNames.set(Product.torchProfilerInstallName, 'torch-tb-profiler'); diff --git a/src/client/common/installer/productPath.ts b/src/client/common/installer/productPath.ts index 3b3f1d7c1794..b06e4b7a48a9 100644 --- a/src/client/common/installer/productPath.ts +++ b/src/client/common/installer/productPath.ts @@ -7,7 +7,6 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; import { IServiceContainer } from '../../ioc/types'; -import { ILinterManager } from '../../linters/types'; import { ITestingService } from '../../testing/types'; import { IConfigurationService, IInstaller, Product } from '../types'; import { IProductPathService } from './types'; @@ -36,17 +35,6 @@ export abstract class BaseProductPathsService implements IProductPathService { } } -@injectable() -export class LinterProductPathService extends BaseProductPathsService { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer); - } - public getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const linterManager = this.serviceContainer.get(ILinterManager); - return linterManager.getLinterInfo(product).pathName(resource); - } -} - @injectable() export class TestFrameworkProductPathService extends BaseProductPathsService { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { diff --git a/src/client/common/installer/productService.ts b/src/client/common/installer/productService.ts index af2192755fe8..bf5597cc5859 100644 --- a/src/client/common/installer/productService.ts +++ b/src/client/common/installer/productService.ts @@ -12,14 +12,6 @@ export class ProductService implements IProductService { private ProductTypes = new Map(); constructor() { - this.ProductTypes.set(Product.bandit, ProductType.Linter); - this.ProductTypes.set(Product.flake8, ProductType.Linter); - this.ProductTypes.set(Product.mypy, ProductType.Linter); - this.ProductTypes.set(Product.pycodestyle, ProductType.Linter); - this.ProductTypes.set(Product.prospector, ProductType.Linter); - this.ProductTypes.set(Product.pydocstyle, ProductType.Linter); - this.ProductTypes.set(Product.pylama, ProductType.Linter); - this.ProductTypes.set(Product.pylint, ProductType.Linter); this.ProductTypes.set(Product.pytest, ProductType.TestFramework); this.ProductTypes.set(Product.unittest, ProductType.TestFramework); this.ProductTypes.set(Product.tensorboard, ProductType.DataScience); diff --git a/src/client/common/installer/serviceRegistry.ts b/src/client/common/installer/serviceRegistry.ts index c4e7c1a089c6..d4d8a05c3a49 100644 --- a/src/client/common/installer/serviceRegistry.ts +++ b/src/client/common/installer/serviceRegistry.ts @@ -9,11 +9,7 @@ import { CondaInstaller } from './condaInstaller'; import { PipEnvInstaller } from './pipEnvInstaller'; import { PipInstaller } from './pipInstaller'; import { PoetryInstaller } from './poetryInstaller'; -import { - DataScienceProductPathService, - LinterProductPathService, - TestFrameworkProductPathService, -} from './productPath'; +import { DataScienceProductPathService, TestFrameworkProductPathService } from './productPath'; import { ProductService } from './productService'; import { IInstallationChannelManager, IModuleInstaller, IProductPathService, IProductService } from './types'; @@ -24,7 +20,6 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IModuleInstaller, PoetryInstaller); serviceManager.addSingleton(IInstallationChannelManager, InstallationChannelManager); serviceManager.addSingleton(IProductService, ProductService); - serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); serviceManager.addSingleton( IProductPathService, TestFrameworkProductPathService, diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 05a8a985a5ff..742948a49652 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -8,7 +8,6 @@ import { CancellationToken, ConfigurationChangeEvent, ConfigurationTarget, - DiagnosticSeverity, Disposable, DocumentSymbolProvider, Event, @@ -85,24 +84,14 @@ export enum ProductInstallStatus { } export enum ProductType { - Linter = 'Linter', TestFramework = 'TestFramework', - RefactoringLibrary = 'RefactoringLibrary', DataScience = 'DataScience', Python = 'Python', } export enum Product { pytest = 1, - pylint = 3, - flake8 = 4, - pycodestyle = 5, - pylama = 6, - prospector = 7, - pydocstyle = 8, - mypy = 11, unittest = 12, - bandit = 17, tensorboard = 24, torchProfilerInstallName = 25, torchProfilerImportName = 26, @@ -179,7 +168,6 @@ export interface IPythonSettings { readonly pipenvPath: string; readonly poetryPath: string; readonly devOptions: string[]; - readonly linting: ILintingSettings; readonly testing: ITestingSettings; readonly autoComplete: IAutoCompleteSettings; readonly terminal: ITerminalSettings; @@ -197,67 +185,10 @@ export interface ITensorBoardSettings { logDirectory: string | undefined; } -export interface IPylintCategorySeverity { - readonly convention: DiagnosticSeverity; - readonly refactor: DiagnosticSeverity; - readonly warning: DiagnosticSeverity; - readonly error: DiagnosticSeverity; - readonly fatal: DiagnosticSeverity; -} -export interface IPycodestyleCategorySeverity { - readonly W: DiagnosticSeverity; - readonly E: DiagnosticSeverity; -} - -export interface Flake8CategorySeverity { - readonly F: DiagnosticSeverity; - readonly E: DiagnosticSeverity; - readonly W: DiagnosticSeverity; -} -export interface IMypyCategorySeverity { - readonly error: DiagnosticSeverity; - readonly note: DiagnosticSeverity; -} export interface IInterpreterSettings { infoVisibility: 'never' | 'onPythonRelated' | 'always'; } -export interface ILintingSettings { - readonly enabled: boolean; - readonly ignorePatterns: string[]; - readonly prospectorEnabled: boolean; - readonly prospectorArgs: string[]; - readonly pylintEnabled: boolean; - readonly pylintArgs: string[]; - readonly pycodestyleEnabled: boolean; - readonly pycodestyleArgs: string[]; - readonly pylamaEnabled: boolean; - readonly pylamaArgs: string[]; - readonly flake8Enabled: boolean; - readonly flake8Args: string[]; - readonly pydocstyleEnabled: boolean; - readonly pydocstyleArgs: string[]; - readonly lintOnSave: boolean; - readonly maxNumberOfProblems: number; - readonly pylintCategorySeverity: IPylintCategorySeverity; - readonly pycodestyleCategorySeverity: IPycodestyleCategorySeverity; - readonly flake8CategorySeverity: Flake8CategorySeverity; - readonly mypyCategorySeverity: IMypyCategorySeverity; - cwd?: string; - prospectorPath: string; - pylintPath: string; - pycodestylePath: string; - pylamaPath: string; - flake8Path: string; - pydocstylePath: string; - mypyEnabled: boolean; - mypyArgs: string[]; - mypyPath: string; - banditEnabled: boolean; - banditArgs: string[]; - banditPath: string; -} - export interface ITerminalSettings { readonly executeInFileDir: boolean; readonly focusAfterLaunch: boolean; diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index bbb55a79ce40..b5d1721d14fa 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -506,14 +506,3 @@ export namespace CreateEnv { export const disableCheckWorkspace = l10n.t('Disable (Workspace)'); } } - -export namespace ToolsExtensions { - export const flake8PromptMessage = l10n.t( - 'Use the Flake8 extension to enable easier configuration and new features such as quick fixes.', - ); - export const pylintPromptMessage = l10n.t( - 'Use the Pylint extension to enable easier configuration and new features such as quick fixes.', - ); - export const installPylintExtension = l10n.t('Install Pylint extension'); - export const installFlake8Extension = l10n.t('Install Flake8 extension'); -} diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 0d3b04d9bb8c..37ca1ad54afc 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -27,7 +27,6 @@ import { registerTypes as debugConfigurationRegisterTypes } from './debugger/ext import { IDebugConfigurationService, IDynamicDebugConfigurationService } from './debugger/extension/types'; import { IInterpreterService } from './interpreter/contracts'; import { getLanguageConfiguration } from './language/languageConfiguration'; -import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; import { ReplProvider } from './providers/replProvider'; import { registerTypes as providersRegisterTypes } from './providers/serviceRegistry'; import { TerminalProvider } from './providers/terminalProvider'; @@ -122,7 +121,6 @@ async function activateLegacy(ext: ExtensionState): Promise { serviceManager.addSingletonInstance(UseProposedApi, enableProposedApi); // Feature specific registrations. unitTestsRegisterTypes(serviceManager); - lintersRegisterTypes(serviceManager); installerRegisterTypes(serviceManager); commonRegisterTerminalTypes(serviceManager); debugConfigurationRegisterTypes(serviceManager); diff --git a/src/client/linters/bandit.ts b/src/client/linters/bandit.ts deleted file mode 100644 index bbc8836bfc6b..000000000000 --- a/src/client/linters/bandit.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage, LintMessageSeverity } from './types'; - -const severityMapping: Record = { - LOW: LintMessageSeverity.Information, - MEDIUM: LintMessageSeverity.Warning, - HIGH: LintMessageSeverity.Error, -}; - -export const BANDIT_REGEX = - '(?\\d+),(?(col)?(\\d+)?),(?\\w+),(?\\w+\\d+):(?.*)\\r?(\\n|$)'; - -export class Bandit extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.bandit, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - // View all errors in bandit <= 1.5.1 (https://github.com/PyCQA/bandit/issues/371) - const messages = await this.run([document.uri.fsPath], document, cancellation, BANDIT_REGEX); - - messages.forEach((msg) => { - msg.severity = severityMapping[msg.type]; - }); - return messages; - } -} diff --git a/src/client/linters/baseLinter.ts b/src/client/linters/baseLinter.ts deleted file mode 100644 index bb24bee1637f..000000000000 --- a/src/client/linters/baseLinter.ts +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IWorkspaceService } from '../common/application/types'; -import { isTestExecution } from '../common/constants'; -import '../common/extensions'; -import { IPythonToolExecutionService } from '../common/process/types'; -import { splitLines } from '../common/stringUtils'; -import { - ExecutionInfo, - Flake8CategorySeverity, - IConfigurationService, - IMypyCategorySeverity, - IPycodestyleCategorySeverity, - IPylintCategorySeverity, - IPythonSettings, - Product, -} from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError, traceLog } from '../logging'; -import { ErrorHandler } from './errorHandlers/errorHandler'; -import { ILinter, ILinterInfo, ILinterManager, ILintMessage, LinterId, LintMessageSeverity } from './types'; - -const namedRegexp = require('named-js-regexp'); -// Allow negative column numbers (https://github.com/PyCQA/pylint/issues/1822) -// Allow codes with more than one letter (i.e. ABC123) -const REGEX = '(?\\d+),(?-?\\d+),(?\\w+),(?\\w+\\d+):(?.*)\\r?(\\n|$)'; - -interface IRegexGroup { - line: number; - column: number; - code: string; - message: string; - type: string; -} - -function matchNamedRegEx(data: string, regex: string): IRegexGroup | undefined { - const compiledRegexp = namedRegexp(regex, 'g'); - const rawMatch = compiledRegexp.exec(data); - if (rawMatch !== null) { - return rawMatch.groups(); - } - - return undefined; -} - -export function parseLine(line: string, regex: string, linterID: LinterId, colOffset = 0): ILintMessage | undefined { - const match = matchNamedRegEx(line, regex)!; - if (!match) { - return undefined; - } - - match.line = Number(match.line); - - match.column = Number(match.column); - - return { - code: match.code, - message: match.message, - column: Number.isNaN(match.column) || match.column <= 0 ? 0 : match.column - colOffset, - line: match.line, - type: match.type, - provider: linterID, - }; -} - -export abstract class BaseLinter implements ILinter { - protected readonly configService: IConfigurationService; - - private errorHandler: ErrorHandler; - - private _pythonSettings!: IPythonSettings; - - private _info: ILinterInfo; - - private workspace: IWorkspaceService; - - protected get pythonSettings(): IPythonSettings { - return this._pythonSettings; - } - - constructor( - product: Product, - protected readonly serviceContainer: IServiceContainer, - protected readonly columnOffset = 0, - ) { - this._info = serviceContainer.get(ILinterManager).getLinterInfo(product); - this.errorHandler = new ErrorHandler(this.info.product, serviceContainer); - this.configService = serviceContainer.get(IConfigurationService); - this.workspace = serviceContainer.get(IWorkspaceService); - } - - public get info(): ILinterInfo { - return this._info; - } - - public async lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise { - this._pythonSettings = this.configService.getSettings(document.uri); - return this.runLinter(document, cancellation); - } - - protected getWorkspaceRootPath(document: vscode.TextDocument): string { - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - const workspaceRootPath = - workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string' ? workspaceFolder.uri.fsPath : undefined; - return typeof workspaceRootPath === 'string' ? workspaceRootPath : path.dirname(document.uri.fsPath); - } - - protected getWorkingDirectoryPath(document: vscode.TextDocument): string { - return this._pythonSettings.linting.cwd || this.getWorkspaceRootPath(document); - } - - protected abstract runLinter( - document: vscode.TextDocument, - cancellation: vscode.CancellationToken, - ): Promise; - - // eslint-disable-next-line class-methods-use-this - protected parseMessagesSeverity( - error: string, - categorySeverity: - | Flake8CategorySeverity - | IMypyCategorySeverity - | IPycodestyleCategorySeverity - | IPylintCategorySeverity, - ): LintMessageSeverity { - const severity = error as keyof typeof categorySeverity; - - if (categorySeverity[severity]) { - const severityName = categorySeverity[severity]; - switch (severityName) { - case 'Error': - return LintMessageSeverity.Error; - case 'Hint': - return LintMessageSeverity.Hint; - case 'Information': - return LintMessageSeverity.Information; - case 'Warning': - return LintMessageSeverity.Warning; - default: { - if (LintMessageSeverity[severityName]) { - return (LintMessageSeverity[severityName] as unknown) as LintMessageSeverity; - } - } - } - } - return LintMessageSeverity.Information; - } - - protected async run( - args: string[], - document: vscode.TextDocument, - cancellation: vscode.CancellationToken, - regEx: string = REGEX, - ): Promise { - if (!this.info.isEnabled(document.uri)) { - return []; - } - const executionInfo = this.info.getExecutionInfo(args, document.uri); - const cwd = this.getWorkingDirectoryPath(document); - const pythonToolsExecutionService = this.serviceContainer.get( - IPythonToolExecutionService, - ); - try { - const result = await pythonToolsExecutionService.execForLinter( - executionInfo, - { cwd, token: cancellation, mergeStdOutErr: false }, - document.uri, - ); - this.displayLinterResultHeader(result.stdout); - return await this.parseMessages(result.stdout, document, cancellation, regEx); - } catch (error) { - await this.handleError(error as Error, document.uri, executionInfo); - return []; - } - } - - protected async parseMessages( - output: string, - _document: vscode.TextDocument, - _token: vscode.CancellationToken, - regEx: string, - ): Promise { - const outputLines = splitLines(output, { removeEmptyEntries: false, trim: false }); - return this.parseLines(outputLines, regEx); - } - - protected async handleError(error: Error, resource: vscode.Uri, execInfo: ExecutionInfo): Promise { - if (isTestExecution()) { - this.errorHandler.handleError(error, resource, execInfo).ignoreErrors(); - } else { - this.errorHandler - .handleError(error, resource, execInfo) - .catch((ex) => traceError('Error in errorHandler.handleError', ex)) - .ignoreErrors(); - } - } - - private parseLine(line: string, regEx: string): ILintMessage | undefined { - return parseLine(line, regEx, this.info.id, this.columnOffset); - } - - private parseLines(outputLines: string[], regEx: string): ILintMessage[] { - const messages: ILintMessage[] = []; - for (const line of outputLines) { - try { - const msg = this.parseLine(line, regEx); - if (msg) { - messages.push(msg); - if (messages.length >= this.pythonSettings.linting.maxNumberOfProblems) { - break; - } - } - } catch (ex) { - traceError(`Linter '${this.info.id}' failed to parse the line '${line}.`, ex); - } - } - return messages; - } - - private displayLinterResultHeader(data: string) { - traceLog(`${'#'.repeat(10)}Linting Output - ${this.info.id}${'#'.repeat(10)}\n`); - traceLog(data); - } -} diff --git a/src/client/linters/constants.ts b/src/client/linters/constants.ts deleted file mode 100644 index 27b7c80db7f4..000000000000 --- a/src/client/linters/constants.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Product } from '../common/types'; -import { LinterId } from './types'; - -// All supported linters must be in this map. -export const LINTERID_BY_PRODUCT = new Map([ - [Product.bandit, LinterId.Bandit], - [Product.flake8, LinterId.Flake8], - [Product.pylint, LinterId.PyLint], - [Product.mypy, LinterId.MyPy], - [Product.pycodestyle, LinterId.PyCodeStyle], - [Product.prospector, LinterId.Prospector], - [Product.pydocstyle, LinterId.PyDocStyle], - [Product.pylama, LinterId.PyLama], -]); diff --git a/src/client/linters/errorHandlers/baseErrorHandler.ts b/src/client/linters/errorHandlers/baseErrorHandler.ts deleted file mode 100644 index 16c5e93ae012..000000000000 --- a/src/client/linters/errorHandlers/baseErrorHandler.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { ExecutionInfo, IInstaller, Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { IErrorHandler } from '../types'; - -export abstract class BaseErrorHandler implements IErrorHandler { - protected installer: IInstaller; - - private handler?: IErrorHandler; - - constructor(protected product: Product, protected serviceContainer: IServiceContainer) { - this.installer = this.serviceContainer.get(IInstaller); - } - - protected get nextHandler(): IErrorHandler | undefined { - return this.handler; - } - - public setNextHandler(handler: IErrorHandler): void { - this.handler = handler; - } - - public abstract handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise; -} diff --git a/src/client/linters/errorHandlers/errorHandler.ts b/src/client/linters/errorHandlers/errorHandler.ts deleted file mode 100644 index af28dd61c3a4..000000000000 --- a/src/client/linters/errorHandlers/errorHandler.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Uri } from 'vscode'; -import { ExecutionInfo, Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { IErrorHandler } from '../types'; -import { BaseErrorHandler } from './baseErrorHandler'; -import { StandardErrorHandler } from './standard'; - -export class ErrorHandler implements IErrorHandler { - private handler: BaseErrorHandler; - - constructor(product: Product, serviceContainer: IServiceContainer) { - this.handler = new StandardErrorHandler(product, serviceContainer); - } - - public handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - return this.handler.handleError(error, resource, execInfo); - } -} diff --git a/src/client/linters/errorHandlers/standard.ts b/src/client/linters/errorHandlers/standard.ts deleted file mode 100644 index 6367da7abe4a..000000000000 --- a/src/client/linters/errorHandlers/standard.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { l10n, Uri } from 'vscode'; -import { IApplicationShell } from '../../common/application/types'; -import { ExecutionInfo, ILogOutputChannel } from '../../common/types'; -import { traceError, traceLog } from '../../logging'; -import { ILinterManager, LinterId } from '../types'; -import { BaseErrorHandler } from './baseErrorHandler'; - -export class StandardErrorHandler extends BaseErrorHandler { - public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - if ( - typeof error === 'string' && - (error as string).includes("OSError: [Errno 2] No such file or directory: '/") - ) { - return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : Promise.resolve(false); - } - - const linterManager = this.serviceContainer.get(ILinterManager); - const info = linterManager.getLinterInfo(execInfo.product!); - - traceError(`There was an error in running the linter ${info.id}`, error); - if (info.id === LinterId.PyLint) { - traceError('Support for "pylint" is moved to ms-python.pylint extension.'); - traceError( - 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.pylint', - ); - } else if (info.id === LinterId.Flake8) { - traceError('Support for "flake8" is moved to ms-python.flake8 extension.'); - traceError( - 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.flake8', - ); - } else if (info.id === LinterId.MyPy) { - traceError('Support for "mypy" is moved to ms-python.mypy-type-checker extension.'); - traceError( - 'Please install the extension from: https://marketplace.visualstudio.com/items?itemName=ms-python.mypy-type-checker', - ); - } - traceError(`If the error is due to missing ${info.id}, please install ${info.id} using pip manually.`); - traceError('Learn more here: https://aka.ms/AAlgvkb'); - traceLog(`Linting with ${info.id} failed.`); - traceLog(error.toString()); - - this.displayLinterError(info.id).ignoreErrors(); - return true; - } - - private async displayLinterError(linterId: LinterId) { - const message = l10n.t("There was an error in running the linter '{0}'", linterId); - const appShell = this.serviceContainer.get(IApplicationShell); - const outputChannel = this.serviceContainer.get(ILogOutputChannel); - const action = await appShell.showErrorMessage(message, 'View Errors'); - if (action === 'View Errors') { - outputChannel.show(); - } - } -} diff --git a/src/client/linters/flake8.ts b/src/client/linters/flake8.ts deleted file mode 100644 index e79d09158741..000000000000 --- a/src/client/linters/flake8.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceLog } from '../logging'; -import { BaseLinter } from './baseLinter'; -import { isExtensionEnabled } from './prompts/common'; -import { FLAKE8_EXTENSION } from './prompts/flake8Prompt'; -import { IToolsExtensionPrompt } from './prompts/types'; -import { ILintMessage } from './types'; - -const COLUMN_OFF_SET = 1; - -export class Flake8 extends BaseLinter { - constructor(serviceContainer: IServiceContainer, private readonly prompt: IToolsExtensionPrompt) { - super(Product.flake8, serviceContainer, COLUMN_OFF_SET); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - await this.prompt.showPrompt(); - - if (isExtensionEnabled(this.serviceContainer, FLAKE8_EXTENSION)) { - traceLog( - 'LINTING: Skipping linting from Python extension, since Flake8 extension is installed and enabled.', - ); - return []; - } - - const messages = await this.run([document.uri.fsPath], document, cancellation); - messages.forEach((msg) => { - msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.flake8CategorySeverity); - // flake8 uses 0th line for some file-wide problems - // but diagnostics expects positive line numbers. - if (msg.line === 0) { - msg.line = 1; - } - }); - return messages; - } -} diff --git a/src/client/linters/linterInfo.ts b/src/client/linters/linterInfo.ts deleted file mode 100644 index 321f23b0f304..000000000000 --- a/src/client/linters/linterInfo.ts +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as path from 'path'; -import { Uri } from 'vscode'; -import { linterScript } from '../common/process/internal/scripts'; -import { ExecutionInfo, IConfigurationService, ILintingSettings, Product } from '../common/types'; -import { ILinterInfo, LinterId } from './types'; - -export class LinterInfo implements ILinterInfo { - private _id: LinterId; - - private _product: Product; - - private _configFileNames: string[]; - - constructor( - product: Product, - id: LinterId, - protected configService: IConfigurationService, - configFileNames: string[] = [], - ) { - this._product = product; - this._id = id; - this._configFileNames = configFileNames; - } - - public get id(): LinterId { - return this._id; - } - - public get product(): Product { - return this._product; - } - - public get pathSettingName(): string { - return `${this.id}Path`; - } - - public get argsSettingName(): string { - return `${this.id}Args`; - } - - public get enabledSettingName(): string { - return `${this.id}Enabled`; - } - - public get configFileNames(): string[] { - return this._configFileNames; - } - - public async enableAsync(enabled: boolean, resource?: Uri): Promise { - return this.configService.updateSetting(`linting.${this.enabledSettingName}`, enabled, resource); - } - - public isEnabled(resource?: Uri): boolean { - const settings = this.configService.getSettings(resource); - const name = this.enabledSettingName as keyof ILintingSettings; - return settings.linting[name] as boolean; - } - - public pathName(resource?: Uri): string { - const settings = this.configService.getSettings(resource); - const name = this.pathSettingName as keyof ILintingSettings; - return settings.linting[name] as string; - } - - public linterArgs(resource?: Uri): string[] { - const settings = this.configService.getSettings(resource); - const name = this.argsSettingName as keyof ILintingSettings; - const args = settings.linting[name]; - return Array.isArray(args) ? (args as string[]) : []; - } - - public getExecutionInfo(customArgs: string[], resource?: Uri): ExecutionInfo { - const execPath = this.pathName(resource); - const args = this.linterArgs(resource).concat(customArgs); - const script = linterScript(); - if (path.basename(execPath) === execPath) { - return { - execPath: undefined, - args: [script, '-m', this.id, ...args], - product: this.product, - moduleName: execPath, - }; - } - return { - execPath, - moduleName: this.id, - args: [script, '-p', this.id, execPath, ...args], - product: this.product, - }; - } -} diff --git a/src/client/linters/linterManager.ts b/src/client/linters/linterManager.ts deleted file mode 100644 index 72c92aa1c77d..000000000000 --- a/src/client/linters/linterManager.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* eslint-disable max-classes-per-file */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { CancellationToken, TextDocument, Uri } from 'vscode'; -import { IConfigurationService, Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError } from '../logging'; -import { Bandit } from './bandit'; -import { Flake8 } from './flake8'; -import { LinterInfo } from './linterInfo'; -import { MyPy } from './mypy'; -import { getOrCreateFlake8Prompt } from './prompts/flake8Prompt'; -import { getOrCreatePylintPrompt } from './prompts/pylintPrompt'; -import { Prospector } from './prospector'; -import { Pycodestyle } from './pycodestyle'; -import { PyDocStyle } from './pydocstyle'; -import { PyLama } from './pylama'; -import { Pylint } from './pylint'; -import { ILinter, ILinterInfo, ILinterManager, ILintMessage, LinterId } from './types'; - -class DisabledLinter implements ILinter { - constructor(private configService: IConfigurationService) {} - - public get info() { - return new LinterInfo(Product.pylint, LinterId.PyLint, this.configService); - } - - // eslint-disable-next-line class-methods-use-this - public async lint(_document: TextDocument, _cancellation: CancellationToken): Promise { - return []; - } -} - -@injectable() -export class LinterManager implements ILinterManager { - protected linters: ILinterInfo[]; - - constructor(@inject(IConfigurationService) private configService: IConfigurationService) { - // Note that we use unit tests to ensure all the linters are here. - this.linters = [ - new LinterInfo(Product.bandit, LinterId.Bandit, this.configService), - new LinterInfo(Product.flake8, LinterId.Flake8, this.configService), - new LinterInfo(Product.pylint, LinterId.PyLint, this.configService, ['pylintrc', '.pylintrc']), - new LinterInfo(Product.mypy, LinterId.MyPy, this.configService), - new LinterInfo(Product.pycodestyle, LinterId.PyCodeStyle, this.configService), - new LinterInfo(Product.prospector, LinterId.Prospector, this.configService), - new LinterInfo(Product.pydocstyle, LinterId.PyDocStyle, this.configService), - new LinterInfo(Product.pylama, LinterId.PyLama, this.configService), - ]; - } - - public getAllLinterInfos(): ILinterInfo[] { - return this.linters; - } - - public getLinterInfo(product: Product): ILinterInfo { - const x = this.linters.findIndex((value, _index, _obj) => value.product === product); - if (x >= 0) { - return this.linters[x]; - } - throw new Error(`Invalid linter '${Product[product]}'`); - } - - public async isLintingEnabled(resource?: Uri): Promise { - const settings = this.configService.getSettings(resource); - const activeLintersPresent = await this.getActiveLinters(resource); - return settings.linting.enabled && activeLintersPresent.length > 0; - } - - public async enableLintingAsync(enable: boolean, resource?: Uri): Promise { - await this.configService.updateSetting('linting.enabled', enable, resource); - } - - public async getActiveLinters(resource?: Uri): Promise { - return this.linters.filter((x) => x.isEnabled(resource)); - } - - public async setActiveLintersAsync(products: Product[], resource?: Uri): Promise { - // ensure we only allow valid linters to be set, otherwise leave things alone. - // filter out any invalid products: - const validProducts = products.filter((product) => { - const foundIndex = this.linters.findIndex((validLinter) => validLinter.product === product); - return foundIndex !== -1; - }); - - // if we have valid linter product(s), enable only those - if (validProducts.length > 0) { - const active = await this.getActiveLinters(resource); - for (const x of active) { - await x.enableAsync(false, resource); - } - if (products.length > 0) { - const toActivate = this.linters.filter((x) => products.findIndex((p) => x.product === p) >= 0); - for (const x of toActivate) { - await x.enableAsync(true, resource); - } - await this.enableLintingAsync(true, resource); - } - } - } - - public async createLinter(product: Product, serviceContainer: IServiceContainer, resource?: Uri): Promise { - if (!(await this.isLintingEnabled(resource))) { - return new DisabledLinter(this.configService); - } - const error = 'Linter manager: Unknown linter'; - switch (product) { - case Product.bandit: - return new Bandit(serviceContainer); - case Product.flake8: - return new Flake8(serviceContainer, getOrCreateFlake8Prompt(serviceContainer)); - case Product.pylint: - return new Pylint(serviceContainer, getOrCreatePylintPrompt(serviceContainer)); - case Product.mypy: - return new MyPy(serviceContainer); - case Product.prospector: - return new Prospector(serviceContainer); - case Product.pylama: - return new PyLama(serviceContainer); - case Product.pydocstyle: - return new PyDocStyle(serviceContainer); - case Product.pycodestyle: - return new Pycodestyle(serviceContainer); - default: - traceError(error); - break; - } - throw new Error(error); - } -} diff --git a/src/client/linters/lintingEngine.ts b/src/client/linters/lintingEngine.ts deleted file mode 100644 index 2a4bf4e10848..000000000000 --- a/src/client/linters/lintingEngine.ts +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { Minimatch } from 'minimatch'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { Commands } from '../common/constants'; -import { IFileSystem } from '../common/platform/types'; -import { IConfigurationService } from '../common/types'; -import { isNotebookCell, noop } from '../common/utils/misc'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { LinterTrigger, LintingTelemetry } from '../telemetry/types'; -import { ILinterInfo, ILinterManager, ILintingEngine, ILintMessage, LintMessageSeverity } from './types'; - -const PYTHON: vscode.DocumentFilter = { language: 'python' }; - -const lintSeverityToVSSeverity = new Map(); -lintSeverityToVSSeverity.set(LintMessageSeverity.Error, vscode.DiagnosticSeverity.Error); -lintSeverityToVSSeverity.set(LintMessageSeverity.Hint, vscode.DiagnosticSeverity.Hint); -lintSeverityToVSSeverity.set(LintMessageSeverity.Information, vscode.DiagnosticSeverity.Information); -lintSeverityToVSSeverity.set(LintMessageSeverity.Warning, vscode.DiagnosticSeverity.Warning); - -@injectable() -export class LintingEngine implements ILintingEngine { - private workspace: IWorkspaceService; - - private documents: IDocumentManager; - - private configurationService: IConfigurationService; - - private linterManager: ILinterManager; - - private diagnosticCollection: vscode.DiagnosticCollection; - - private pendingLintings = new Map(); - - private fileSystem: IFileSystem; - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.documents = serviceContainer.get(IDocumentManager); - this.workspace = serviceContainer.get(IWorkspaceService); - this.configurationService = serviceContainer.get(IConfigurationService); - this.linterManager = serviceContainer.get(ILinterManager); - this.fileSystem = serviceContainer.get(IFileSystem); - this.diagnosticCollection = vscode.languages.createDiagnosticCollection('python'); - } - - public get diagnostics(): vscode.DiagnosticCollection { - return this.diagnosticCollection; - } - - public clearDiagnostics(document: vscode.TextDocument): void { - if (this.diagnosticCollection.has(document.uri)) { - this.diagnosticCollection.delete(document.uri); - } - } - - public async lintOpenPythonFiles(trigger: LinterTrigger = 'auto'): Promise { - this.diagnosticCollection.clear(); - const promises = this.documents.textDocuments.map(async (document) => this.lintDocument(document, trigger)); - await Promise.all(promises); - return this.diagnosticCollection; - } - - public async lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise { - if (isNotebookCell(document)) { - return; - } - this.diagnosticCollection.set(document.uri, []); - - // Check if we need to lint this document - if (!(await this.shouldLintDocument(document, trigger))) { - return; - } - - if (this.pendingLintings.has(document.uri.fsPath)) { - this.pendingLintings.get(document.uri.fsPath)!.cancel(); - this.pendingLintings.delete(document.uri.fsPath); - } - - const cancelToken = new vscode.CancellationTokenSource(); - cancelToken.token.onCancellationRequested(() => { - if (this.pendingLintings.has(document.uri.fsPath)) { - this.pendingLintings.delete(document.uri.fsPath); - } - }); - - this.pendingLintings.set(document.uri.fsPath, cancelToken); - - const activeLinters = await this.linterManager.getActiveLinters(document.uri); - const promises: Promise[] = activeLinters.map(async (info: ILinterInfo) => { - const stopWatch = new StopWatch(); - const linter = await this.linterManager.createLinter(info.product, this.serviceContainer, document.uri); - const promise = linter.lint(document, cancelToken.token); - this.sendLinterRunTelemetry(info, document.uri, promise, stopWatch, trigger); - return promise; - }); - - // linters will resolve asynchronously - keep a track of all - // diagnostics reported as them come in. - let diagnostics: vscode.Diagnostic[] = []; - const settings = this.configurationService.getSettings(document.uri); - - for (const p of promises) { - const msgs = await p; - if (cancelToken.token.isCancellationRequested) { - break; - } - - if (this.isDocumentOpen(document.uri)) { - // Build the message and suffix the message with the name of the linter used. - for (const m of msgs) { - diagnostics.push(this.createDiagnostics(m, document)); - } - // Limit the number of messages to the max value. - diagnostics = diagnostics.filter((_value, index) => index <= settings.linting.maxNumberOfProblems); - } - } - // Set all diagnostics found in this pass, as this method always clears existing diagnostics. - this.diagnosticCollection.set(document.uri, diagnostics); - } - - // eslint-disable-next-line class-methods-use-this - private sendLinterRunTelemetry( - info: ILinterInfo, - resource: vscode.Uri, - promise: Promise, - stopWatch: StopWatch, - trigger: LinterTrigger, - ): void { - const linterExecutablePathName = info.pathName(resource); - const properties: LintingTelemetry = { - tool: info.id, - hasCustomArgs: info.linterArgs(resource).length > 0, - trigger, - executableSpecified: linterExecutablePathName !== info.id, - }; - sendTelemetryWhenDone(EventName.LINTING, promise, stopWatch, properties); - } - - private isDocumentOpen(uri: vscode.Uri): boolean { - return this.documents.textDocuments.some((document) => document.uri.fsPath === uri.fsPath); - } - - // eslint-disable-next-line class-methods-use-this - private createDiagnostics(message: ILintMessage, _document: vscode.TextDocument): vscode.Diagnostic { - const position = new vscode.Position(message.line - 1, message.column); - let endPosition: vscode.Position = position; - if (message.endLine && message.endColumn) { - endPosition = new vscode.Position(message.endLine - 1, message.endColumn); - } - const range = new vscode.Range(position, endPosition); - - const severity = lintSeverityToVSSeverity.get(message.severity!)!; - const diagnostic = new vscode.Diagnostic(range, message.message, severity); - diagnostic.code = message.code; - diagnostic.source = message.provider; - return diagnostic; - } - - private async shouldLintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise { - const interpreterService = this.serviceContainer.get(IInterpreterService); - const interpreter = await interpreterService.getActiveInterpreter(document.uri); - if (!interpreter && trigger === 'manual') { - this.serviceContainer - .get(ICommandManager) - .executeCommand(Commands.TriggerEnvironmentSelection, document.uri) - .then(noop, noop); - return false; - } - if (!(await this.linterManager.isLintingEnabled(document.uri))) { - this.diagnosticCollection.set(document.uri, []); - return false; - } - - if (document.languageId !== PYTHON.language) { - return false; - } - - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - const workspaceRootPath = - workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string' ? workspaceFolder.uri.fsPath : undefined; - const relativeFileName = - typeof workspaceRootPath === 'string' - ? path.relative(workspaceRootPath, document.fileName) - : document.fileName; - - const settings = this.configurationService.getSettings(document.uri); - // { dot: true } is important so dirs like `.venv` will be matched by globs - const ignoreMinmatches = settings.linting.ignorePatterns.map( - (pattern) => new Minimatch(pattern, { dot: true }), - ); - if (ignoreMinmatches.some((matcher) => matcher.match(document.fileName) || matcher.match(relativeFileName))) { - return false; - } - if (document.uri.scheme !== 'file' || !document.uri.fsPath) { - return false; - } - return this.fileSystem.fileExists(document.uri.fsPath); - } -} diff --git a/src/client/linters/mypy.ts b/src/client/linters/mypy.ts deleted file mode 100644 index f39eef99b422..000000000000 --- a/src/client/linters/mypy.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { escapeRegExp } from 'lodash'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage } from './types'; - -export function getRegex(filepath: string): string { - return `${escapeRegExp(filepath)}:(?\\d+)(:(?\\d+))?: (?\\w+): (?.*)\\r?(\\n|$)`; -} -const COLUMN_OFF_SET = 1; - -export class MyPy extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.mypy, serviceContainer, COLUMN_OFF_SET); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const relativeFilePath = document.uri.fsPath.slice(this.getWorkspaceRootPath(document).length + 1); - const regex = getRegex(relativeFilePath); - const messages = await this.run([document.uri.fsPath], document, cancellation, regex); - messages.forEach((msg) => { - msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.mypyCategorySeverity); - msg.code = msg.type; - }); - return messages; - } -} diff --git a/src/client/linters/prompts/common.ts b/src/client/linters/prompts/common.ts deleted file mode 100644 index ab88282db607..000000000000 --- a/src/client/linters/prompts/common.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { ShowToolsExtensionPrompt } from '../../common/experiments/groups'; -import { IExperimentService, IExtensions, IPersistentState, IPersistentStateFactory } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { traceLog } from '../../logging'; - -export function isExtensionDisabled(serviceContainer: IServiceContainer, extensionId: string): boolean { - const extensions: IExtensions = serviceContainer.get(IExtensions); - // When debugging the python extension this `extensionPath` below will point to your repo. - // If you are debugging this feature then set the `extensionPath` to right location after - // the next line. - const pythonExt = extensions.getExtension('ms-python.python'); - if (pythonExt) { - let found = false; - traceLog(`Extension search path: ${path.dirname(pythonExt.extensionPath)}`); - fs.readdirSync(path.dirname(pythonExt.extensionPath), { withFileTypes: false }).forEach((s) => { - if (s.toString().startsWith(extensionId)) { - found = true; - } - }); - return found; - } - return false; -} - -/** - * Detects if extension is installed and enabled. - */ -export function isExtensionEnabled(serviceContainer: IServiceContainer, extensionId: string): boolean { - const extensions: IExtensions = serviceContainer.get(IExtensions); - const extension = extensions.getExtension(extensionId); - return extension !== undefined; -} - -export function doNotShowPromptState( - serviceContainer: IServiceContainer, - promptKey: string, -): IPersistentState { - const persistFactory: IPersistentStateFactory = serviceContainer.get( - IPersistentStateFactory, - ); - return persistFactory.createWorkspacePersistentState(promptKey, false); -} - -export function inToolsExtensionsExperiment(serviceContainer: IServiceContainer): Promise { - const experiments: IExperimentService = serviceContainer.get(IExperimentService); - return experiments.inExperiment(ShowToolsExtensionPrompt.experiment); -} diff --git a/src/client/linters/prompts/flake8Prompt.ts b/src/client/linters/prompts/flake8Prompt.ts deleted file mode 100644 index fa1969df682a..000000000000 --- a/src/client/linters/prompts/flake8Prompt.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IApplicationEnvironment } from '../../common/application/types'; -import { Common, ToolsExtensions } from '../../common/utils/localize'; -import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../ioc/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { doNotShowPromptState, inToolsExtensionsExperiment, isExtensionDisabled, isExtensionEnabled } from './common'; -import { IToolsExtensionPrompt } from './types'; - -export const FLAKE8_EXTENSION = 'ms-python.flake8'; -const FLAKE8_PROMPT_DONOTSHOW_KEY = 'showFlake8ExtensionPrompt'; - -export class Flake8ExtensionPrompt implements IToolsExtensionPrompt { - private shownThisSession = false; - - public constructor(private readonly serviceContainer: IServiceContainer) {} - - public async showPrompt(): Promise { - const isEnabled = isExtensionEnabled(this.serviceContainer, FLAKE8_EXTENSION); - if (isEnabled || isExtensionDisabled(this.serviceContainer, FLAKE8_EXTENSION)) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED, undefined, { - extensionId: FLAKE8_EXTENSION, - isEnabled, - }); - return true; - } - - const doNotShow = doNotShowPromptState(this.serviceContainer, FLAKE8_PROMPT_DONOTSHOW_KEY); - if (this.shownThisSession || doNotShow.value) { - return false; - } - - if (!(await inToolsExtensionsExperiment(this.serviceContainer))) { - return false; - } - - this.shownThisSession = true; - const response = await showInformationMessage( - ToolsExtensions.flake8PromptMessage, - ToolsExtensions.installFlake8Extension, - Common.doNotShowAgain, - ); - - if (response === Common.doNotShowAgain) { - doNotShow.updateValue(true); - return false; - } - - if (response === ToolsExtensions.installFlake8Extension) { - const appEnv: IApplicationEnvironment = this.serviceContainer.get( - IApplicationEnvironment, - ); - await executeCommand('workbench.extensions.installExtension', FLAKE8_EXTENSION, { - installPreReleaseVersion: appEnv.extensionChannel === 'insiders', - }); - return true; - } - - return false; - } -} - -let _prompt: IToolsExtensionPrompt | undefined; -export function getOrCreateFlake8Prompt(serviceContainer: IServiceContainer): IToolsExtensionPrompt { - if (!_prompt) { - _prompt = new Flake8ExtensionPrompt(serviceContainer); - } - return _prompt; -} diff --git a/src/client/linters/prompts/pylintPrompt.ts b/src/client/linters/prompts/pylintPrompt.ts deleted file mode 100644 index 37e583243078..000000000000 --- a/src/client/linters/prompts/pylintPrompt.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IApplicationEnvironment } from '../../common/application/types'; -import { Common, ToolsExtensions } from '../../common/utils/localize'; -import { executeCommand } from '../../common/vscodeApis/commandApis'; -import { showInformationMessage } from '../../common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../ioc/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { doNotShowPromptState, inToolsExtensionsExperiment, isExtensionDisabled, isExtensionEnabled } from './common'; -import { IToolsExtensionPrompt } from './types'; - -export const PYLINT_EXTENSION = 'ms-python.pylint'; -const PYLINT_PROMPT_DONOTSHOW_KEY = 'showPylintExtensionPrompt'; - -export class PylintExtensionPrompt implements IToolsExtensionPrompt { - private shownThisSession = false; - - public constructor(private readonly serviceContainer: IServiceContainer) {} - - public async showPrompt(): Promise { - const isEnabled = isExtensionEnabled(this.serviceContainer, PYLINT_EXTENSION); - if (isEnabled || isExtensionDisabled(this.serviceContainer, PYLINT_EXTENSION)) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED, undefined, { - extensionId: PYLINT_EXTENSION, - isEnabled, - }); - return true; - } - - const doNotShow = doNotShowPromptState(this.serviceContainer, PYLINT_PROMPT_DONOTSHOW_KEY); - if (this.shownThisSession || doNotShow.value) { - return false; - } - - if (!(await inToolsExtensionsExperiment(this.serviceContainer))) { - return false; - } - - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_SHOWN, undefined, { extensionId: PYLINT_EXTENSION }); - this.shownThisSession = true; - const response = await showInformationMessage( - ToolsExtensions.pylintPromptMessage, - ToolsExtensions.installPylintExtension, - Common.doNotShowAgain, - ); - - if (response === Common.doNotShowAgain) { - await doNotShow.updateValue(true); - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED, undefined, { - extensionId: PYLINT_EXTENSION, - dismissType: 'doNotShow', - }); - return false; - } - - if (response === ToolsExtensions.installPylintExtension) { - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_INSTALL_SELECTED, undefined, { - extensionId: PYLINT_EXTENSION, - }); - const appEnv: IApplicationEnvironment = this.serviceContainer.get( - IApplicationEnvironment, - ); - await executeCommand('workbench.extensions.installExtension', PYLINT_EXTENSION, { - installPreReleaseVersion: appEnv.extensionChannel === 'insiders', - }); - return true; - } - - sendTelemetryEvent(EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED, undefined, { - extensionId: PYLINT_EXTENSION, - dismissType: 'close', - }); - - return false; - } -} - -let _prompt: IToolsExtensionPrompt | undefined; -export function getOrCreatePylintPrompt(serviceContainer: IServiceContainer): IToolsExtensionPrompt { - if (!_prompt) { - _prompt = new PylintExtensionPrompt(serviceContainer); - } - return _prompt; -} diff --git a/src/client/linters/prompts/types.ts b/src/client/linters/prompts/types.ts deleted file mode 100644 index d7c884b3a00d..000000000000 --- a/src/client/linters/prompts/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -export interface IToolsExtensionPrompt { - showPrompt(): Promise; -} diff --git a/src/client/linters/prospector.ts b/src/client/linters/prospector.ts deleted file mode 100644 index fa4b3907255b..000000000000 --- a/src/client/linters/prospector.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as path from 'path'; -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError, traceLog } from '../logging'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage } from './types'; - -interface IProspectorResponse { - messages: IProspectorMessage[]; -} -interface IProspectorMessage { - source: string; - message: string; - code: string; - location: IProspectorLocation; -} -interface IProspectorLocation { - function: string; - path: string; - line: number; - character: number; - module: 'beforeFormat'; -} - -export class Prospector extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.prospector, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const cwd = this.getWorkingDirectoryPath(document); - const relativePath = path.relative(cwd, document.uri.fsPath); - return this.run([relativePath], document, cancellation); - } - - protected async parseMessages( - output: string, - _document: TextDocument, - _token: CancellationToken, - _regEx: string, - ): Promise { - let parsedData: IProspectorResponse; - try { - parsedData = JSON.parse(output); - } catch (ex) { - traceLog(`${'#'.repeat(10)}Linting Output - ${this.info.id}${'#'.repeat(10)}`); - traceLog(output); - traceError('Failed to parse Prospector output', ex); - return []; - } - return parsedData.messages - .filter((_value, index) => index <= this.pythonSettings.linting.maxNumberOfProblems) - .map((msg) => { - const lineNumber = - msg.location.line === null || Number.isNaN(msg.location.line) ? 1 : msg.location.line; - - return { - code: msg.code, - message: msg.message, - column: msg.location.character, - line: lineNumber, - type: msg.code, - provider: `${this.info.id} - ${msg.source}`, - }; - }); - } -} diff --git a/src/client/linters/pycodestyle.ts b/src/client/linters/pycodestyle.ts deleted file mode 100644 index 30517980e83c..000000000000 --- a/src/client/linters/pycodestyle.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage } from './types'; - -const COLUMN_OFF_SET = 1; - -export class Pycodestyle extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.pycodestyle, serviceContainer, COLUMN_OFF_SET); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run([document.uri.fsPath], document, cancellation); - messages.forEach((msg) => { - msg.severity = this.parseMessagesSeverity( - msg.type, - this.pythonSettings.linting.pycodestyleCategorySeverity, - ); - }); - return messages; - } -} diff --git a/src/client/linters/pydocstyle.ts b/src/client/linters/pydocstyle.ts deleted file mode 100644 index 4851190a92ac..000000000000 --- a/src/client/linters/pydocstyle.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as path from 'path'; -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError } from '../logging'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage, LintMessageSeverity } from './types'; -import { isWindows } from '../common/platform/platformService'; - -export class PyDocStyle extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.pydocstyle, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run([document.uri.fsPath], document, cancellation); - // All messages in pep8 are treated as warnings for now. - messages.forEach((msg) => { - msg.severity = LintMessageSeverity.Warning; - }); - - return messages; - } - - protected async parseMessages( - output: string, - document: TextDocument, - _token: CancellationToken, - _regEx: string, - ): Promise { - let outputLines = output.split(/\r?\n/g); - const baseFileName = path.basename(document.uri.fsPath); - - // Remember, the first line of the response contains the file name and line number, the next line contains the error message. - // So we have two lines per message, hence we need to take lines in pairs. - const maxLines = this.pythonSettings.linting.maxNumberOfProblems * 2; - // First line is almost always empty. - const oldOutputLines = outputLines.filter((line) => line.length > 0); - outputLines = []; - for (let counter = 0; counter < oldOutputLines.length / 2; counter += 1) { - outputLines.push(oldOutputLines[2 * counter] + oldOutputLines[2 * counter + 1]); - } - - return ( - outputLines - .filter((value, index) => index < maxLines && value.indexOf(':') >= 0) - .map((line) => { - // Windows will have a : after the drive letter (e.g. c:\). - if (isWindows()) { - return line.substring(line.indexOf(`${baseFileName}:`) + baseFileName.length + 1).trim(); - } - return line.substring(line.indexOf(':') + 1).trim(); - }) - // Iterate through the lines (skipping the messages). - // So, just iterate the response in pairs. - .map((line) => { - try { - if (line.trim().length === 0) { - return undefined; - } - const lineNumber = parseInt(line.substring(0, line.indexOf(' ')), 10); - const part = line.substring(line.indexOf(':') + 1).trim(); - const code = part.substring(0, part.indexOf(':')).trim(); - const message = part.substring(part.indexOf(':') + 1).trim(); - - const sourceLine = document.lineAt(lineNumber - 1).text; - const trimmedSourceLine = sourceLine.trim(); - const sourceStart = sourceLine.indexOf(trimmedSourceLine); - - return { - code, - message, - column: sourceStart, - line: lineNumber, - type: '', - provider: this.info.id, - } as ILintMessage; - } catch (ex) { - traceError(`Failed to parse pydocstyle line '${line}'`, ex); - } - - return undefined; - }) - .filter((item) => item !== undefined) - .map((item) => item!) - ); - } -} diff --git a/src/client/linters/pylama.ts b/src/client/linters/pylama.ts deleted file mode 100644 index d5930c839445..000000000000 --- a/src/client/linters/pylama.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage, LintMessageSeverity } from './types'; - -/** - * Example messages to parse from PyLama - * 1. Linter: pycodestyle - recent version removed an extra colon (:) after line:col, hence made it optional in the regex (to be backward compatibile) - * `src/test_py.py:23:60 [E] E226 missing whitespace around arithmetic operator [pycodestyle]` - * 2. Linter: mypy - output is missing the error code, something like `E226` - hence made it optional in the regex - * `src/test_py.py:7:4 [E] Argument 1 to "fn" has incompatible type "str"; expected "int" [mypy]` - */ - -const REGEX = - '(?.py):(?\\d+):(?\\d+):? \\[(?\\w+)\\]( (?\\w\\d+)?:?)? (?.*)\\r?(\\n|$)'; -const COLUMN_OFF_SET = 1; - -export class PyLama extends BaseLinter { - constructor(serviceContainer: IServiceContainer) { - super(Product.pylama, serviceContainer, COLUMN_OFF_SET); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run([document.uri.fsPath], document, cancellation, REGEX); - // All messages in pylama are treated as warnings for now. - messages.forEach((msg) => { - msg.severity = LintMessageSeverity.Warning; - }); - - return messages; - } -} diff --git a/src/client/linters/pylint.ts b/src/client/linters/pylint.ts deleted file mode 100644 index 0b635417f906..000000000000 --- a/src/client/linters/pylint.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { CancellationToken, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { traceError, traceLog } from '../logging'; -import { BaseLinter } from './baseLinter'; -import { isExtensionEnabled } from './prompts/common'; -import { PYLINT_EXTENSION } from './prompts/pylintPrompt'; -import { IToolsExtensionPrompt } from './prompts/types'; -import { ILintMessage } from './types'; - -interface IJsonMessage { - column: number | null; - line: number; - message: string; - symbol: string; - type: string; - endLine?: number | null; - endColumn?: number | null; -} - -export class Pylint extends BaseLinter { - constructor(serviceContainer: IServiceContainer, private readonly prompt: IToolsExtensionPrompt) { - super(Product.pylint, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - await this.prompt.showPrompt(); - - if (isExtensionEnabled(this.serviceContainer, PYLINT_EXTENSION)) { - traceLog( - 'LINTING: Skipping linting from Python extension, since Pylint extension is installed and enabled.', - ); - return []; - } - - const { uri } = document; - const settings = this.configService.getSettings(uri); - const args = [uri.fsPath]; - const messages = await this.run(args, document, cancellation); - messages.forEach((msg) => { - msg.severity = this.parseMessagesSeverity(msg.type, settings.linting.pylintCategorySeverity); - }); - return messages; - } - - private parseOutputMessage(outputMsg: IJsonMessage, colOffset = 0): ILintMessage | undefined { - // Both 'endLine' and 'endColumn' are only present on pylint 2.12.2+ - // If present, both can still be 'null' if AST node didn't have endLine and / or endColumn information. - // If 'endColumn' is 'null' or not preset, set it to 'undefined' to - // prevent the lintingEngine from inferring an error range. - if (outputMsg.endColumn) { - outputMsg.endColumn = outputMsg.endColumn <= 0 ? 0 : outputMsg.endColumn - colOffset; - } else { - outputMsg.endColumn = undefined; - } - - return { - code: outputMsg.symbol, - message: outputMsg.message, - column: outputMsg.column === null || outputMsg.column <= 0 ? 0 : outputMsg.column - colOffset, - line: outputMsg.line, - type: outputMsg.type, - provider: this.info.id, - endLine: outputMsg.endLine === null ? undefined : outputMsg.endLine, - endColumn: outputMsg.endColumn, - }; - } - - protected async parseMessages( - output: string, - _document: TextDocument, - _token: CancellationToken, - _: string, - ): Promise { - const messages: ILintMessage[] = []; - try { - const parsedOutput: IJsonMessage[] = JSON.parse(output); - for (const outputMsg of parsedOutput) { - const msg = this.parseOutputMessage(outputMsg, this.columnOffset); - if (msg) { - messages.push(msg); - if (messages.length >= this.pythonSettings.linting.maxNumberOfProblems) { - break; - } - } - } - } catch (ex) { - traceError(`Linter '${this.info.id}' failed to parse the output '${output}.`, ex); - } - return messages; - } -} diff --git a/src/client/linters/serviceRegistry.ts b/src/client/linters/serviceRegistry.ts deleted file mode 100644 index 26ada4d0cc8f..000000000000 --- a/src/client/linters/serviceRegistry.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { IExtensionActivationService } from '../activation/types'; -import { IServiceManager } from '../ioc/types'; -import { LinterProvider } from '../providers/linterProvider'; -import { LinterManager } from './linterManager'; -import { LintingEngine } from './lintingEngine'; -import { ILinterManager, ILintingEngine } from './types'; - -export function registerTypes(serviceManager: IServiceManager): void { - serviceManager.addSingleton(ILintingEngine, LintingEngine); - serviceManager.addSingleton(ILinterManager, LinterManager); - serviceManager.addSingleton(IExtensionActivationService, LinterProvider); -} diff --git a/src/client/linters/types.ts b/src/client/linters/types.ts deleted file mode 100644 index b24fe508ea1c..000000000000 --- a/src/client/linters/types.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as vscode from 'vscode'; -import { ExecutionInfo, Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { LinterTrigger } from '../telemetry/types'; - -export interface IErrorHandler { - handleError(error: Error, resource: vscode.Uri, execInfo: ExecutionInfo): Promise; -} - -export enum LinterId { - Flake8 = 'flake8', - MyPy = 'mypy', - PyCodeStyle = 'pycodestyle', - Prospector = 'prospector', - PyDocStyle = 'pydocstyle', - PyLama = 'pylama', - PyLint = 'pylint', - Bandit = 'bandit', -} - -export interface ILinterInfo { - readonly id: LinterId; - readonly product: Product; - readonly pathSettingName: string; - readonly argsSettingName: string; - readonly enabledSettingName: string; - readonly configFileNames: string[]; - enableAsync(enabled: boolean, resource?: vscode.Uri): Promise; - isEnabled(resource?: vscode.Uri): boolean; - pathName(resource?: vscode.Uri): string; - linterArgs(resource?: vscode.Uri): string[]; - getExecutionInfo(customArgs: string[], resource?: vscode.Uri): ExecutionInfo; -} - -export interface ILinter { - readonly info: ILinterInfo; - lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise; -} - -export const ILinterManager = Symbol('ILinterManager'); -export interface ILinterManager { - getAllLinterInfos(): ILinterInfo[]; - getLinterInfo(product: Product): ILinterInfo; - getActiveLinters(resource?: vscode.Uri): Promise; - isLintingEnabled(resource?: vscode.Uri): Promise; - enableLintingAsync(enable: boolean, resource?: vscode.Uri): Promise; - setActiveLintersAsync(products: Product[], resource?: vscode.Uri): Promise; - createLinter(product: Product, serviceContainer: IServiceContainer, resource?: vscode.Uri): Promise; -} - -export interface ILintMessage { - line: number; - column: number; - endLine?: number; - endColumn?: number; - code: string | undefined; - message: string; - type: string; - severity?: LintMessageSeverity; - provider: string; -} -export enum LintMessageSeverity { - Hint, - Error, - Warning, - Information, -} - -export const ILintingEngine = Symbol('ILintingEngine'); -export interface ILintingEngine { - readonly diagnostics: vscode.DiagnosticCollection; - lintOpenPythonFiles(trigger?: LinterTrigger): Promise; - lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise; - clearDiagnostics(document: vscode.TextDocument): void; -} diff --git a/src/client/providers/linterProvider.ts b/src/client/providers/linterProvider.ts deleted file mode 100644 index 7821eaeccd53..000000000000 --- a/src/client/providers/linterProvider.ts +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { ConfigurationChangeEvent, Disposable, TextDocument, Uri, workspace } from 'vscode'; -import { IExtensionActivationService } from '../activation/types'; -import { IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { isTestExecution } from '../common/constants'; -import '../common/extensions'; -import { IFileSystem } from '../common/platform/types'; -import { IConfigurationService, IDisposable } from '../common/types'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { ILinterManager, ILintingEngine } from '../linters/types'; - -@injectable() -export class LinterProvider implements IExtensionActivationService, Disposable { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private interpreterService: IInterpreterService; - - private documents: IDocumentManager; - - private configuration: IConfigurationService; - - private linterManager: ILinterManager; - - private engine: ILintingEngine; - - private fs: IFileSystem; - - private readonly disposables: IDisposable[] = []; - - private workspaceService: IWorkspaceService; - - private activatedOnce = false; - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.serviceContainer = serviceContainer; - this.fs = this.serviceContainer.get(IFileSystem); - this.engine = this.serviceContainer.get(ILintingEngine); - this.linterManager = this.serviceContainer.get(ILinterManager); - this.interpreterService = this.serviceContainer.get(IInterpreterService); - this.documents = this.serviceContainer.get(IDocumentManager); - this.configuration = this.serviceContainer.get(IConfigurationService); - this.workspaceService = this.serviceContainer.get(IWorkspaceService); - } - - public async activate(): Promise { - if (this.activatedOnce) { - return; - } - this.activatedOnce = true; - this.disposables.push(this.interpreterService.onDidChangeInterpreter(() => this.engine.lintOpenPythonFiles())); - - this.documents.onDidOpenTextDocument((e) => this.onDocumentOpened(e), this.disposables); - this.documents.onDidCloseTextDocument((e) => this.onDocumentClosed(e), this.disposables); - this.documents.onDidSaveTextDocument((e) => this.onDocumentSaved(e), this.disposables); - - const disposable = this.workspaceService.onDidChangeConfiguration(this.lintSettingsChangedHandler.bind(this)); - this.disposables.push(disposable); - - // On workspace reopen we don't get `onDocumentOpened` since it is first opened - // and then the extension is activated. So schedule linting pass now. - if (!isTestExecution()) { - const timer = setTimeout(() => this.engine.lintOpenPythonFiles().ignoreErrors(), 1200); - this.disposables.push({ dispose: () => clearTimeout(timer) }); - } - } - - public dispose(): void { - this.disposables.forEach((d) => d.dispose()); - } - - private isDocumentOpen(uri: Uri): boolean { - return this.documents.textDocuments.some((document) => this.fs.arePathsSame(document.uri.fsPath, uri.fsPath)); - } - - private lintSettingsChangedHandler(e: ConfigurationChangeEvent) { - // Look for python files that belong to the specified workspace folder. - workspace.textDocuments.forEach((document) => { - if (e.affectsConfiguration('python.linting', document.uri)) { - this.engine.lintDocument(document, 'auto').ignoreErrors(); - } - }); - } - - private onDocumentOpened(document: TextDocument): void { - this.engine.lintDocument(document, 'auto').ignoreErrors(); - } - - private onDocumentSaved(document: TextDocument): void { - const settings = this.configuration.getSettings(document.uri); - if (document.languageId === 'python' && settings.linting.enabled && settings.linting.lintOnSave) { - this.engine.lintDocument(document, 'save').ignoreErrors(); - return; - } - - this.linterManager - .getActiveLinters(document.uri) - .then((linters) => { - const fileName = path.basename(document.uri.fsPath).toLowerCase(); - const watchers = linters.filter((info) => info.configFileNames.indexOf(fileName) >= 0); - if (watchers.length > 0) { - setTimeout(() => this.engine.lintOpenPythonFiles(), 1000); - } - }) - .ignoreErrors(); - } - - private onDocumentClosed(document: TextDocument) { - if (!document || !document.fileName || !document.uri) { - return; - } - // Check if this document is still open as a duplicate editor. - if (!this.isDocumentOpen(document.uri)) { - this.engine.clearDiagnostics(document); - } - } -} diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 301502a0f6fa..de0980ada257 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -6,7 +6,6 @@ export enum EventName { FORMAT_ON_TYPE = 'FORMAT.FORMAT_ON_TYPE', EDITOR_LOAD = 'EDITOR.LOAD', - LINTING = 'LINTING', REPL = 'REPL', CREATE_NEW_FILE_COMMAND = 'CREATE_NEW_FILE_COMMAND', SELECT_INTERPRETER = 'SELECT_INTERPRETER', @@ -78,10 +77,8 @@ export enum EventName { DIAGNOSTICS_ACTION = 'DIAGNOSTICS.ACTION', DIAGNOSTICS_MESSAGE = 'DIAGNOSTICS.MESSAGE', - SELECT_LINTER = 'LINTING.SELECT', USE_REPORT_ISSUE_COMMAND = 'USE_REPORT_ISSUE_COMMAND', - LINTER_NOT_INSTALLED_PROMPT = 'LINTER_NOT_INSTALLED_PROMPT', HASHED_PACKAGE_NAME = 'HASHED_PACKAGE_NAME', JEDI_LANGUAGE_SERVER_ENABLED = 'JEDI_LANGUAGE_SERVER.ENABLED', @@ -115,11 +112,6 @@ export enum EventName { ENVIRONMENT_CHECK_TRIGGER = 'ENVIRONMENT.CHECK.TRIGGER', ENVIRONMENT_CHECK_RESULT = 'ENVIRONMENT.CHECK.RESULT', - - TOOLS_EXTENSIONS_ALREADY_INSTALLED = 'TOOLS_EXTENSIONS.ALREADY_INSTALLED', - TOOLS_EXTENSIONS_PROMPT_SHOWN = 'TOOLS_EXTENSIONS.PROMPT_SHOWN', - TOOLS_EXTENSIONS_INSTALL_SELECTED = 'TOOLS_EXTENSIONS.INSTALL_SELECTED', - TOOLS_EXTENSIONS_PROMPT_DISMISSED = 'TOOLS_EXTENSIONS.PROMPT_DISMISSED', } export enum PlatformErrors { diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index ba65c4d1913f..cc600d2d59a4 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -13,7 +13,6 @@ import { StopWatch } from '../common/utils/stopWatch'; import { isPromise } from '../common/utils/async'; import { DebugConfigurationType } from '../debugger/extension/types'; import { ConsoleType, TriggerType } from '../debugger/types'; -import { LinterId } from '../linters/types'; import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info'; import { TensorBoardPromptSelection, @@ -22,7 +21,7 @@ import { TensorBoardEntrypoint, } from '../tensorBoard/constants'; import { EventName } from './constants'; -import type { LinterTrigger, TestTool } from './types'; +import type { TestTool } from './types'; /** * Checks whether telemetry is supported. @@ -894,33 +893,6 @@ export interface IEventNamePropertyMapping { hashedName: string; }; - /** - * Telemetry event sent with details of selection in prompt - * `Prompt message` :- 'Linter ${productName} is not installed' - */ - /* __GDPR__ - "linter_not_installed_prompt" : { - "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "action": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } - } - */ - [EventName.LINTER_NOT_INSTALLED_PROMPT]: { - /** - * Name of the linter - * - * @type {LinterId} - */ - tool?: LinterId; - /** - * `select` When 'Select linter' option is selected - * `disablePrompt` When "Don't show again" option is selected - * `install` When 'Install' option is selected - * - * @type {('select' | 'disablePrompt' | 'install')} - */ - action: 'select' | 'disablePrompt' | 'install'; - }; - /** * Telemetry event sent when installing modules */ @@ -961,44 +933,6 @@ export interface IEventNamePropertyMapping { */ version?: string; }; - /** - * Telemetry sent with details immediately after linting a document completes - */ - /* __GDPR__ - "linting" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "karthiknadig" }, - "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "hascustomargs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "trigger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "executablespecified" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } - } - */ - [EventName.LINTING]: { - /** - * Name of the linter being used - * - * @type {LinterId} - */ - tool: LinterId; - /** - * If custom arguments for linter is provided in settings.json - * - * @type {boolean} - */ - hasCustomArgs: boolean; - /** - * Carries the source which triggered configuration of tests - * - * @type {LinterTrigger} - */ - trigger: LinterTrigger; - /** - * Carries `true` if linter executable is specified, `false` otherwise - * - * @type {boolean} - */ - executableSpecified: boolean; - }; /** * Telemetry event sent when an environment without contain a python binary is selected. */ @@ -1545,25 +1479,6 @@ export interface IEventNamePropertyMapping { } */ [EventName.REPL]: never | undefined; - /** - * Telemetry event sent with details of linter selected in quickpick of linter list. - */ - /* __GDPR__ - "linting.select" : { - "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "enabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } - } - */ - [EventName.SELECT_LINTER]: { - /** - * The name of the linter - */ - tool?: LinterId; - /** - * Carries `true` if linter is enabled, `false` otherwise - */ - enabled: boolean; - }; /** * Telemetry event sent if and when user configure tests command. This command can be trigerred from multiple places in the extension. (Command palette, prompt etc.) */ @@ -2135,53 +2050,6 @@ export interface IEventNamePropertyMapping { [EventName.ENVIRONMENT_CHECK_RESULT]: { result: 'criteria-met' | 'criteria-not-met' | 'already-ran' | 'turned-off' | 'no-uri'; }; - /** - * Telemetry event sent when a linter or formatter extension is already installed. - */ - /* __GDPR__ - "tools_extensions.already_installed" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } - } - */ - [EventName.TOOLS_EXTENSIONS_ALREADY_INSTALLED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8'; - isEnabled: boolean; - }; - /** - * Telemetry event sent when install linter or formatter extension prompt is shown. - */ - /* __GDPR__ - "tools_extensions.prompt_shown" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } - } - */ - [EventName.TOOLS_EXTENSIONS_PROMPT_SHOWN]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8'; - }; - /** - * Telemetry event sent when clicking to install linter or formatter extension from the suggestion prompt. - */ - /* __GDPR__ - "tools_extensions.install_selected" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } - } - */ - [EventName.TOOLS_EXTENSIONS_INSTALL_SELECTED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8'; - }; - /** - * Telemetry event sent when dismissing prompt suggesting to install the linter or formatter extension. - */ - /* __GDPR__ - "tools_extensions.prompt_dismissed" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, - "dismissType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } - } - */ - [EventName.TOOLS_EXTENSIONS_PROMPT_DISMISSED]: { - extensionId: 'ms-python.pylint' | 'ms-python.flake8'; - dismissType: 'close' | 'doNotShow'; - }; /* __GDPR__ "query-expfeature" : { "owner": "luabud", diff --git a/src/client/telemetry/types.ts b/src/client/telemetry/types.ts index ae98707d94a8..865dca278bf0 100644 --- a/src/client/telemetry/types.ts +++ b/src/client/telemetry/types.ts @@ -8,10 +8,6 @@ import { EventName } from './constants'; export type EditorLoadTelemetry = IEventNamePropertyMapping[EventName.EDITOR_LOAD]; -export type LinterTrigger = 'auto' | 'save' | 'manual'; - -export type LintingTelemetry = IEventNamePropertyMapping[EventName.LINTING]; - export type PythonInterpreterTelemetry = IEventNamePropertyMapping[EventName.PYTHON_INTERPRETER]; export type DebuggerTelemetry = IEventNamePropertyMapping[EventName.DEBUGGER]; export type TestTool = 'pytest' | 'unittest'; diff --git a/src/test/common.ts b/src/test/common.ts index 4cc985c795b6..2ef366a3a472 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -40,23 +40,12 @@ export enum OSType { export type PythonSettingKeys = | 'defaultInterpreterPath' | 'languageServer' - | 'linting.lintOnSave' - | 'linting.enabled' - | 'linting.pylintEnabled' - | 'linting.flake8Enabled' - | 'linting.pycodestyleEnabled' - | 'linting.pylamaEnabled' - | 'linting.prospectorEnabled' - | 'linting.pydocstyleEnabled' - | 'linting.mypyEnabled' - | 'linting.banditEnabled' | 'testing.pytestArgs' | 'testing.unittestArgs' | 'formatting.provider' | 'testing.pytestEnabled' | 'testing.unittestEnabled' | 'envFile' - | 'linting.ignorePatterns' | 'terminal.activateEnvironment'; async function disposePythonSettings() { diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index e43ac7b7fbd8..83b5b4a3d524 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -20,7 +20,6 @@ import { IAutoCompleteSettings, IExperiments, IInterpreterSettings, - ILintingSettings, ITerminalSettings, } from '../../../client/common/types'; import { noop } from '../../../client/common/utils/misc'; @@ -115,7 +114,6 @@ suite('Python Settings', async () => { // complex settings config.setup((c) => c.get('interpreter')).returns(() => sourceSettings.interpreter); - config.setup((c) => c.get('linting')).returns(() => sourceSettings.linting); config.setup((c) => c.get('autoComplete')).returns(() => sourceSettings.autoComplete); config.setup((c) => c.get('testing')).returns(() => sourceSettings.testing); config.setup((c) => c.get('terminal')).returns(() => sourceSettings.terminal); diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts deleted file mode 100644 index 9523572ccfe2..000000000000 --- a/src/test/common/installer.test.ts +++ /dev/null @@ -1,331 +0,0 @@ -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri } from 'vscode'; -import { IExtensionSingleActivationService } from '../../client/activation/types'; -import { ActiveResourceService } from '../../client/common/application/activeResource'; -import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; -import { ClipboardService } from '../../client/common/application/clipboard'; -import { ReloadVSCodeCommandHandler } from '../../client/common/application/commands/reloadCommand'; -import { ReportIssueCommandHandler } from '../../client/common/application/commands/reportIssueCommand'; -import { DebugService } from '../../client/common/application/debugService'; -import { DebugSessionTelemetry } from '../../client/common/application/debugSessionTelemetry'; -import { DocumentManager } from '../../client/common/application/documentManager'; -import { Extensions } from '../../client/common/application/extensions'; -import { - IActiveResourceService, - IApplicationEnvironment, - IApplicationShell, - IClipboard, - ICommandManager, - IDebugService, - IDocumentManager, - IWorkspaceService, -} from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { ExperimentService } from '../../client/common/experiments/service'; -import { InstallationChannelManager } from '../../client/common/installer/channelManager'; -import { ProductInstaller } from '../../client/common/installer/productInstaller'; -import { LinterProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; -import { ProductService } from '../../client/common/installer/productService'; -import { - IInstallationChannelManager, - IModuleInstaller, - IProductPathService, - IProductService, -} from '../../client/common/installer/types'; -import { InterpreterPathService } from '../../client/common/interpreterPathService'; -import { BrowserService } from '../../client/common/net/browser'; -import { PersistentStateFactory } from '../../client/common/persistentState'; -import { PathUtils } from '../../client/common/platform/pathUtils'; -import { CurrentProcess } from '../../client/common/process/currentProcess'; -import { ProcessLogger } from '../../client/common/process/logger'; -import { IProcessLogger, IProcessServiceFactory } from '../../client/common/process/types'; -import { TerminalActivator } from '../../client/common/terminal/activator'; -import { PowershellTerminalActivationFailedHandler } from '../../client/common/terminal/activator/powershellFailedHandler'; -import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; -import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; -import { Nushell } from '../../client/common/terminal/environmentActivationProviders/nushell'; -import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; -import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; -import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; -import { TerminalServiceFactory } from '../../client/common/terminal/factory'; -import { TerminalHelper } from '../../client/common/terminal/helper'; -import { SettingsShellDetector } from '../../client/common/terminal/shellDetectors/settingsShellDetector'; -import { TerminalNameShellDetector } from '../../client/common/terminal/shellDetectors/terminalNameShellDetector'; -import { UserEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; -import { VSCEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; -import { - IShellDetector, - ITerminalActivationCommandProvider, - ITerminalActivationHandler, - ITerminalActivator, - ITerminalHelper, - ITerminalServiceFactory, - TerminalActivationProviders, -} from '../../client/common/terminal/types'; -import { - IBrowserService, - IConfigurationService, - ICurrentProcess, - IExperimentService, - IExtensions, - IInstaller, - IInterpreterPathService, - IPathUtils, - IPersistentStateFactory, - IRandom, - IsWindows, - Product, - ProductType, -} from '../../client/common/types'; -import { createDeferred } from '../../client/common/utils/async'; -import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; -import { Random } from '../../client/common/utils/random'; -import { ImportTracker } from '../../client/telemetry/importTracker'; -import { IImportTracker } from '../../client/telemetry/types'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { MockModuleInstaller } from '../mocks/moduleInstaller'; -import { MockProcessService } from '../mocks/proc'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; -import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST, TEST_TIMEOUT } from '../initialize'; -import { IActivatedEnvironmentLaunch } from '../../client/interpreter/contracts'; -import { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs/activatedEnvLaunch'; -import { - IPythonPathUpdaterServiceFactory, - IPythonPathUpdaterServiceManager, -} from '../../client/interpreter/configuration/types'; -import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; -import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; -import { getProductsForInstallerTests } from './productsToTest'; - -suite('Installer', () => { - let ioc: UnitTestIocContainer; - const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); - const resource = IS_MULTI_ROOT_TEST ? workspaceUri : undefined; - suiteSetup(initializeTest); - setup(async () => { - await initializeTest(); - await resetSettings(); - await initializeDI(); - }); - suiteTeardown(async () => { - await closeActiveWindows(); - await resetSettings(); - }); - teardown(async () => { - await ioc.dispose(); - await closeActiveWindows(); - }); - - async function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerUnitTestTypes(); - ioc.registerFileSystemTypes(); - ioc.registerVariableTypes(); - ioc.registerLinterTypes(); - ioc.registerInterpreterStorageTypes(); - - ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); - ioc.serviceManager.addSingleton(IInstaller, ProductInstaller); - ioc.serviceManager.addSingleton(IPathUtils, PathUtils); - ioc.serviceManager.addSingleton(IProcessLogger, ProcessLogger); - ioc.serviceManager.addSingleton(ICurrentProcess, CurrentProcess); - ioc.serviceManager.addSingleton( - IInstallationChannelManager, - InstallationChannelManager, - ); - ioc.serviceManager.addSingletonInstance( - ICommandManager, - TypeMoq.Mock.ofType().object, - ); - - ioc.serviceManager.addSingletonInstance( - IApplicationShell, - TypeMoq.Mock.ofType().object, - ); - ioc.serviceManager.addSingleton(IConfigurationService, ConfigurationService); - ioc.serviceManager.addSingleton(IWorkspaceService, WorkspaceService); - - await ioc.registerMockInterpreterTypes(); - ioc.registerMockProcessTypes(); - ioc.serviceManager.addSingletonInstance(IsWindows, false); - ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - LinterProductPathService, - ProductType.Linter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - TestFrameworkProductPathService, - ProductType.TestFramework, - ); - ioc.serviceManager.addSingleton( - IActivatedEnvironmentLaunch, - ActivatedEnvironmentLaunch, - ); - ioc.serviceManager.addSingleton( - IPythonPathUpdaterServiceManager, - PythonPathUpdaterService, - ); - ioc.serviceManager.addSingleton( - IPythonPathUpdaterServiceFactory, - PythonPathUpdaterServiceFactory, - ); - ioc.serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); - ioc.serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); - ioc.serviceManager.addSingleton(IExtensions, Extensions); - ioc.serviceManager.addSingleton(IRandom, Random); - ioc.serviceManager.addSingleton(ITerminalServiceFactory, TerminalServiceFactory); - ioc.serviceManager.addSingleton(IClipboard, ClipboardService); - ioc.serviceManager.addSingleton(IDocumentManager, DocumentManager); - ioc.serviceManager.addSingleton(IDebugService, DebugService); - ioc.serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); - ioc.serviceManager.addSingleton(IBrowserService, BrowserService); - ioc.serviceManager.addSingleton(ITerminalActivator, TerminalActivator); - ioc.serviceManager.addSingleton( - ITerminalActivationHandler, - PowershellTerminalActivationFailedHandler, - ); - ioc.serviceManager.addSingleton(IExperimentService, ExperimentService); - - ioc.serviceManager.addSingleton(ITerminalHelper, TerminalHelper); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - Bash, - TerminalActivationProviders.bashCShellFish, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - CommandPromptAndPowerShell, - TerminalActivationProviders.commandPromptAndPowerShell, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - Nushell, - TerminalActivationProviders.nushell, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - PyEnvActivationCommandProvider, - TerminalActivationProviders.pyenv, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - CondaActivationCommandProvider, - TerminalActivationProviders.conda, - ); - ioc.serviceManager.addSingleton( - ITerminalActivationCommandProvider, - PipEnvActivationCommandProvider, - TerminalActivationProviders.pipenv, - ); - ioc.serviceManager.addSingleton(IMultiStepInputFactory, MultiStepInputFactory); - ioc.serviceManager.addSingleton(IImportTracker, ImportTracker); - ioc.serviceManager.addBinding(IImportTracker, IExtensionSingleActivationService); - ioc.serviceManager.addSingleton(IShellDetector, TerminalNameShellDetector); - ioc.serviceManager.addSingleton(IShellDetector, SettingsShellDetector); - ioc.serviceManager.addSingleton(IShellDetector, UserEnvironmentShellDetector); - ioc.serviceManager.addSingleton(IShellDetector, VSCEnvironmentShellDetector); - ioc.serviceManager.addSingleton( - IExtensionSingleActivationService, - ReloadVSCodeCommandHandler, - ); - ioc.serviceManager.addSingleton( - IExtensionSingleActivationService, - ReportIssueCommandHandler, - ); - - ioc.serviceManager.addSingleton( - IExtensionSingleActivationService, - DebugSessionTelemetry, - ); - } - async function resetSettings() { - await updateSetting('linting.pylintEnabled', true, rootWorkspaceUri, ConfigurationTarget.Workspace); - } - - async function testCheckingIfProductIsInstalled(product: Product) { - const installer = ioc.serviceContainer.get(IInstaller); - const processService = (await ioc.serviceContainer - .get(IProcessServiceFactory) - .create()) as MockProcessService; - const checkInstalledDef = createDeferred(); - processService.onExec((_file, args, _options, callback) => { - const moduleName = installer.translateProductToModuleName(product); - if (args.length > 1 && args[0] === '-c' && args[1] === `import ${moduleName}`) { - checkInstalledDef.resolve(true); - } - callback({ stdout: '' }); - }); - await installer.isInstalled(product, resource); - await checkInstalledDef.promise; - } - - getProductsForInstallerTests().forEach((prod) => { - test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async function () { - if ( - new ProductService().getProductType(prod.value) === ProductType.DataScience || - new ProductService().getProductType(prod.value) === ProductType.Python - ) { - return this.skip(); - } - ioc.serviceManager.addSingletonInstance( - IModuleInstaller, - new MockModuleInstaller('one', false), - ); - ioc.serviceManager.addSingletonInstance( - IModuleInstaller, - new MockModuleInstaller('two', true), - ); - ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest) { - return undefined; - } - await testCheckingIfProductIsInstalled(prod.value); - - return undefined; - }).timeout(TEST_TIMEOUT * 3); - }); - - async function testInstallingProduct(product: Product) { - const installer = ioc.serviceContainer.get(IInstaller); - const checkInstalledDef = createDeferred(); - const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); - const moduleInstallerOne = moduleInstallers.find((item) => item.displayName === 'two')!; - - moduleInstallerOne.on('installModule', (name: Product | string) => { - if (product === name) { - checkInstalledDef.resolve(); - } - }); - await installer.install(product); - await checkInstalledDef.promise; - } - - getProductsForInstallerTests().forEach((prod) => { - test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async function () { - const productType = new ProductService().getProductType(prod.value); - if (productType === ProductType.DataScience || productType === ProductType.Python) { - return this.skip(); - } - ioc.serviceManager.addSingletonInstance( - IModuleInstaller, - new MockModuleInstaller('one', false), - ); - ioc.serviceManager.addSingletonInstance( - IModuleInstaller, - new MockModuleInstaller('two', true), - ); - ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest) { - return undefined; - } - await testInstallingProduct(prod.value); - - return undefined; - }).timeout(TEST_TIMEOUT * 3); - }); -}); diff --git a/src/test/common/installer/installer.invalidPath.unit.test.ts b/src/test/common/installer/installer.invalidPath.unit.test.ts deleted file mode 100644 index b6738759f0d7..000000000000 --- a/src/test/common/installer/installer.invalidPath.unit.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { Uri } from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; -import '../../../client/common/extensions'; -import { ProductInstaller } from '../../../client/common/installer/productInstaller'; -import { ProductService } from '../../../client/common/installer/productService'; -import { IProductPathService, IProductService } from '../../../client/common/installer/types'; -import { IPersistentState, IPersistentStateFactory, Product, ProductType } from '../../../client/common/types'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { getProductsForInstallerTests } from '../productsToTest'; - -use(chaiAsPromised); - -suite('Module Installer - Invalid Paths', () => { - [undefined, Uri.file('resource')].forEach((resource) => { - ['moduleName', path.join('users', 'dev', 'tool', 'executable')].forEach((pathToExecutable) => { - const isExecutableAModule = path.basename(pathToExecutable) === pathToExecutable; - - getProductsForInstallerTests().forEach((product) => { - let installer: ProductInstaller; - let serviceContainer: TypeMoq.IMock; - let app: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let productPathService: TypeMoq.IMock; - let persistentState: TypeMoq.IMock; - - setup(function () { - if (new ProductService().getProductType(product.value) === ProductType.DataScience) { - return this.skip(); - } - serviceContainer = TypeMoq.Mock.ofType(); - - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())) - .returns(() => new ProductService()); - app = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) - .returns(() => app.object); - workspaceService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) - .returns(() => workspaceService.object); - - productPathService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductPathService), TypeMoq.It.isAny())) - .returns(() => productPathService.object); - - const interpreterService = TypeMoq.Mock.ofType(); - - const pythonInterpreter = TypeMoq.Mock.ofType(); - - pythonInterpreter.setup((i) => (i as any).then).returns(() => undefined); - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(pythonInterpreter.object)); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) - .returns(() => interpreterService.object); - - persistentState = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory), TypeMoq.It.isAny())) - .returns(() => persistentState.object); - - installer = new ProductInstaller(serviceContainer.object); - }); - - switch (product.value) { - case Product.unittest: { - return; - } - default: { - test(`Ensure invalid path message is ${isExecutableAModule ? 'not displayed' : 'displayed'} ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - // If the path to executable is a module, then we won't display error message indicating path is invalid. - - productPathService - .setup((p) => - p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource)), - ) - .returns(() => pathToExecutable) - .verifiable(TypeMoq.Times.atLeast(isExecutableAModule ? 0 : 1)); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => isExecutableAModule) - .verifiable(TypeMoq.Times.atLeastOnce()); - const anyParams = [0, 1, 2, 3, 4, 5].map(() => TypeMoq.It.isAny()); - app.setup((a) => a.showErrorMessage(TypeMoq.It.isAny(), ...anyParams)) - .callback((message) => { - if (!isExecutableAModule) { - expect(message).contains(pathToExecutable); - } - }) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.exactly(1)); - const persistValue = TypeMoq.Mock.ofType>(); - persistValue.setup((pv) => pv.value).returns(() => false); - persistValue.setup((pv) => pv.updateValue(TypeMoq.It.isValue(true))); - persistentState - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistValue.object); - await installer.promptToInstall(product.value, resource); - productPathService.verifyAll(); - }); - } - } - }); - }); - }); -}); diff --git a/src/test/common/installer/installer.unit.test.ts b/src/test/common/installer/installer.unit.test.ts deleted file mode 100644 index 69a5f3678f69..000000000000 --- a/src/test/common/installer/installer.unit.test.ts +++ /dev/null @@ -1,621 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -/* eslint-disable max-classes-per-file */ - -import { assert, expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { Disposable, Uri, WorkspaceFolder } from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; -import '../../../client/common/extensions'; -import { ProductInstaller } from '../../../client/common/installer/productInstaller'; -import { ProductService } from '../../../client/common/installer/productService'; -import { - IInstallationChannelManager, - IModuleInstaller, - IProductPathService, - IProductService, -} from '../../../client/common/installer/types'; -import { - ExecutionResult, - IProcessService, - IProcessServiceFactory, - IPythonExecutionFactory, - IPythonExecutionService, -} from '../../../client/common/process/types'; -import { - IDisposableRegistry, - InstallerResponse, - IPersistentState, - IPersistentStateFactory, - Product, - ProductType, -} from '../../../client/common/types'; -import { createDeferred, Deferred } from '../../../client/common/utils/async'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { sleep } from '../../common'; -import { getProductsForInstallerTests } from '../productsToTest'; - -use(chaiAsPromised); - -suite('Module Installer only', () => { - [undefined, Uri.file('resource')].forEach((resource) => { - getProductsForInstallerTests() - .concat([{ name: 'Unknown product', value: 404 }]) - - .forEach((product) => { - let disposables: Disposable[] = []; - let installer: ProductInstaller; - let installationChannel: TypeMoq.IMock; - let moduleInstaller: TypeMoq.IMock; - let serviceContainer: TypeMoq.IMock; - let app: TypeMoq.IMock; - let promptDeferred: Deferred | undefined; - let workspaceService: TypeMoq.IMock; - let persistentStore: TypeMoq.IMock; - - let productPathService: TypeMoq.IMock; - let interpreterService: TypeMoq.IMock; - const productService = new ProductService(); - - setup(function () { - if (new ProductService().getProductType(product.value) === ProductType.DataScience) { - return this.skip(); - } - promptDeferred = createDeferred(); - serviceContainer = TypeMoq.Mock.ofType(); - - disposables = []; - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) - .returns(() => disposables); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())) - .returns(() => productService); - installationChannel = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInstallationChannelManager), TypeMoq.It.isAny())) - .returns(() => installationChannel.object); - app = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) - .returns(() => app.object); - workspaceService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) - .returns(() => workspaceService.object); - persistentStore = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory), TypeMoq.It.isAny())) - .returns(() => persistentStore.object); - - moduleInstaller = TypeMoq.Mock.ofType(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - moduleInstaller.setup((x: any) => x.then).returns(() => undefined); - installationChannel - .setup((i) => i.getInstallationChannel(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(moduleInstaller.object)); - installationChannel - .setup((i) => i.getInstallationChannel(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(moduleInstaller.object)); - - productPathService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductPathService), TypeMoq.It.isAny())) - .returns(() => productPathService.object); - productPathService - .setup((p) => p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => 'xyz'); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => true); - interpreterService = TypeMoq.Mock.ofType(); - const pythonInterpreter = TypeMoq.Mock.ofType(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pythonInterpreter.setup((i) => (i as any).then).returns(() => undefined); - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(pythonInterpreter.object)); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) - .returns(() => interpreterService.object); - installer = new ProductInstaller(serviceContainer.object); - - return undefined; - }); - - teardown(() => { - if (new ProductService().getProductType(product.value) === ProductType.DataScience) { - sinon.restore(); - return; - } - // This must be resolved, else all subsequent tests will fail (as this same promise will be used for other tests). - if (promptDeferred) { - promptDeferred.resolve(); - } - disposables.forEach((disposable) => { - if (disposable) { - disposable.dispose(); - } - }); - sinon.restore(); - }); - - switch (product.value) { - case 404 as Product: { - test(`If product type is not recognized, throw error (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - app.setup((a) => - a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - ).verifiable(TypeMoq.Times.never()); - const getProductType = sinon.stub(ProductService.prototype, 'getProductType'); - - getProductType.returns('random' as ProductType); - const promise = installer.promptToInstall(product.value, resource); - await expect(promise).to.eventually.be.rejectedWith(`Unknown product ${product.value}`); - app.verifyAll(); - assert.ok(getProductType.calledOnce); - }); - return; - } - case Product.unittest: { - test(`Ensure resource info is passed into the module installer ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const response = await installer.install(product.value, resource); - expect(response).to.be.equal(InstallerResponse.Installed); - }); - test(`Ensure resource info is passed into the module installer (created using ProductInstaller) ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const response = await installer.install(product.value, resource); - expect(response).to.be.equal(InstallerResponse.Installed); - }); - break; - } - - default: - test(`Ensure the prompt is displayed only once, until the prompt is closed, ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.exactly(resource ? 5 : 0)); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => promptDeferred!.promise) - .verifiable(TypeMoq.Times.once()); - const persistVal = TypeMoq.Mock.ofType>(); - persistVal.setup((p) => p.value).returns(() => false); - persistVal.setup((p) => p.updateValue(TypeMoq.It.isValue(true))); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object); - - // Display first prompt. - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - - // Display a few more prompts. - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - installer.promptToInstall(product.value, resource).ignoreErrors(); - await sleep(1); - - app.verifyAll(); - workspaceService.verifyAll(); - }); - test(`Ensure the prompt is displayed again when previous prompt has been closed, ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.exactly(resource ? 3 : 0)); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.exactly(3)); - const persistVal = TypeMoq.Mock.ofType>(); - persistVal.setup((p) => p.value).returns(() => false); - persistVal.setup((p) => p.updateValue(TypeMoq.It.isValue(true))); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object); - - await installer.promptToInstall(product.value, resource); - await installer.promptToInstall(product.value, resource); - await installer.promptToInstall(product.value, resource); - - app.verifyAll(); - workspaceService.verifyAll(); - }); - - if (product.value === Product.pylint) { - test(`Ensure the install prompt is not displayed when the user requests it not be shown again, ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.exactly(resource ? 2 : 0)); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue('Install'), - TypeMoq.It.isValue('Select Linter'), - TypeMoq.It.isValue("Don't show again"), - ), - ) - .returns(async () => "Don't show again") - .verifiable(TypeMoq.Times.once()); - const persistVal = TypeMoq.Mock.ofType>(); - let mockPersistVal = false; - persistVal.setup((p) => p.value).returns(() => mockPersistVal); - persistVal - .setup((p) => p.updateValue(TypeMoq.It.isValue(true))) - .returns(() => { - mockPersistVal = true; - return Promise.resolve(); - }) - .verifiable(TypeMoq.Times.once()); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object) - .verifiable(TypeMoq.Times.exactly(3)); - - // Display first prompt. - const initialResponse = await installer.promptToInstall(product.value, resource); - - // Display a second prompt. - const secondResponse = await installer.promptToInstall(product.value, resource); - - expect(initialResponse).to.be.equal(InstallerResponse.Ignore); - expect(secondResponse).to.be.equal(InstallerResponse.Ignore); - - app.verifyAll(); - workspaceService.verifyAll(); - persistentStore.verifyAll(); - persistVal.verifyAll(); - }); - } else if (productService.getProductType(product.value) === ProductType.Linter) { - test(`Ensure the 'do not show again' prompt isn't shown for non-pylint linters, ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue('Install'), - TypeMoq.It.isValue('Select Linter'), - ), - ) - .returns(async () => undefined) - .verifiable(TypeMoq.Times.once()); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue('Install'), - TypeMoq.It.isValue('Select Linter'), - TypeMoq.It.isValue("Don't show again"), - ), - ) - .returns(async () => undefined) - .verifiable(TypeMoq.Times.never()); - const persistVal = TypeMoq.Mock.ofType>(); - let mockPersistVal = false; - persistVal.setup((p) => p.value).returns(() => mockPersistVal); - persistVal - .setup((p) => p.updateValue(TypeMoq.It.isValue(true))) - .returns(() => { - mockPersistVal = true; - return Promise.resolve(); - }); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object); - - // Display the prompt. - await installer.promptToInstall(product.value, resource); - - // we're just ensuring the 'disable pylint' prompt never appears... - app.verifyAll(); - }); - } - - test(`Ensure resource info is passed into the module installer ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - moduleInstaller - .setup((m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => Promise.reject(new Error('UnitTesting'))); - - try { - await installer.install(product.value, resource); - } catch (ex) { - moduleInstaller.verify( - (m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - TypeMoq.Times.once(), - ); - } - }); - - test(`Return InstallerResponse.Ignore for the module installer ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - }) if installation channel is not defined`, async () => { - moduleInstaller - .setup((m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => Promise.reject(new Error('UnitTesting'))); - installationChannel.reset(); - installationChannel - .setup((i) => i.getInstallationChannel(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)); - try { - const response = await installer.install(product.value, resource); - expect(response).to.equal(InstallerResponse.Ignore); - } catch (ex) { - assert(false, `Should not throw errors, ${ex}`); - } - }); - test(`Ensure resource info is passed into the module installer (created using ProductInstaller) ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - moduleInstaller - .setup((m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => Promise.reject(new Error('UnitTesting'))); - - try { - await installer.install(product.value, resource); - } catch (ex) { - moduleInstaller.verify( - (m) => - m.installModule( - TypeMoq.It.isValue(product.value), - TypeMoq.It.isValue(resource), - TypeMoq.It.isValue(undefined), - ), - TypeMoq.Times.once(), - ); - } - }); - } - // Test isInstalled() - if (product.value === Product.unittest) { - test(`Method isInstalled() returns true for module installer ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const result = await installer.isInstalled(product.value, resource); - expect(result).to.equal(true, 'Should be true'); - }); - } else { - test(`Method isInstalled() returns true if module is installed for the module installer ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const pythonExecutionFactory = TypeMoq.Mock.ofType(); - const pythonExecutionService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPythonExecutionFactory))) - .returns(() => pythonExecutionFactory.object); - pythonExecutionFactory - .setup((p) => p.createActivatedEnvironment(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(pythonExecutionService.object)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pythonExecutionService.setup((p) => (p as any).then).returns(() => undefined); - pythonExecutionService - .setup((p) => p.isModuleInstalled(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - - const response = await installer.isInstalled(product.value, resource); - expect(response).to.equal(true, 'Should be true'); - pythonExecutionService.verifyAll(); - }); - test(`Method isInstalled() returns false if module is not installed for the module installer ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const pythonExecutionFactory = TypeMoq.Mock.ofType(); - const pythonExecutionService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPythonExecutionFactory))) - .returns(() => pythonExecutionFactory.object); - pythonExecutionFactory - .setup((p) => p.createActivatedEnvironment(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(pythonExecutionService.object)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pythonExecutionService.setup((p) => (p as any).then).returns(() => undefined); - pythonExecutionService - .setup((p) => p.isModuleInstalled(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.once()); - - const response = await installer.isInstalled(product.value, resource); - expect(response).to.equal(false, 'Should be false'); - - pythonExecutionService.verifyAll(); - }); - test(`Method isInstalled() returns true if running 'path/to/module_executable --version' succeeds for the module installer ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const processServiceFactory = TypeMoq.Mock.ofType(); - const processService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(IProcessServiceFactory)) - .returns(() => processServiceFactory.object); - processServiceFactory - .setup((p) => p.create(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(processService.object)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - processService.setup((p) => (p as any).then).returns(() => undefined); - const executionResult: ExecutionResult = { - stdout: 'output', - }; - processService - .setup((p) => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(executionResult)) - .verifiable(TypeMoq.Times.once()); - - productPathService.reset(); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => false); - - const response = await installer.isInstalled(product.value, resource); - expect(response).to.equal(true, 'Should be true'); - - processService.verifyAll(); - }); - test(`Method isInstalled() returns false if running 'path/to/module_executable --version' fails for the module installer ${ - product.name - } (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const processServiceFactory = TypeMoq.Mock.ofType(); - const processService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(IProcessServiceFactory)) - .returns(() => processServiceFactory.object); - processServiceFactory - .setup((p) => p.create(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(processService.object)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - processService.setup((p) => (p as any).then).returns(() => undefined); - processService - .setup((p) => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.reject(new Error('Kaboom'))) - .verifiable(TypeMoq.Times.once()); - - productPathService.reset(); - productPathService - .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) - .returns(() => false); - - const response = await installer.isInstalled(product.value, resource); - expect(response).to.equal(false, 'Should be false'); - - processService.verifyAll(); - }); - } - - // Test promptToInstall() when no interpreter is selected - test(`If no interpreter is selected, promptToInstall() doesn't prompt for product ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType().object) - .verifiable(TypeMoq.Times.never()); - app.setup((a) => - a.showErrorMessage( - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - ), - ) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - const persistVal = TypeMoq.Mock.ofType>(); - persistVal.setup((p) => p.value).returns(() => false); - persistVal.setup((p) => p.updateValue(TypeMoq.It.isValue(true))); - persistentStore - .setup((ps) => - ps.createGlobalPersistentState( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue(undefined), - ), - ) - .returns(() => persistVal.object); - - interpreterService.reset(); - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - await installer.promptToInstall(product.value, resource); - - app.verifyAll(); - interpreterService.verifyAll(); - workspaceService.verifyAll(); - }); - }); - }); -}); diff --git a/src/test/common/installer/moduleInstaller.unit.test.ts b/src/test/common/installer/moduleInstaller.unit.test.ts index 01ac0e315555..3df64ceb2dec 100644 --- a/src/test/common/installer/moduleInstaller.unit.test.ts +++ b/src/test/common/installer/moduleInstaller.unit.test.ts @@ -322,110 +322,6 @@ suite('Module Installer', () => { terminalService.verifyAll(); } - if (product.value === Product.pylint) { - generatePythonInterpreterVersions().forEach((interpreterInfo) => { - const majorVersion = interpreterInfo.version - ? interpreterInfo.version.major - : 0; - if (majorVersion === 2) { - const testTitle = `Ensure install arg is \'pylint<2.0.0\' in ${ - interpreterInfo.version ? interpreterInfo.version.raw : '' - }`; - if (InstallerClass === PipInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const proxyArgs = - proxyServer.length === 0 ? [] : ['--proxy', proxyServer]; - const expectedArgs = [ - '-m', - 'pip', - ...proxyArgs, - 'install', - '-U', - '"pylint<2.0.0"', - ]; - await installModuleAndVerifyCommand(pythonPath, expectedArgs); - }); - } - if (InstallerClass === PipEnvInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const expectedArgs = ['install', '"pylint<2.0.0"', '--dev']; - await installModuleAndVerifyCommand(pipenvName, expectedArgs); - }); - } - if (InstallerClass === CondaInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const expectedArgs = ['install']; - if (condaEnvInfo && condaEnvInfo.name) { - expectedArgs.push('--name'); - expectedArgs.push( - condaEnvInfo.name.toCommandArgumentForPythonExt(), - ); - } else if (condaEnvInfo && condaEnvInfo.path) { - expectedArgs.push('--prefix'); - expectedArgs.push( - condaEnvInfo.path.fileToCommandArgumentForPythonExt(), - ); - } - expectedArgs.push('"pylint<2.0.0"'); - expectedArgs.push('-y'); - await installModuleAndVerifyCommand(condaExecutable, expectedArgs); - }); - } - } else { - const testTitle = `Ensure install arg is \'pylint\' in ${ - interpreterInfo.version ? interpreterInfo.version.raw : '' - }`; - if (InstallerClass === PipInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const proxyArgs = - proxyServer.length === 0 ? [] : ['--proxy', proxyServer]; - const expectedArgs = [ - '-m', - 'pip', - ...proxyArgs, - 'install', - '-U', - 'pylint', - ]; - await installModuleAndVerifyCommand(pythonPath, expectedArgs); - }); - } - if (InstallerClass === PipEnvInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const expectedArgs = ['install', 'pylint', '--dev']; - await installModuleAndVerifyCommand(pipenvName, expectedArgs); - }); - } - if (InstallerClass === CondaInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const expectedArgs = ['install']; - if (condaEnvInfo && condaEnvInfo.name) { - expectedArgs.push('--name'); - expectedArgs.push( - condaEnvInfo.name.toCommandArgumentForPythonExt(), - ); - } else if (condaEnvInfo && condaEnvInfo.path) { - expectedArgs.push('--prefix'); - expectedArgs.push( - condaEnvInfo.path.fileToCommandArgumentForPythonExt(), - ); - } - expectedArgs.push('pylint'); - expectedArgs.push('-y'); - await installModuleAndVerifyCommand(condaExecutable, expectedArgs); - }); - } - } - }); - return; - } - if (InstallerClass === TestModuleInstaller) { suite(`If interpreter type is Unknown (${product.name})`, async () => { test(`If 'python.globalModuleInstallation' is set to true and pythonPath directory is read only, do an elevated install`, async () => { @@ -692,21 +588,6 @@ suite('Module Installer', () => { }); }); -function generatePythonInterpreterVersions() { - const versions: SemVer[] = ['2.7.0-final', '3.4.0-final', '3.5.0-final', '3.6.0-final', '3.7.0-final'].map( - (ver) => new SemVer(ver), - ); - return versions.map((version) => { - const info = TypeMoq.Mock.ofType(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - info.setup((t: any) => t.then).returns(() => undefined); - info.setup((t) => t.envType).returns(() => EnvironmentType.VirtualEnv); - info.setup((t) => t.version).returns(() => version); - info.setup((t) => t.path).returns(() => pythonPath); - return info.object; - }); -} - function getModuleNamesForTesting(): { name: string; value: Product; moduleName: string }[] { return getNamesAndValues(Product) .map((product) => { diff --git a/src/test/common/installer/productPath.unit.test.ts b/src/test/common/installer/productPath.unit.test.ts deleted file mode 100644 index 8e65f3a5caed..000000000000 --- a/src/test/common/installer/productPath.unit.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { fail } from 'assert'; -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as TypeMoq from 'typemoq'; -import { Uri } from 'vscode'; -import '../../../client/common/extensions'; -import { ProductInstaller } from '../../../client/common/installer/productInstaller'; -import { - BaseProductPathsService, - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../../client/common/installer/productPath'; -import { ProductService } from '../../../client/common/installer/productService'; -import { IProductService } from '../../../client/common/installer/types'; -import { IConfigurationService, IInstaller, IPythonSettings, Product, ProductType } from '../../../client/common/types'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { ILinterInfo, ILinterManager } from '../../../client/linters/types'; -import { ITestsHelper } from '../../../client/testing/common/types'; -import { ITestingSettings } from '../../../client/testing/configuration/types'; -import { getProductsForInstallerTests } from '../productsToTest'; - -use(chaiAsPromised); - -suite('Product Path', () => { - [undefined, Uri.file('resource')].forEach((resource) => { - getProductsForInstallerTests().forEach((product) => { - class TestBaseProductPathsService extends BaseProductPathsService { - public getExecutableNameFromSettings(_: Product, _resource?: Uri): string { - return ''; - } - } - let serviceContainer: TypeMoq.IMock; - let unitTestSettings: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let productInstaller: ProductInstaller; - setup(function () { - if (new ProductService().getProductType(product.value) === ProductType.DataScience) { - return this.skip(); - } - serviceContainer = TypeMoq.Mock.ofType(); - configService = TypeMoq.Mock.ofType(); - unitTestSettings = TypeMoq.Mock.ofType(); - - productInstaller = new ProductInstaller(serviceContainer.object); - const pythonSettings = TypeMoq.Mock.ofType(); - pythonSettings.setup((p) => p.testing).returns(() => unitTestSettings.object); - configService - .setup((s) => s.getSettings(TypeMoq.It.isValue(resource))) - .returns(() => pythonSettings.object); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) - .returns(() => configService.object); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IInstaller), TypeMoq.It.isAny())) - .returns(() => productInstaller); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())) - .returns(() => new ProductService()); - }); - - suite('Method isExecutableAModule()', () => { - test('Returns true if User has customized the executable name', () => { - productInstaller.translateProductToModuleName = () => 'moduleName'; - const productPathService = new TestBaseProductPathsService(serviceContainer.object); - productPathService.getExecutableNameFromSettings = () => 'executableName'; - expect(productPathService.isExecutableAModule(product.value)).to.equal(true, 'Should be true'); - }); - test('Returns false if User has customized the full path to executable', () => { - productInstaller.translateProductToModuleName = () => 'moduleName'; - const productPathService = new TestBaseProductPathsService(serviceContainer.object); - productPathService.getExecutableNameFromSettings = () => 'path/to/executable'; - expect(productPathService.isExecutableAModule(product.value)).to.equal(false, 'Should be false'); - }); - test('Returns false if translating product to module name fails with error', () => { - productInstaller.translateProductToModuleName = () => { - return new Error('Kaboom') as any; - }; - const productPathService = new TestBaseProductPathsService(serviceContainer.object); - productPathService.getExecutableNameFromSettings = () => 'executableName'; - expect(productPathService.isExecutableAModule(product.value)).to.equal(false, 'Should be false'); - }); - }); - const productType = new ProductService().getProductType(product.value); - switch (productType) { - case ProductType.Linter: { - test(`Ensure path is returned for ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const productPathService = new LinterProductPathService(serviceContainer.object); - const linterManager = TypeMoq.Mock.ofType(); - const linterInfo = TypeMoq.Mock.ofType(); - const expectedPath = 'Some Path'; - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) - .returns(() => linterManager.object); - linterInfo - .setup((l) => l.pathName(TypeMoq.It.isValue(resource))) - .returns(() => expectedPath) - .verifiable(TypeMoq.Times.once()); - linterManager - .setup((l) => l.getLinterInfo(TypeMoq.It.isValue(product.value))) - .returns(() => linterInfo.object) - .verifiable(TypeMoq.Times.once()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - expect(value).to.be.equal(expectedPath); - linterInfo.verifyAll(); - linterManager.verifyAll(); - }); - break; - } - case ProductType.TestFramework: { - test(`Ensure path is returned for ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const productPathService = new TestFrameworkProductPathService(serviceContainer.object); - const testHelper = TypeMoq.Mock.ofType(); - const expectedPath = 'Some Path'; - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(ITestsHelper), TypeMoq.It.isAny())) - .returns(() => testHelper.object); - testHelper - .setup((t) => t.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) - .returns(() => { - return { - argsName: 'autoTestDiscoverOnSaveEnabled', - enabledName: 'autoTestDiscoverOnSaveEnabled', - pathName: 'pytestPath', - }; - }) - .verifiable(TypeMoq.Times.once()); - unitTestSettings - .setup((u) => u.pytestPath) - .returns(() => expectedPath) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - expect(value).to.be.equal(expectedPath); - testHelper.verifyAll(); - unitTestSettings.verifyAll(); - }); - test(`Ensure module name is returned for ${product.name} (${ - resource ? 'With a resource' : 'without a resource' - })`, async () => { - const productPathService = new TestFrameworkProductPathService(serviceContainer.object); - const testHelper = TypeMoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(ITestsHelper), TypeMoq.It.isAny())) - .returns(() => testHelper.object); - testHelper - .setup((t) => t.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) - .returns(() => { - return { - argsName: 'autoTestDiscoverOnSaveEnabled', - enabledName: 'autoTestDiscoverOnSaveEnabled', - pathName: undefined, - }; - }) - .verifiable(TypeMoq.Times.once()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - const moduleName = productInstaller.translateProductToModuleName(product.value); - expect(value).to.be.equal(moduleName); - testHelper.verifyAll(); - }); - break; - } - default: { - test(`No tests for Product Path of this Product Type ${product.name}`, () => { - fail('No tests for Product Path of this Product Type'); - }); - } - } - }); - }); -}); diff --git a/src/test/common/installer/serviceRegistry.unit.test.ts b/src/test/common/installer/serviceRegistry.unit.test.ts index 5b971790fa9a..8a811ad7ac4d 100644 --- a/src/test/common/installer/serviceRegistry.unit.test.ts +++ b/src/test/common/installer/serviceRegistry.unit.test.ts @@ -9,10 +9,7 @@ import { CondaInstaller } from '../../../client/common/installer/condaInstaller' import { PipEnvInstaller } from '../../../client/common/installer/pipEnvInstaller'; import { PipInstaller } from '../../../client/common/installer/pipInstaller'; import { PoetryInstaller } from '../../../client/common/installer/poetryInstaller'; -import { - LinterProductPathService, - TestFrameworkProductPathService, -} from '../../../client/common/installer/productPath'; +import { TestFrameworkProductPathService } from '../../../client/common/installer/productPath'; import { ProductService } from '../../../client/common/installer/productService'; import { registerTypes } from '../../../client/common/installer/serviceRegistry'; import { @@ -45,13 +42,6 @@ suite('Common installer Service Registry', () => { ), ).once(); verify(serviceManager.addSingleton(IProductService, ProductService)).once(); - verify( - serviceManager.addSingleton( - IProductPathService, - LinterProductPathService, - ProductType.Linter, - ), - ).once(); verify( serviceManager.addSingleton( IProductPathService, diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index d91c32fc7350..6d1d153aba94 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -3,7 +3,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import { SemVer } from 'semver'; import { instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri } from 'vscode'; +import { Uri } from 'vscode'; import { IExtensionSingleActivationService } from '../../client/activation/types'; import { ActiveResourceService } from '../../client/common/application/activeResource'; import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; @@ -96,7 +96,7 @@ import { JupyterExtensionDependencyManager } from '../../client/jupyter/jupyterE import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; import { ImportTracker } from '../../client/telemetry/importTracker'; import { IImportTracker } from '../../client/telemetry/types'; -import { PYTHON_PATH, rootWorkspaceUri } from '../common'; +import { PYTHON_PATH } from '../common'; import { MockModuleInstaller } from '../mocks/moduleInstaller'; import { MockProcessService } from '../mocks/proc'; import { UnitTestIocContainer } from '../testing/serviceRegistry'; @@ -130,7 +130,6 @@ suite('Module Installer', () => { chaiShould(); await initializeDI(); await initializeTest(); - await resetSettings(); }); suiteTeardown(async () => { await closeActiveWindows(); @@ -144,7 +143,6 @@ suite('Module Installer', () => { ioc = new UnitTestIocContainer(); ioc.registerUnitTestTypes(); ioc.registerVariableTypes(); - ioc.registerLinterTypes(); ioc.registerInterpreterStorageTypes(); ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); @@ -263,15 +261,6 @@ suite('Module Installer', () => { DebugSessionTelemetry, ); } - async function resetSettings(): Promise { - const configService = ioc.serviceManager.get(IConfigurationService); - await configService.updateSetting( - 'linting.pylintEnabled', - true, - rootWorkspaceUri, - ConfigurationTarget.Workspace, - ); - } test('Ensure pip is supported and conda is not', async () => { ioc.serviceManager.addSingletonInstance( IModuleInstaller, diff --git a/src/test/common/productsToTest.ts b/src/test/common/productsToTest.ts deleted file mode 100644 index e82d12bbd9eb..000000000000 --- a/src/test/common/productsToTest.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Product } from '../../client/common/types'; -import { getNamesAndValues } from '../../client/common/utils/enum'; - -export function getProductsForInstallerTests(): { name: string; value: Product }[] { - return getNamesAndValues(Product).filter( - (p) => - !['pylint', 'flake8', 'pycodestyle', 'pylama', 'prospector', 'pydocstyle', 'mypy', 'bandit'].includes( - p.name, - ), - ); -} diff --git a/src/test/install/channelManager.channels.test.ts b/src/test/install/channelManager.channels.test.ts index 5e102a0a5182..0d8190f046a3 100644 --- a/src/test/install/channelManager.channels.test.ts +++ b/src/test/install/channelManager.channels.test.ts @@ -89,7 +89,7 @@ suite('Installation - installation channels', () => { installer2.setup((x) => x.displayName).returns(() => 'Name 2'); const cm = new InstallationChannelManager(serviceContainer); - await cm.getInstallationChannel(Product.pylint); + await cm.getInstallationChannel(Product.pytest); assert.notStrictEqual(items, undefined, 'showQuickPick not called'); assert.strictEqual(items!.length, 2, 'Incorrect number of installer shown'); diff --git a/src/test/install/channelManager.messages.test.ts b/src/test/install/channelManager.messages.test.ts index c21612e8f56c..326ba1ad4bfd 100644 --- a/src/test/install/channelManager.messages.test.ts +++ b/src/test/install/channelManager.messages.test.ts @@ -185,7 +185,7 @@ suite('Installation - channel messages', () => { if (methodType === 'showNoInstallersMessage') { await channels.showNoInstallersMessage(); } else { - await channels.getInstallationChannel(Product.pylint); + await channels.getInstallationChannel(Product.pytest); } await verify(message, url); } diff --git a/src/test/linters/bandit.unit.test.ts b/src/test/linters/bandit.unit.test.ts deleted file mode 100644 index 6a44158034bd..000000000000 --- a/src/test/linters/bandit.unit.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { parseLine } from '../../client/linters/baseLinter'; -import { BANDIT_REGEX } from '../../client/linters/bandit'; - -import { ILintMessage, LinterId } from '../../client/linters/types'; - -suite('Linting - Bandit', () => { - test('parsing new bandit with col', () => { - const newOutput = `\ -1,0,LOW,B404:Consider possible security implications associated with subprocess module. -19,4,HIGH,B602:subprocess call with shell=True identified, security issue. -`; - - const lines = newOutput.split('\n'); - const tests: [string, ILintMessage | undefined][] = [ - [ - lines[0], - { - code: 'B404', - message: 'Consider possible security implications associated with subprocess module.', - column: 0, - line: 1, - type: 'LOW', - provider: 'bandit', - }, - ], - [ - lines[1], - { - code: 'B602', - message: 'subprocess call with shell=True identified, security issue.', - column: 3, - line: 19, - type: 'HIGH', - provider: 'bandit', - }, - ], - ]; - for (const [line, expected] of tests) { - const msg = parseLine(line, BANDIT_REGEX, LinterId.Bandit, 1); - - expect(msg).to.deep.equal(expected); - } - }); - test('parsing old bandit with no col', () => { - const newOutput = `\ -1,col,LOW,B404:Consider possible security implications associated with subprocess module. -19,col,HIGH,B602:subprocess call with shell=True identified, security issue. -`; - - const lines = newOutput.split('\n'); - const tests: [string, ILintMessage | undefined][] = [ - [ - lines[0], - { - code: 'B404', - message: 'Consider possible security implications associated with subprocess module.', - column: 0, - line: 1, - type: 'LOW', - provider: 'bandit', - }, - ], - [ - lines[1], - { - code: 'B602', - message: 'subprocess call with shell=True identified, security issue.', - column: 0, - line: 19, - type: 'HIGH', - provider: 'bandit', - }, - ], - ]; - for (const [line, expected] of tests) { - const msg = parseLine(line, BANDIT_REGEX, LinterId.Bandit, 1); - - expect(msg).to.deep.equal(expected); - } - }); -}); diff --git a/src/test/linters/common.ts b/src/test/linters/common.ts deleted file mode 100644 index 3c8f72a8d710..000000000000 --- a/src/test/linters/common.ts +++ /dev/null @@ -1,405 +0,0 @@ -/* eslint-disable max-classes-per-file */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as os from 'os'; -import * as TypeMoq from 'typemoq'; -import { DiagnosticSeverity, TextDocument, Uri, WorkspaceFolder } from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; -import { Product } from '../../client/common/installer/productInstaller'; -import { ProductNames } from '../../client/common/installer/productNames'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { IPythonExecutionFactory, IPythonToolExecutionService } from '../../client/common/process/types'; -import { - Flake8CategorySeverity, - IConfigurationService, - IInstaller, - IMypyCategorySeverity, - ILogOutputChannel, - IPycodestyleCategorySeverity, - IPylintCategorySeverity, - IPythonSettings, -} from '../../client/common/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import { LinterManager } from '../../client/linters/linterManager'; -import { ILinter, ILinterManager, ILintMessage, LinterId } from '../../client/linters/types'; - -export function newMockDocument(filename: string): TypeMoq.IMock { - const uri = Uri.file(filename); - const doc = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - doc.setup((s) => s.uri).returns(() => uri); - return doc; -} - -export function linterMessageAsLine(msg: ILintMessage): string { - switch (msg.provider) { - case 'pydocstyle': { - return `:${msg.line} spam:${os.EOL}\t${msg.code}: ${msg.message}`; - } - default: { - return `${msg.line},${msg.column},${msg.type},${msg.code}:${msg.message}`; - } - } -} - -function pylintMessageAsString(msg: ILintMessage, trailingComma = true): string { - return ` { - "type": "${msg.type}", - "line": ${msg.line}, - "column": ${msg.column}, - "symbol": "${msg.code}", - "message": "${msg.message}", - "endLine": ${msg.endLine ?? null}, - "endColumn": ${msg.endColumn ?? null} - }${trailingComma ? ',' : ''}`; -} - -export function pylintLinterMessagesAsOutput(messages: ILintMessage[]): string { - const lines: string[] = ['[']; - if (messages) { - const pylintMessages = messages.slice(0, -1).map((msg) => pylintMessageAsString(msg, true)); - const lastMessage = pylintMessageAsString(messages[messages.length - 1], false); - - lines.push(...pylintMessages, lastMessage); - } - lines.push(']'); - return lines.join(os.EOL); -} - -export function getLinterID(product: Product): LinterId { - const linterID = LINTERID_BY_PRODUCT.get(product); - if (!linterID) { - throwUnknownProduct(product); - } - return linterID!; -} - -export function getProductName(product: Product, capitalize = true): string { - let prodName = ProductNames.get(product); - if (!prodName) { - prodName = Product[product]; - } - if (capitalize) { - return prodName.charAt(0).toUpperCase() + prodName.slice(1); - } - return prodName; -} - -export function throwUnknownProduct(product: Product): void { - throw Error(`unsupported product ${Product[product]} (${product})`); -} - -export class LintingSettings { - public enabled: boolean; - - public cwd?: string; - - public ignorePatterns: string[]; - - public prospectorEnabled: boolean; - - public prospectorArgs: string[]; - - public pylintEnabled: boolean; - - public pylintArgs: string[]; - - public pycodestyleEnabled: boolean; - - public pycodestyleArgs: string[]; - - public pylamaEnabled: boolean; - - public pylamaArgs: string[]; - - public flake8Enabled: boolean; - - public flake8Args: string[]; - - public pydocstyleEnabled: boolean; - - public pydocstyleArgs: string[]; - - public lintOnSave: boolean; - - public maxNumberOfProblems: number; - - public pylintCategorySeverity: IPylintCategorySeverity; - - public pycodestyleCategorySeverity: IPycodestyleCategorySeverity; - - public flake8CategorySeverity: Flake8CategorySeverity; - - public mypyCategorySeverity: IMypyCategorySeverity; - - public prospectorPath: string; - - public pylintPath: string; - - public pycodestylePath: string; - - public pylamaPath: string; - - public flake8Path: string; - - public pydocstylePath: string; - - public mypyEnabled: boolean; - - public mypyArgs: string[]; - - public mypyPath: string; - - public banditEnabled: boolean; - - public banditArgs: string[]; - - public banditPath: string; - - constructor() { - // mostly from configSettings.ts - - this.enabled = true; - this.cwd = undefined; - this.ignorePatterns = []; - this.lintOnSave = false; - this.maxNumberOfProblems = 100; - - this.flake8Enabled = false; - this.flake8Path = 'flake8'; - this.flake8Args = []; - this.flake8CategorySeverity = { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning, - F: DiagnosticSeverity.Warning, - }; - - this.mypyEnabled = false; - this.mypyPath = 'mypy'; - this.mypyArgs = []; - this.mypyCategorySeverity = { - error: DiagnosticSeverity.Error, - note: DiagnosticSeverity.Hint, - }; - - this.banditEnabled = false; - this.banditPath = 'bandit'; - this.banditArgs = []; - - this.pycodestyleEnabled = false; - this.pycodestylePath = 'pycodestyle'; - this.pycodestyleArgs = []; - this.pycodestyleCategorySeverity = { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning, - }; - - this.pylamaEnabled = false; - this.pylamaPath = 'pylama'; - this.pylamaArgs = []; - - this.prospectorEnabled = false; - this.prospectorPath = 'prospector'; - this.prospectorArgs = []; - - this.pydocstyleEnabled = false; - this.pydocstylePath = 'pydocstyle'; - this.pydocstyleArgs = []; - - this.pylintEnabled = false; - this.pylintPath = 'pylint'; - this.pylintArgs = []; - this.pylintCategorySeverity = { - convention: DiagnosticSeverity.Hint, - error: DiagnosticSeverity.Error, - fatal: DiagnosticSeverity.Error, - refactor: DiagnosticSeverity.Hint, - warning: DiagnosticSeverity.Warning, - }; - } -} - -export class BaseTestFixture { - public serviceContainer: TypeMoq.IMock; - - public linterManager: LinterManager; - - // services - public workspaceService: TypeMoq.IMock; - - public installer: TypeMoq.IMock; - - public appShell: TypeMoq.IMock; - - // config - public configService: TypeMoq.IMock; - - public pythonSettings: TypeMoq.IMock; - - public lintingSettings: LintingSettings; - - // data - public outputChannel: TypeMoq.IMock; - - // artifacts - public output: string; - - public logged: string[]; - - constructor( - platformService: IPlatformService, - filesystem: IFileSystem, - pythonToolExecService: IPythonToolExecutionService, - pythonExecFactory: IPythonExecutionFactory, - configService?: TypeMoq.IMock, - serviceContainer?: TypeMoq.IMock, - ignoreConfigUpdates = false, - public readonly workspaceDir = '.', - protected readonly printLogs = false, - ) { - this.serviceContainer = - serviceContainer || TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - - // services - - this.workspaceService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.installer = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.appShell = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) - .returns(() => filesystem); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) - .returns(() => this.workspaceService.object); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInstaller), TypeMoq.It.isAny())) - .returns(() => this.installer.object); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())) - .returns(() => platformService); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPythonToolExecutionService), TypeMoq.It.isAny())) - .returns(() => pythonToolExecService); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPythonExecutionFactory), TypeMoq.It.isAny())) - .returns(() => pythonExecFactory); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) - .returns(() => this.appShell.object); - this.initServices(); - - // config - - this.configService = - configService || TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.pythonSettings = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.lintingSettings = new LintingSettings(); - - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) - .returns(() => this.configService.object); - this.configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => this.pythonSettings.object); - this.pythonSettings.setup((s) => s.linting).returns(() => this.lintingSettings); - this.initConfig(ignoreConfigUpdates); - - // data - - this.outputChannel = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILogOutputChannel))) - .returns(() => this.outputChannel.object); - this.initData(); - - // artifacts - - this.output = ''; - this.logged = []; - - // linting - - this.linterManager = new LinterManager(this.configService.object); - this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) - .returns(() => this.linterManager); - } - - public async getLinter(product: Product, enabled = true): Promise { - const info = this.linterManager.getLinterInfo(product); - - // @ts-ignore We only do this during testing. - this.lintingSettings[info.enabledSettingName] = enabled; - - await this.linterManager.setActiveLintersAsync([product]); - await this.linterManager.enableLintingAsync(enabled); - return this.linterManager.createLinter(product, this.serviceContainer.object); - } - - public async getEnabledLinter(product: Product): Promise { - return this.getLinter(product, true); - } - - public async getDisabledLinter(product: Product): Promise { - return this.getLinter(product, false); - } - - // eslint-disable-next-line class-methods-use-this - protected newMockDocument(filename: string): TypeMoq.IMock { - return newMockDocument(filename); - } - - private initServices(): void { - const workspaceFolder = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - workspaceFolder.setup((f) => f.uri).returns(() => Uri.file(this.workspaceDir)); - this.workspaceService - .setup((s) => s.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder.object); - - this.appShell - .setup((a) => a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)); - } - - private initConfig(ignoreUpdates = false): void { - this.configService - .setup((c) => - c.updateSetting(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - ) - .callback((setting, value) => { - if (ignoreUpdates) { - return; - } - const prefix = 'linting.'; - if (setting.startsWith(prefix)) { - // @ts-ignore We only do this during testing. - this.lintingSettings[setting.substr(prefix.length)] = value; - } - }) - .returns(() => Promise.resolve(undefined)); - - this.pythonSettings.setup((s) => s.languageServer).returns(() => LanguageServerType.Jedi); - } - - private initData(): void { - this.outputChannel - .setup((o) => o.appendLine(TypeMoq.It.isAny())) - .callback((line) => { - if (this.output === '') { - this.output = line; - } else { - this.output = `${this.output}${os.EOL}${line}`; - } - }); - this.outputChannel - .setup((o) => o.append(TypeMoq.It.isAny())) - .callback((data) => { - this.output += data; - }); - this.outputChannel.setup((o) => o.show()); - } -} diff --git a/src/test/linters/lint.args.test.ts b/src/test/linters/lint.args.test.ts deleted file mode 100644 index 2c32a73052bf..000000000000 --- a/src/test/linters/lint.args.test.ts +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { Container } from 'inversify'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, TextDocument, Uri, WorkspaceFolder } from 'vscode'; -import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import '../../client/common/extensions'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { - IConfigurationService, - IExtensions, - IInstaller, - ILintingSettings, - IPythonSettings, -} from '../../client/common/types'; -import { - IInterpreterAutoSelectionService, - IInterpreterAutoSelectionProxyService, -} from '../../client/interpreter/autoSelection/types'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { Bandit } from '../../client/linters/bandit'; -import { BaseLinter } from '../../client/linters/baseLinter'; -import { Flake8 } from '../../client/linters/flake8'; -import { LinterManager } from '../../client/linters/linterManager'; -import { MyPy } from '../../client/linters/mypy'; -import { Prospector } from '../../client/linters/prospector'; -import { Pycodestyle } from '../../client/linters/pycodestyle'; -import { PyDocStyle } from '../../client/linters/pydocstyle'; -import { PyLama } from '../../client/linters/pylama'; -import { Pylint } from '../../client/linters/pylint'; -import { ILinterManager, ILintingEngine } from '../../client/linters/types'; -import { initialize } from '../initialize'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; - -suite('Linting - Arguments', () => { - [undefined, path.join('users', 'dev_user')].forEach((workspaceUri) => { - [ - Uri.file(path.join('users', 'dev_user', 'development path to', 'one.py')), - Uri.file(path.join('users', 'dev_user', 'development', 'one.py')), - ].forEach((fileUri) => { - suite( - `File path ${fileUri.fsPath.indexOf(' ') > 0 ? 'with' : 'without'} spaces and ${ - workspaceUri ? 'without' : 'with' - } a workspace`, - () => { - let interpreterService: TypeMoq.IMock; - let engine: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let docManager: TypeMoq.IMock; - let settings: TypeMoq.IMock; - let lm: ILinterManager; - let serviceContainer: ServiceContainer; - let document: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let extensionsService: TypeMoq.IMock; - const cancellationToken = new CancellationTokenSource().token; - suiteSetup(initialize); - setup(async () => { - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - - serviceContainer = new ServiceContainer(cont); - - const fs = TypeMoq.Mock.ofType(); - fs.setup((x) => x.fileExists(TypeMoq.It.isAny())).returns( - () => new Promise((resolve) => resolve(true)), - ); - fs.setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns( - () => true, - ); - serviceManager.addSingletonInstance(IFileSystem, fs.object); - - interpreterService = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance( - IInterpreterService, - interpreterService.object, - ); - serviceManager.addSingleton( - IInterpreterAutoSelectionService, - MockAutoSelectionService, - ); - serviceManager.addSingleton( - IInterpreterAutoSelectionProxyService, - MockAutoSelectionService, - ); - engine = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(ILintingEngine, engine.object); - - docManager = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IDocumentManager, docManager.object); - - const lintSettings = TypeMoq.Mock.ofType(); - lintSettings.setup((x) => x.enabled).returns(() => true); - lintSettings.setup((x) => x.lintOnSave).returns(() => true); - lintSettings.setup((x) => x.cwd).returns(() => undefined); - - settings = TypeMoq.Mock.ofType(); - settings.setup((x) => x.linting).returns(() => lintSettings.object); - - configService = TypeMoq.Mock.ofType(); - configService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceManager.addSingletonInstance( - IConfigurationService, - configService.object, - ); - - const workspaceFolder: WorkspaceFolder | undefined = workspaceUri - ? { uri: Uri.file(workspaceUri), index: 0, name: '' } - : undefined; - workspaceService = TypeMoq.Mock.ofType(); - workspaceService - .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder); - serviceManager.addSingletonInstance( - IWorkspaceService, - workspaceService.object, - ); - - const installer = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IInstaller, installer.object); - - const platformService = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IPlatformService, platformService.object); - - extensionsService = TypeMoq.Mock.ofType(); - extensionsService.setup((e) => e.getExtension(TypeMoq.It.isAny())).returns(() => undefined); - serviceManager.addSingletonInstance(IExtensions, extensionsService.object); - - lm = new LinterManager(configService.object); - serviceManager.addSingletonInstance(ILinterManager, lm); - document = TypeMoq.Mock.ofType(); - }); - - async function testLinter(linter: BaseLinter, expectedArgs: string[]) { - document.setup((d) => d.uri).returns(() => fileUri); - - let invoked = false; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (linter as any).run = (args: string[]) => { - expect(args).to.deep.equal(expectedArgs); - invoked = true; - return Promise.resolve([]); - }; - await linter.lint(document.object, cancellationToken); - expect(invoked).to.be.equal(true, 'method not invoked'); - } - test('Flake8', async () => { - const linter = new Flake8(serviceContainer, { showPrompt: () => Promise.resolve(false) }); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Pycodestyle', async () => { - const linter = new Pycodestyle(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Prospector', async () => { - const linter = new Prospector(serviceContainer); - const expectedPath = workspaceUri - ? fileUri.fsPath.substring(workspaceUri.length + 2) - : path.basename(fileUri.fsPath); - const expectedArgs = [expectedPath]; - await testLinter(linter, expectedArgs); - }); - test('Pylama', async () => { - const linter = new PyLama(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('MyPy', async () => { - const linter = new MyPy(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Pydocstyle', async () => { - const linter = new PyDocStyle(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Pylint', async () => { - const linter = new Pylint(serviceContainer, { showPrompt: () => Promise.resolve(false) }); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('Bandit', async () => { - const linter = new Bandit(serviceContainer); - const expectedArgs = [fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - }, - ); - }); - }); -}); diff --git a/src/test/linters/lint.functional.test.ts b/src/test/linters/lint.functional.test.ts deleted file mode 100644 index 9887cbc5605a..000000000000 --- a/src/test/linters/lint.functional.test.ts +++ /dev/null @@ -1,889 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as fs from 'fs-extra'; -import * as os from 'os'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import { anything, instance, mock, when } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, TextDocument, TextLine, Uri } from 'vscode'; -import { Product } from '../../client/common/installer/productInstaller'; -import { FileSystem } from '../../client/common/platform/fileSystem'; -import { PlatformService } from '../../client/common/platform/platformService'; -import { IFileSystem } from '../../client/common/platform/types'; -import { ProcessServiceFactory } from '../../client/common/process/processFactory'; -import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; -import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; -import { - IProcessLogger, - IPythonExecutionFactory, - IPythonToolExecutionService, -} from '../../client/common/process/types'; -import { - IConfigurationService, - IDisposableRegistry, - IInterpreterPathService, - IPersistentState, -} from '../../client/common/types'; -import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; -import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; -import { - IActivatedEnvironmentLaunch, - IComponentAdapter, - IInterpreterService, -} from '../../client/interpreter/contracts'; -import { IServiceContainer } from '../../client/ioc/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import { ILintMessage, LinterId, LintMessageSeverity } from '../../client/linters/types'; -import { deleteFile, PYTHON_PATH } from '../common'; -import { BaseTestFixture, getLinterID, getProductName, newMockDocument, throwUnknownProduct } from './common'; -import { IInterpreterAutoSelectionService } from '../../client/interpreter/autoSelection/types'; -import { Conda } from '../../client/pythonEnvironments/common/environmentManagers/conda'; -import * as promptApis from '../../client/linters/prompts/common'; - -const workspaceDir = path.join(__dirname, '..', '..', '..', 'src', 'test'); -const workspaceUri = Uri.file(workspaceDir); -const pythonFilesDir = path.join(workspaceDir, 'pythonFiles', 'linting'); -const fileToLint = path.join(pythonFilesDir, 'file.py'); - -const linterConfigDirs = new Map([ - [LinterId.Flake8, path.join(pythonFilesDir, 'flake8config')], - [LinterId.PyCodeStyle, path.join(pythonFilesDir, 'pycodestyleconfig')], - [LinterId.PyDocStyle, path.join(pythonFilesDir, 'pydocstyleconfig27')], - [LinterId.PyLint, path.join(pythonFilesDir, 'pylintconfig')], -]); -const linterConfigRCFiles = new Map([ - [LinterId.PyLint, '.pylintrc'], - [LinterId.PyDocStyle, '.pydocstyle'], -]); - -const pylintMessagesToBeReturned: ILintMessage[] = [ - { - line: 24, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 30, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 34, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 40, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 44, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 55, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 59, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 62, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling undefined-variable (E0602)', - provider: '', - type: 'warning', - }, - { - line: 70, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 84, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 87, - column: 0, - severity: LintMessageSeverity.Hint, - code: 'C0304', - message: 'Final newline missing', - provider: '', - type: 'warning', - }, - { - line: 11, - column: 20, - severity: LintMessageSeverity.Warning, - code: 'W0613', - message: "Unused argument 'arg'", - provider: '', - type: 'warning', - }, - { - line: 26, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blop' member", - provider: '', - type: 'warning', - }, - { - line: 36, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 46, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 61, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 72, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 75, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 77, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 83, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, -]; -const flake8MessagesToBeReturned: ILintMessage[] = [ - { - line: 5, - column: 1, - severity: LintMessageSeverity.Error, - code: 'E302', - message: 'expected 2 blank lines, found 1', - provider: '', - type: 'E', - }, - { - line: 19, - column: 15, - severity: LintMessageSeverity.Error, - code: 'E127', - message: 'continuation line over-indented for visual indent', - provider: '', - type: 'E', - }, - { - line: 24, - column: 23, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 62, - column: 30, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 70, - column: 22, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 80, - column: 5, - severity: LintMessageSeverity.Error, - code: 'E303', - message: 'too many blank lines (2)', - provider: '', - type: 'E', - }, - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: 'E', - }, -]; -const pycodestyleMessagesToBeReturned: ILintMessage[] = [ - { - line: 5, - column: 1, - severity: LintMessageSeverity.Error, - code: 'E302', - message: 'expected 2 blank lines, found 1', - provider: '', - type: 'E', - }, - { - line: 19, - column: 15, - severity: LintMessageSeverity.Error, - code: 'E127', - message: 'continuation line over-indented for visual indent', - provider: '', - type: 'E', - }, - { - line: 24, - column: 23, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 62, - column: 30, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 70, - column: 22, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 80, - column: 5, - severity: LintMessageSeverity.Error, - code: 'E303', - message: 'too many blank lines (2)', - provider: '', - type: 'E', - }, - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: 'E', - }, -]; -const pydocstyleMessagesToBeReturned: ILintMessage[] = [ - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'e')", - column: 0, - line: 1, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 't')", - column: 0, - line: 5, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D102', - severity: LintMessageSeverity.Information, - message: 'Missing docstring in public method', - column: 4, - line: 8, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D401', - severity: LintMessageSeverity.Information, - message: "First line should be in imperative mood ('thi', not 'this')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('This', not 'this')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'e')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('And', not 'and')", - column: 4, - line: 15, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 't')", - column: 4, - line: 15, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 21, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 21, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 28, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 28, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 38, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 38, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 53, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 53, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 68, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 68, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 80, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 80, - type: '', - provider: 'pydocstyle', - }, -]; - -const filteredFlake8MessagesToBeReturned: ILintMessage[] = [ - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: '', - }, -]; -const filteredPycodestyleMessagesToBeReturned: ILintMessage[] = [ - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: '', - }, -]; - -function getMessages(product: Product): ILintMessage[] { - switch (product) { - case Product.pylint: { - return pylintMessagesToBeReturned; - } - case Product.flake8: { - return flake8MessagesToBeReturned; - } - case Product.pycodestyle: { - return pycodestyleMessagesToBeReturned; - } - case Product.pydocstyle: { - return pydocstyleMessagesToBeReturned; - } - default: { - throwUnknownProduct(product); - return []; - } - } -} - -async function getInfoForConfig(product: Product) { - const prodID = getLinterID(product); - const dirname = linterConfigDirs.get(prodID); - assert.notStrictEqual(dirname, undefined, `tests not set up for ${Product[product]}`); - - const filename = path.join(dirname!, product === Product.pylint ? 'file2.py' : 'file.py'); - let messagesToBeReceived: ILintMessage[] = []; - switch (product) { - case Product.flake8: { - messagesToBeReceived = filteredFlake8MessagesToBeReturned; - break; - } - case Product.pycodestyle: { - messagesToBeReceived = filteredPycodestyleMessagesToBeReturned; - break; - } - default: { - break; - } - } - const basename = linterConfigRCFiles.get(prodID); - return { - filename, - messagesToBeReceived, - origRCFile: basename ? path.join(dirname!, basename) : '', - }; -} - -class TestFixture extends BaseTestFixture { - constructor(printLogs = false) { - const serviceContainer = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const configService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const processLogger = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const componentAdapter = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - componentAdapter - .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)); - - const filesystem = new FileSystem(); - processLogger - .setup((p) => p.logProcess(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => { - /** No body */ - }); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IProcessLogger), TypeMoq.It.isAny())) - .returns(() => processLogger.object); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) - .returns(() => filesystem); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IComponentAdapter), TypeMoq.It.isAny())) - .returns(() => componentAdapter.object); - const activatedEnvironmentLaunch = TypeMoq.Mock.ofType(); - activatedEnvironmentLaunch - .setup((a) => a.selectIfLaunchedViaActivatedEnv()) - .returns(() => Promise.resolve(undefined)); - serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IActivatedEnvironmentLaunch), TypeMoq.It.isAny())) - .returns(() => activatedEnvironmentLaunch.object); - const platformService = new PlatformService(); - - super( - platformService, - filesystem, - TestFixture.newPythonToolExecService(serviceContainer.object), - TestFixture.newPythonExecFactory(serviceContainer, configService.object), - configService, - serviceContainer, - false, - workspaceDir, - printLogs, - ); - - this.pythonSettings.setup((s) => s.pythonPath).returns(() => PYTHON_PATH); - } - - private static newPythonToolExecService(serviceContainer: IServiceContainer): IPythonToolExecutionService { - // We do not worry about the IProcessServiceFactory possibly - // needed by PythonToolExecutionService. - return new PythonToolExecutionService(serviceContainer); - } - - private static newPythonExecFactory( - serviceContainer: TypeMoq.IMock, - configService: IConfigurationService, - ): IPythonExecutionFactory { - const envVarsService = TypeMoq.Mock.ofType( - undefined, - TypeMoq.MockBehavior.Strict, - ); - envVarsService - .setup((e) => e.getEnvironmentVariables(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(process.env)); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider), TypeMoq.It.isAny())) - .returns(() => envVarsService.object); - const disposableRegistry: IDisposableRegistry = []; - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) - .returns(() => disposableRegistry); - - const envActivationService = TypeMoq.Mock.ofType( - undefined, - TypeMoq.MockBehavior.Strict, - ); - - const interpreterService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - interpreterService.setup((i) => i.hasInterpreters()).returns(() => Promise.resolve(true)); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) - .returns(() => interpreterService.object); - - sinon.stub(Conda, 'getConda').resolves(new Conda('conda')); - sinon.stub(Conda.prototype, 'getCondaVersion').resolves(undefined); - - const processLogger = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - processLogger - .setup((p) => p.logProcess(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => { - /** No body */ - }); - const procServiceFactory = new ProcessServiceFactory( - envVarsService.object, - processLogger.object, - disposableRegistry, - ); - const pyenvs: IComponentAdapter = mock(); - - const autoSelection = mock(); - const interpreterPathExpHelper = mock(); - when(interpreterPathExpHelper.get(anything())).thenReturn('selected interpreter path'); - - return new PythonExecutionFactory( - serviceContainer.object, - envActivationService.object, - procServiceFactory, - configService, - instance(pyenvs), - instance(autoSelection), - instance(interpreterPathExpHelper), - ); - } - - // eslint-disable-next-line class-methods-use-this - public makeDocument(filename: string): TextDocument { - const doc = newMockDocument(filename); - - doc.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns((lno) => { - const lines = fs.readFileSync(filename).toString().split(os.EOL); - const textline = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - textline.setup((t) => t.text).returns(() => lines[lno]); - return textline.object; - }); - - return doc.object; - } -} - -suite('Linting Functional Tests', () => { - let isExtensionEnabledStub: sinon.SinonStub; - let isExtensionDisabledStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let persistentState: TypeMoq.IMock>; - setup(() => { - isExtensionEnabledStub = sinon.stub(promptApis, 'isExtensionEnabled'); - isExtensionDisabledStub = sinon.stub(promptApis, 'isExtensionDisabled'); - // For these tests we assume that linter extensions are not installed. - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - persistentState = TypeMoq.Mock.ofType>(); - persistentState.setup((p) => p.value).returns(() => true); - doNotShowPromptStateStub = sinon.stub(promptApis, 'doNotShowPromptState'); - doNotShowPromptStateStub.returns(persistentState.object); - }); - teardown(() => { - sinon.restore(); - }); - // These are integration tests that mock out everything except - // the filesystem and process execution. - - async function testLinterMessages( - fixture: TestFixture, - product: Product, - pythonFile: string, - messagesToBeReceived: ILintMessage[], - ) { - const doc = fixture.makeDocument(pythonFile); - await fixture.linterManager.setActiveLintersAsync([product], doc.uri); - const linter = await fixture.linterManager.createLinter(product, fixture.serviceContainer.object); - - const messages = await linter.lint(doc, new CancellationTokenSource().token); - - if (messagesToBeReceived.length === 0) { - assert.strictEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } else if (fixture.output.indexOf('ENOENT') === -1) { - // Pylint for Python Version 2.7 could return 80 linter messages, where as in 3.5 it might only return 1. - // Looks like pylint stops linting as soon as it comes across any ERRORS. - assert.notStrictEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - test(getProductName(product), async function () { - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { - return this.skip(); - } - - const fixture = new TestFixture(); - const messagesToBeReturned = getMessages(product); - await testLinterMessages(fixture, product, fileToLint, messagesToBeReturned); - - return undefined; - }); - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - test(`${getProductName(product)} with config in root`, async function () { - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { - return this.skip(); - } - - const fixture = new TestFixture(); - const { filename, messagesToBeReceived, origRCFile } = await getInfoForConfig(product); - let rcfile = ''; - async function cleanUp() { - if (rcfile !== '') { - await deleteFile(rcfile); - } - } - if (origRCFile !== '') { - rcfile = path.join(workspaceUri.fsPath, path.basename(origRCFile)); - await fs.copy(origRCFile, rcfile); - } - - try { - await testLinterMessages(fixture, product, filename, messagesToBeReceived); - } finally { - await cleanUp(); - } - - return undefined; - }); - } - - async function testLinterMessageCount( - fixture: TestFixture, - product: Product, - pythonFile: string, - messageCountToBeReceived: number, - ) { - const doc = fixture.makeDocument(pythonFile); - await fixture.linterManager.setActiveLintersAsync([product], doc.uri); - const linter = await fixture.linterManager.createLinter(product, fixture.serviceContainer.object); - - const messages = await linter.lint(doc, new CancellationTokenSource().token); - - assert.strictEqual( - messages.length, - messageCountToBeReceived, - 'Expected number of lint errors does not match lint error count', - ); - } - test('Three line output counted as one message', async () => { - const maxErrors = 5; - const fixture = new TestFixture(); - fixture.lintingSettings.maxNumberOfProblems = maxErrors; - await testLinterMessageCount( - fixture, - Product.pylint, - path.join(pythonFilesDir, 'threeLineLints.py'), - maxErrors, - ); - }); - - test('Linters use config in cwd directory', async () => { - const maxErrors = 0; - const fixture = new TestFixture(); - fixture.lintingSettings.cwd = path.join(pythonFilesDir, 'pylintcwd'); - - await testLinterMessageCount( - fixture, - Product.pylint, - path.join(pythonFilesDir, 'threeLineLints.py'), - maxErrors, - ); - }); -}); diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts deleted file mode 100644 index 5c1cae31d158..000000000000 --- a/src/test/linters/lint.multiroot.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import * as assert from 'assert'; -import * as path from 'path'; -import { CancellationTokenSource, ConfigurationTarget, Uri, workspace } from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { PythonSettings } from '../../client/common/configSettings'; -import { LinterProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; -import { ProductService } from '../../client/common/installer/productService'; -import { IProductPathService, IProductService } from '../../client/common/installer/types'; -import { IConfigurationService, Product, ProductType } from '../../client/common/types'; -import { OSType } from '../../client/common/utils/platform'; -import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; -import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; -import { - IPythonPathUpdaterServiceManager, - IPythonPathUpdaterServiceFactory, -} from '../../client/interpreter/configuration/types'; -import { IActivatedEnvironmentLaunch } from '../../client/interpreter/contracts'; -import { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs/activatedEnvLaunch'; -import { ILinter, ILinterManager } from '../../client/linters/types'; -import { isOs } from '../common'; -import { TEST_TIMEOUT } from '../constants'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; - -const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); - -suite('Multiroot Linting', () => { - const pylintSetting = 'linting.pylintEnabled'; - const flake8Setting = 'linting.flake8Enabled'; - - let ioc: UnitTestIocContainer; - suiteSetup(async function () { - if (!IS_MULTI_ROOT_TEST) { - this.skip(); - } - await initialize(); - await initializeDI(); - await initializeTest(); - }); - suiteTeardown(async () => { - await ioc?.dispose(); - await closeActiveWindows(); - PythonSettings.dispose(); - }); - teardown(async () => { - await closeActiveWindows(); - }); - - async function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(false); - ioc.registerProcessTypes(); - ioc.registerLinterTypes(); - ioc.registerVariableTypes(); - ioc.registerFileSystemTypes(); - await ioc.registerMockInterpreterTypes(); - ioc.serviceManager.addSingleton( - IActivatedEnvironmentLaunch, - ActivatedEnvironmentLaunch, - ); - ioc.serviceManager.addSingleton( - IPythonPathUpdaterServiceManager, - PythonPathUpdaterService, - ); - ioc.serviceManager.addSingleton( - IPythonPathUpdaterServiceFactory, - PythonPathUpdaterServiceFactory, - ); - ioc.registerInterpreterStorageTypes(); - ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - LinterProductPathService, - ProductType.Linter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - TestFrameworkProductPathService, - ProductType.TestFramework, - ); - } - - async function createLinter(product: Product): Promise { - const lm = ioc.serviceContainer.get(ILinterManager); - return lm.createLinter(product, ioc.serviceContainer); - } - async function testLinterInWorkspaceFolder( - product: Product, - workspaceFolderRelativePath: string, - mustHaveErrors: boolean, - ): Promise { - const fileToLint = path.join(multirootPath, workspaceFolderRelativePath, 'file.py'); - const cancelToken = new CancellationTokenSource(); - const document = await workspace.openTextDocument(fileToLint); - - const linter = await createLinter(product); - const messages = await linter.lint(document, cancelToken.token); - - const errorMessage = mustHaveErrors ? 'No errors returned by linter' : 'Errors returned by linter'; - assert.strictEqual(messages.length > 0, mustHaveErrors, errorMessage); - } - - test('Enabling Pylint in root and also in Workspace, should return errors', async function () { - // Timing out on Windows, tracked by #18337. - if (isOs(OSType.Windows)) { - return this.skip(); - } - - await runTest(Product.pylint, true, true, pylintSetting); - - return undefined; - }).timeout(TEST_TIMEOUT * 2); - test('Enabling Pylint in root and disabling in Workspace, should not return errors', async () => { - await runTest(Product.pylint, true, false, pylintSetting); - }).timeout(TEST_TIMEOUT * 2); - test('Disabling Pylint in root and enabling in Workspace, should return errors', async function () { - // Timing out on Windows, tracked by #18337. - if (isOs(OSType.Windows)) { - return this.skip(); - } - - await runTest(Product.pylint, false, true, pylintSetting); - - return undefined; - }).timeout(TEST_TIMEOUT * 2); - - test('Enabling Flake8 in root and also in Workspace, should return errors', async function () { - // Timing out on Windows, tracked by #18337. - if (isOs(OSType.Windows)) { - return this.skip(); - } - - await runTest(Product.flake8, true, true, flake8Setting); - - return undefined; - }).timeout(TEST_TIMEOUT * 2); - test('Enabling Flake8 in root and disabling in Workspace, should not return errors', async () => { - await runTest(Product.flake8, true, false, flake8Setting); - }).timeout(TEST_TIMEOUT * 2); - test('Disabling Flake8 in root and enabling in Workspace, should return errors', async function () { - // Timing out on Windows, tracked by #18337. - if (isOs(OSType.Windows)) { - return this.skip(); - } - - await runTest(Product.flake8, false, true, flake8Setting); - - return undefined; - }).timeout(TEST_TIMEOUT * 2); - - async function runTest(product: Product, global: boolean, wks: boolean, setting: string): Promise { - const config = ioc.serviceContainer.get(IConfigurationService); - await config.updateSetting( - 'languageServer', - LanguageServerType.Jedi, - Uri.file(multirootPath), - ConfigurationTarget.Global, - ); - await Promise.all([ - config.updateSetting(setting, global, Uri.file(multirootPath), ConfigurationTarget.Global), - config.updateSetting(setting, wks, Uri.file(multirootPath), ConfigurationTarget.Workspace), - ]); - await testLinterInWorkspaceFolder(product, 'workspace1', wks); - await Promise.all( - [ConfigurationTarget.Global, ConfigurationTarget.Workspace].map((configTarget) => - config.updateSetting(setting, undefined, Uri.file(multirootPath), configTarget), - ), - ); - } -}); diff --git a/src/test/linters/lint.provider.test.ts b/src/test/linters/lint.provider.test.ts deleted file mode 100644 index 760c2282ba05..000000000000 --- a/src/test/linters/lint.provider.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Container } from 'inversify'; -import * as TypeMoq from 'typemoq'; -import * as vscode from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { - IApplicationShell, - ICommandManager, - IDocumentManager, - IWorkspaceService, -} from '../../client/common/application/types'; -import { PersistentStateFactory } from '../../client/common/persistentState'; -import { IFileSystem } from '../../client/common/platform/types'; -import { - GLOBAL_MEMENTO, - IConfigurationService, - IInstaller, - ILintingSettings, - IMemento, - IPersistentStateFactory, - IPythonSettings, - Product, - Resource, - WORKSPACE_MEMENTO, -} from '../../client/common/types'; -import { createDeferred } from '../../client/common/utils/async'; -import { - IInterpreterAutoSelectionService, - IInterpreterAutoSelectionProxyService, -} from '../../client/interpreter/autoSelection/types'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { LinterManager } from '../../client/linters/linterManager'; -import { ILinterManager, ILintingEngine } from '../../client/linters/types'; -import { LinterProvider } from '../../client/providers/linterProvider'; -import { initialize } from '../initialize'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; -import { MockMemento } from '../mocks/mementos'; - -suite('Linting - Provider', () => { - let interpreterService: TypeMoq.IMock; - let engine: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let docManager: TypeMoq.IMock; - let settings: TypeMoq.IMock; - let lm: ILinterManager; - let serviceContainer: ServiceContainer; - let emitter: vscode.EventEmitter; - let document: TypeMoq.IMock; - let fs: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let linterInstaller: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let workspaceConfig: TypeMoq.IMock; - - suiteSetup(initialize); - setup(async () => { - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - - serviceContainer = new ServiceContainer(cont); - - fs = TypeMoq.Mock.ofType(); - fs.setup((x) => x.fileExists(TypeMoq.It.isAny())).returns( - () => new Promise((resolve) => resolve(true)), - ); - fs.setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => true); - serviceManager.addSingletonInstance(IFileSystem, fs.object); - - interpreterService = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); - - engine = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(ILintingEngine, engine.object); - - docManager = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IDocumentManager, docManager.object); - - const lintSettings = TypeMoq.Mock.ofType(); - lintSettings.setup((x) => x.enabled).returns(() => true); - lintSettings.setup((x) => x.lintOnSave).returns(() => true); - - settings = TypeMoq.Mock.ofType(); - settings.setup((x) => x.linting).returns(() => lintSettings.object); - settings.setup((p) => p.languageServer).returns(() => LanguageServerType.Jedi); - - configService = TypeMoq.Mock.ofType(); - configService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceManager.addSingletonInstance(IConfigurationService, configService.object); - - appShell = TypeMoq.Mock.ofType(); - linterInstaller = TypeMoq.Mock.ofType(); - - workspaceService = TypeMoq.Mock.ofType(); - workspaceConfig = TypeMoq.Mock.ofType(); - workspaceService - .setup((w) => w.getConfiguration('python', TypeMoq.It.isAny())) - .returns(() => workspaceConfig.object); - workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); - - serviceManager.addSingletonInstance(IApplicationShell, appShell.object); - serviceManager.addSingletonInstance(IInstaller, linterInstaller.object); - serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); - serviceManager.addSingleton( - IInterpreterAutoSelectionService, - MockAutoSelectionService, - ); - serviceManager.addSingleton( - IInterpreterAutoSelectionProxyService, - MockAutoSelectionService, - ); - serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); - serviceManager.addSingleton(IMemento, MockMemento, GLOBAL_MEMENTO); - serviceManager.addSingleton(IMemento, MockMemento, WORKSPACE_MEMENTO); - serviceManager.addSingletonInstance( - ICommandManager, - TypeMoq.Mock.ofType().object, - ); - lm = new LinterManager(configService.object); - serviceManager.addSingletonInstance(ILinterManager, lm); - emitter = new vscode.EventEmitter(); - document = TypeMoq.Mock.ofType(); - }); - - test('Lint on open file', async () => { - docManager.setup((x) => x.onDidOpenTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.py')); - document.setup((x) => x.languageId).returns(() => 'python'); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - engine.verify((x) => x.lintDocument(document.object, 'auto'), TypeMoq.Times.once()); - }); - - test('Lint on save file', async () => { - docManager.setup((x) => x.onDidSaveTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.py')); - document.setup((x) => x.languageId).returns(() => 'python'); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - engine.verify((x) => x.lintDocument(document.object, 'save'), TypeMoq.Times.once()); - }); - - test('No lint on open other files', async () => { - docManager.setup((x) => x.onDidOpenTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.cs')); - document.setup((x) => x.languageId).returns(() => 'csharp'); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - engine.verify((x) => x.lintDocument(document.object, 'save'), TypeMoq.Times.never()); - }); - - test('No lint on save other files', async () => { - docManager.setup((x) => x.onDidSaveTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.cs')); - document.setup((x) => x.languageId).returns(() => 'csharp'); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - engine.verify((x) => x.lintDocument(document.object, 'save'), TypeMoq.Times.never()); - }); - - test('Lint on change interpreters', async () => { - const e = new vscode.EventEmitter(); - interpreterService.setup((x) => x.onDidChangeInterpreter).returns(() => e.event); - - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - e.fire(undefined); - engine.verify((x) => x.lintOpenPythonFiles(), TypeMoq.Times.once()); - }); - - test('Lint on save pylintrc', async () => { - docManager.setup((x) => x.onDidSaveTextDocument).returns(() => emitter.event); - document.setup((x) => x.uri).returns(() => vscode.Uri.file('.pylintrc')); - - await lm.setActiveLintersAsync([Product.pylint]); - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - emitter.fire(document.object); - - const deferred = createDeferred(); - setTimeout(() => deferred.resolve(), 2000); - await deferred.promise; - engine.verify((x) => x.lintOpenPythonFiles(), TypeMoq.Times.once()); - }); - - test('Diagnostic cleared on file close', async () => testClearDiagnosticsOnClose(true)); - test('Diagnostic not cleared on file opened in another tab', async () => testClearDiagnosticsOnClose(false)); - - async function testClearDiagnosticsOnClose(closed: boolean) { - docManager.setup((x) => x.onDidCloseTextDocument).returns(() => emitter.event); - - const uri = vscode.Uri.file('test.py'); - document.setup((x) => x.uri).returns(() => uri); - document.setup((x) => x.isClosed).returns(() => closed); - - docManager.setup((x) => x.textDocuments).returns(() => (closed ? [] : [document.object])); - const linterProvider = new LinterProvider(serviceContainer); - await linterProvider.activate(); - - emitter.fire(document.object); - const timesExpected = closed ? TypeMoq.Times.once() : TypeMoq.Times.never(); - engine.verify((x) => x.clearDiagnostics(TypeMoq.It.isAny()), timesExpected); - } -}); diff --git a/src/test/linters/lint.test.ts b/src/test/linters/lint.test.ts deleted file mode 100644 index 837830f0c499..000000000000 --- a/src/test/linters/lint.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { ConfigurationTarget } from 'vscode'; -import { Product } from '../../client/common/installer/productInstaller'; -import { LinterProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; -import { ProductService } from '../../client/common/installer/productService'; -import { IProductPathService, IProductService } from '../../client/common/installer/types'; -import { IConfigurationService, ILintingSettings, ProductType } from '../../client/common/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import { LinterManager } from '../../client/linters/linterManager'; -import { ILinterManager } from '../../client/linters/types'; -import { rootWorkspaceUri } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; - -suite('Linting Settings', () => { - let ioc: UnitTestIocContainer; - let linterManager: ILinterManager; - let configService: IConfigurationService; - - suiteSetup(async () => { - await initialize(); - }); - setup(async () => { - await initializeDI(); - await initializeTest(); - }); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await closeActiveWindows(); - await resetSettings(); - await ioc.dispose(); - }); - - async function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(false); - ioc.registerProcessTypes(); - ioc.registerLinterTypes(); - ioc.registerVariableTypes(); - ioc.registerPlatformTypes(); - configService = ioc.serviceContainer.get(IConfigurationService); - linterManager = new LinterManager(configService); - ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton( - IProductPathService, - LinterProductPathService, - ProductType.Linter, - ); - ioc.serviceManager.addSingleton( - IProductPathService, - TestFrameworkProductPathService, - ProductType.TestFramework, - ); - } - - async function resetSettings(lintingEnabled = true) { - // Don't run these updates in parallel, as they are updating the same file. - const target = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - - await configService.updateSetting('linting.enabled', lintingEnabled, rootWorkspaceUri, target); - await configService.updateSetting('linting.lintOnSave', false, rootWorkspaceUri, target); - - linterManager.getAllLinterInfos().forEach(async (x) => { - const settingKey = `linting.${x.enabledSettingName}`; - await configService.updateSetting(settingKey, false, rootWorkspaceUri, target); - }); - } - - test('enable through manager (global)', async () => { - const settings = configService.getSettings(); - await resetSettings(false); - - await linterManager.enableLintingAsync(false); - assert.strictEqual(settings.linting.enabled, false, 'mismatch'); - - await linterManager.enableLintingAsync(true); - assert.strictEqual(settings.linting.enabled, true, 'mismatch'); - }); - - LINTERID_BY_PRODUCT.forEach((_, key) => { - const product = Product[key]; - - test(`enable through manager (${product})`, async () => { - const settings = configService.getSettings(); - await resetSettings(); - - const name = `${product}Enabled` as keyof ILintingSettings; - - assert.strictEqual(settings.linting[name], false, 'mismatch'); - - await linterManager.setActiveLintersAsync([key]); - - assert.strictEqual(settings.linting[name], true, 'mismatch'); - linterManager.getAllLinterInfos().forEach(async (x) => { - if (x.product !== key) { - assert.strictEqual( - settings.linting[x.enabledSettingName as keyof ILintingSettings], - false, - 'mismatch', - ); - } - }); - }); - }); -}); diff --git a/src/test/linters/lint.unit.test.ts b/src/test/linters/lint.unit.test.ts deleted file mode 100644 index 02bdd4c82c79..000000000000 --- a/src/test/linters/lint.unit.test.ts +++ /dev/null @@ -1,854 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as os from 'os'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, TextDocument, TextLine } from 'vscode'; -import { Product } from '../../client/common/installer/productInstaller'; -import { ProductNames } from '../../client/common/installer/productNames'; -import { ProductService } from '../../client/common/installer/productService'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { - IPythonExecutionFactory, - IPythonExecutionService, - IPythonToolExecutionService, -} from '../../client/common/process/types'; -import { IPersistentState, ProductType } from '../../client/common/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import * as promptApis from '../../client/linters/prompts/common'; -import { ILintMessage, LintMessageSeverity } from '../../client/linters/types'; -import { - BaseTestFixture, - getLinterID, - getProductName, - linterMessageAsLine, - pylintLinterMessagesAsOutput, - throwUnknownProduct, -} from './common'; - -const pylintMessagesToBeReturned: ILintMessage[] = [ - { - line: 24, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 30, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 34, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 40, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 44, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 55, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 59, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0012', - message: 'Locally enabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 62, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling undefined-variable (E0602)', - provider: '', - type: 'warning', - }, - { - line: 70, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 84, - column: 0, - severity: LintMessageSeverity.Information, - code: 'I0011', - message: 'Locally disabling no-member (E1101)', - provider: '', - type: 'warning', - }, - { - line: 87, - column: 0, - severity: LintMessageSeverity.Hint, - code: 'C0304', - message: 'Final newline missing', - provider: '', - type: 'warning', - }, - { - line: 11, - column: 20, - severity: LintMessageSeverity.Warning, - code: 'W0613', - message: "Unused argument 'arg'", - provider: '', - type: 'warning', - }, - { - line: 26, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blop' member", - provider: '', - type: 'warning', - }, - { - line: 36, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - }, - { - line: 46, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: undefined, - endColumn: undefined, - }, - { - line: 61, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 61, - endColumn: undefined, - }, - { - line: 72, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 72, - endColumn: 28, - }, - { - line: 75, - column: 18, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 75, - endColumn: 28, - }, - { - line: 77, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 77, - endColumn: 24, - }, - { - line: 83, - column: 14, - severity: LintMessageSeverity.Error, - code: 'E1101', - message: "Instance of 'Foo' has no 'blip' member", - provider: '', - type: 'warning', - endLine: 83, - endColumn: 24, - }, -]; -const flake8MessagesToBeReturned: ILintMessage[] = [ - { - line: 5, - column: 1, - severity: LintMessageSeverity.Error, - code: 'E302', - message: 'expected 2 blank lines, found 1', - provider: '', - type: 'E', - }, - { - line: 19, - column: 15, - severity: LintMessageSeverity.Error, - code: 'E127', - message: 'continuation line over-indented for visual indent', - provider: '', - type: 'E', - }, - { - line: 24, - column: 23, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 62, - column: 30, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 70, - column: 22, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 80, - column: 5, - severity: LintMessageSeverity.Error, - code: 'E303', - message: 'too many blank lines (2)', - provider: '', - type: 'E', - }, - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: 'E', - }, -]; -const pycodestyleMessagesToBeReturned: ILintMessage[] = [ - { - line: 5, - column: 1, - severity: LintMessageSeverity.Error, - code: 'E302', - message: 'expected 2 blank lines, found 1', - provider: '', - type: 'E', - }, - { - line: 19, - column: 15, - severity: LintMessageSeverity.Error, - code: 'E127', - message: 'continuation line over-indented for visual indent', - provider: '', - type: 'E', - }, - { - line: 24, - column: 23, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 62, - column: 30, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 70, - column: 22, - severity: LintMessageSeverity.Error, - code: 'E261', - message: 'at least two spaces before inline comment', - provider: '', - type: 'E', - }, - { - line: 80, - column: 5, - severity: LintMessageSeverity.Error, - code: 'E303', - message: 'too many blank lines (2)', - provider: '', - type: 'E', - }, - { - line: 87, - column: 24, - severity: LintMessageSeverity.Warning, - code: 'W292', - message: 'no newline at end of file', - provider: '', - type: 'E', - }, -]; -const pydocstyleMessagesToBeReturned: ILintMessage[] = [ - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'e')", - column: 0, - line: 1, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 't')", - column: 0, - line: 5, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D102', - severity: LintMessageSeverity.Information, - message: 'Missing docstring in public method', - column: 4, - line: 8, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D401', - severity: LintMessageSeverity.Information, - message: "First line should be in imperative mood ('thi', not 'this')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('This', not 'this')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'e')", - column: 4, - line: 11, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('And', not 'and')", - column: 4, - line: 15, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 't')", - column: 4, - line: 15, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 21, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 21, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 28, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 28, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 38, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 38, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 53, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 53, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 68, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 68, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D403', - severity: LintMessageSeverity.Information, - message: "First word of the first line should be properly capitalized ('Test', not 'test')", - column: 4, - line: 80, - type: '', - provider: 'pydocstyle', - }, - { - code: 'D400', - severity: LintMessageSeverity.Information, - message: "First line should end with a period (not 'g')", - column: 4, - line: 80, - type: '', - provider: 'pydocstyle', - }, -]; - -class TestFixture extends BaseTestFixture { - public platformService: TypeMoq.IMock; - - public filesystem: TypeMoq.IMock; - - public pythonToolExecService: TypeMoq.IMock; - - public pythonExecService: TypeMoq.IMock; - - public pythonExecFactory: TypeMoq.IMock; - - constructor(workspaceDir = '.', printLogs = false) { - const platformService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const filesystem = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - const pythonToolExecService = TypeMoq.Mock.ofType( - undefined, - TypeMoq.MockBehavior.Strict, - ); - const pythonExecFactory = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - super( - platformService.object, - filesystem.object, - pythonToolExecService.object, - pythonExecFactory.object, - undefined, - undefined, - true, - workspaceDir, - printLogs, - ); - - this.platformService = platformService; - this.filesystem = filesystem; - this.pythonToolExecService = pythonToolExecService; - this.pythonExecService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - this.pythonExecFactory = pythonExecFactory; - - this.filesystem.setup((f) => f.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.pythonExecService.setup((s: any) => s.then).returns(() => undefined); - this.pythonExecService - .setup((s) => s.isModuleInstalled(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)); - - this.pythonExecFactory - .setup((f) => f.create(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(this.pythonExecService.object)); - } - - public makeDocument(product: Product, filename: string): TextDocument { - const doc = this.newMockDocument(filename); - if (product === Product.pydocstyle) { - const dummyLine = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - dummyLine.setup((d) => d.text).returns(() => ' ...'); - doc.setup((s) => s.lineAt(TypeMoq.It.isAny())).returns(() => dummyLine.object); - } - return doc.object; - } - - public setDefaultMessages(product: Product): ILintMessage[] { - let messages: ILintMessage[]; - switch (product) { - case Product.pylint: { - messages = pylintMessagesToBeReturned; - break; - } - case Product.flake8: { - messages = flake8MessagesToBeReturned; - break; - } - case Product.pycodestyle: { - messages = pycodestyleMessagesToBeReturned; - break; - } - case Product.pydocstyle: { - messages = pydocstyleMessagesToBeReturned; - break; - } - default: { - throwUnknownProduct(product); - return []; - } - } - this.setMessages(messages, product); - return messages; - } - - public setMessages(messages: ILintMessage[], product?: Product) { - if (messages.length === 0) { - this.setStdout(''); - return; - } - - if (product && getLinterID(product) === 'pylint') { - this.setStdout(pylintLinterMessagesAsOutput(messages)); - return; - } - const lines: string[] = []; - for (const msg of messages) { - if (msg.provider === '' && product) { - msg.provider = getLinterID(product); - } - const line = linterMessageAsLine(msg); - lines.push(line); - } - this.setStdout(lines.join(os.EOL) + os.EOL); - } - - public setStdout(stdout: string) { - this.pythonToolExecService - .setup((s) => s.execForLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve({ stdout })); - } -} - -suite('Linting Scenarios', () => { - // Note that these aren't actually unit tests. Instead they are - // integration tests with heavy usage of mocks. - - let isExtensionEnabledStub: sinon.SinonStub; - let isExtensionDisabledStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let persistentState: TypeMoq.IMock>; - setup(() => { - isExtensionEnabledStub = sinon.stub(promptApis, 'isExtensionEnabled'); - isExtensionDisabledStub = sinon.stub(promptApis, 'isExtensionDisabled'); - // For these tests we assume that linter extensions are not installed. - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - persistentState = TypeMoq.Mock.ofType>(); - persistentState.setup((p) => p.value).returns(() => true); - doNotShowPromptStateStub = sinon.stub(promptApis, 'doNotShowPromptState'); - doNotShowPromptStateStub.returns(persistentState.object); - }); - - teardown(() => { - sinon.restore(); - }); - - test('No linting with PyLint (enabled) when disabled at top-level', async () => { - const product = Product.pylint; - const fixture = new TestFixture(); - fixture.lintingSettings.enabled = false; - fixture.setDefaultMessages(product); - const linter = await fixture.getEnabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - assert.strictEqual( - messages.length, - 0, - `Unexpected linter errors when linting is disabled, Output - ${fixture.output}`, - ); - }); - - test('No linting with Pylint disabled (and Flake8 enabled)', async () => { - const product = Product.pylint; - const fixture = new TestFixture(); - fixture.lintingSettings.enabled = true; - fixture.lintingSettings.flake8Enabled = true; - fixture.setDefaultMessages(Product.pylint); - const linter = await fixture.getDisabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - assert.strictEqual( - messages.length, - 0, - `Unexpected linter errors when linting is disabled, Output - ${fixture.output}`, - ); - }); - - async function testEnablingDisablingOfLinter(fixture: TestFixture, product: Product, enabled: boolean) { - fixture.lintingSettings.enabled = true; - fixture.setDefaultMessages(product); - if (enabled) { - fixture.setDefaultMessages(product); - } - const linter = await fixture.getLinter(product, enabled); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - if (enabled) { - assert.notStrictEqual( - messages.length, - 0, - `Expected linter errors when linter is enabled, Output - ${fixture.output}`, - ); - } else { - assert.strictEqual( - messages.length, - 0, - `Unexpected linter errors when linter is disabled, Output - ${fixture.output}`, - ); - } - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - for (const enabled of [false, true]) { - test(`${enabled ? 'Enable' : 'Disable'} ${getProductName(product)} and run linter`, async function () { - // TODO: Add coverage for these linters. - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { - this.skip(); - } - - const fixture = new TestFixture(); - await testEnablingDisablingOfLinter(fixture, product, enabled); - }); - } - } - for (const useMinimal of [true, false]) { - for (const enabled of [true, false]) { - test(`PyLint ${enabled ? 'enabled' : 'disabled'} with${ - useMinimal ? '' : 'out' - } minimal checkers`, async () => { - const fixture = new TestFixture(); - await testEnablingDisablingOfLinter(fixture, Product.pylint, enabled); - }); - } - } - - async function testLinterMessages(fixture: TestFixture, product: Product) { - const messagesToBeReceived = fixture.setDefaultMessages(product); - const linter = await fixture.getEnabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - if (messagesToBeReceived.length === 0) { - assert.strictEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } else if (fixture.output.indexOf('ENOENT') === -1) { - // Pylint for Python Version 2.7 could return 80 linter messages, where as in 3.5 it might only return 1. - // Looks like pylint stops linting as soon as it comes across any ERRORS. - assert.notStrictEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - test(`Check ${getProductName(product)} messages`, async function () { - // TODO: Add coverage for these linters. - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { - this.skip(); - } - - const fixture = new TestFixture(); - await testLinterMessages(fixture, product); - }); - } - - async function testLinterMessageCount(fixture: TestFixture, product: Product, messageCountToBeReceived: number) { - fixture.setDefaultMessages(product); - const linter = await fixture.getEnabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - new CancellationTokenSource().token, - ); - - assert.strictEqual( - messages.length, - messageCountToBeReceived, - `Expected number of lint errors does not match lint error count, Output - ${fixture.output}`, - ); - } - test('Three line output counted as one message (Pylint)', async () => { - const maxErrors = 5; - const fixture = new TestFixture(); - fixture.lintingSettings.maxNumberOfProblems = maxErrors; - - await testLinterMessageCount(fixture, Product.pylint, maxErrors); - }); -}); - -suite('Linting Products', () => { - const prodService = new ProductService(); - - test('All linting products are represented by linters', async () => { - const products = Object.keys(Product) - .filter((item) => Number.isNaN(Number(item))) - .map((key) => Product[Number(key)]); - - products.forEach((p) => { - const product = (p as unknown) as Product; - if (prodService.getProductType(product) === ProductType.Linter) { - const found = LINTERID_BY_PRODUCT.get(product); - assert.notStrictEqual(found, undefined, `did find linter ${Product[product]}`); - } - }); - }); - - test('All linters match linting products', async () => { - for (const product of LINTERID_BY_PRODUCT.keys()) { - const prodType = prodService.getProductType(product); - assert.notStrictEqual(prodType, undefined, `${Product[product]} is not not properly registered`); - assert.strictEqual(prodType, ProductType.Linter, `${Product[product]} is not a linter product`); - } - }); - - test('All linting product names match linter IDs', async () => { - for (const [product, linterID] of LINTERID_BY_PRODUCT) { - const prodName = ProductNames.get(product); - assert.strictEqual(prodName, linterID, 'product name does not match linter ID'); - } - }); -}); diff --git a/src/test/linters/lintengine.test.ts b/src/test/linters/lintengine.test.ts deleted file mode 100644 index 1bf77c502af5..000000000000 --- a/src/test/linters/lintengine.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as TypeMoq from 'typemoq'; -import { TextDocument, Uri } from 'vscode'; -import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import { PYTHON_LANGUAGE } from '../../client/common/constants'; -import '../../client/common/extensions'; -import { IFileSystem } from '../../client/common/platform/types'; -import { IConfigurationService, ILintingSettings, ILogOutputChannel, IPythonSettings } from '../../client/common/types'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { IServiceContainer } from '../../client/ioc/types'; -import { LintingEngine } from '../../client/linters/lintingEngine'; -import { ILinterManager, ILintingEngine } from '../../client/linters/types'; -import { PythonEnvironment } from '../../client/pythonEnvironments/info'; -import { initialize } from '../initialize'; - -suite('Linting - LintingEngine', () => { - let serviceContainer: TypeMoq.IMock; - let lintManager: TypeMoq.IMock; - let settings: TypeMoq.IMock; - let lintSettings: TypeMoq.IMock; - let fileSystem: TypeMoq.IMock; - let lintingEngine: ILintingEngine; - - suiteSetup(initialize); - setup(async () => { - serviceContainer = TypeMoq.Mock.ofType(); - - const docManager = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager), TypeMoq.It.isAny())) - .returns(() => docManager.object); - - const workspaceService = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) - .returns(() => workspaceService.object); - - fileSystem = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) - .returns(() => fileSystem.object); - - lintSettings = TypeMoq.Mock.ofType(); - settings = TypeMoq.Mock.ofType(); - - const configService = TypeMoq.Mock.ofType(); - configService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - configService.setup((x) => x.isTestExecution()).returns(() => true); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) - .returns(() => configService.object); - - const outputChannel = TypeMoq.Mock.ofType(); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ILogOutputChannel))).returns(() => outputChannel.object); - - lintManager = TypeMoq.Mock.ofType(); - lintManager.setup((x) => x.isLintingEnabled(TypeMoq.It.isAny())).returns(async () => true); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) - .returns(() => lintManager.object); - - lintingEngine = new LintingEngine(serviceContainer.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(ILintingEngine), TypeMoq.It.isAny())) - .returns(() => lintingEngine); - - const interpreterService = TypeMoq.Mock.ofType(); - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); - serviceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); - }); - - test('Ensure document.uri is passed into isLintingEnabled', () => { - const doc = mockTextDocument('a.py', PYTHON_LANGUAGE, true); - try { - lintingEngine.lintDocument(doc, 'auto').ignoreErrors(); - } catch { - lintManager.verify((l) => l.isLintingEnabled(TypeMoq.It.isValue(doc.uri)), TypeMoq.Times.once()); - } - }); - test('Ensure document.uri is passed into createLinter', () => { - const doc = mockTextDocument('a.py', PYTHON_LANGUAGE, true); - try { - lintingEngine.lintDocument(doc, 'auto').ignoreErrors(); - } catch { - lintManager.verify( - (l) => - l.createLinter( - TypeMoq.It.isAny(), - - TypeMoq.It.isAny(), - TypeMoq.It.isValue(doc.uri), - ), - TypeMoq.Times.atLeastOnce(), - ); - } - }); - - test('Verify files that match ignore pattern are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, true, ['a*.py']); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - - test('Ensure non-Python files are not linted', async () => { - const doc = mockTextDocument('a.ts', 'typescript', true); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - - test('Ensure files with git scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'git'); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - test('Ensure files with showModifications scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'showModifications'); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - test('Ensure files with svn scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'svn'); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - - test('Ensure non-existing files are not linted', async () => { - const doc = mockTextDocument('file.py', PYTHON_LANGUAGE, false, []); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify( - (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), - TypeMoq.Times.never(), - ); - }); - - function mockTextDocument( - fileName: string, - language: string, - exists: boolean, - ignorePattern: string[] = [], - scheme?: string, - ): TextDocument { - fileSystem.setup((x) => x.fileExists(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(exists)); - - lintSettings.setup((l) => l.ignorePatterns).returns(() => ignorePattern); - settings.setup((x) => x.linting).returns(() => lintSettings.object); - - const doc = TypeMoq.Mock.ofType(); - if (scheme) { - doc.setup((d) => d.uri).returns(() => Uri.parse(`${scheme}:${fileName}`)); - } else { - doc.setup((d) => d.uri).returns(() => Uri.file(fileName)); - } - doc.setup((d) => d.fileName).returns(() => fileName); - doc.setup((d) => d.languageId).returns(() => language); - return doc.object; - } -}); diff --git a/src/test/linters/linterManager.unit.test.ts b/src/test/linters/linterManager.unit.test.ts deleted file mode 100644 index 42feb642ce8c..000000000000 --- a/src/test/linters/linterManager.unit.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { expect } from 'chai'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { CommandManager } from '../../client/common/application/commandManager'; -import { DocumentManager } from '../../client/common/application/documentManager'; -import { - IApplicationShell, - ICommandManager, - IDocumentManager, - IWorkspaceService, -} from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { ProductNames } from '../../client/common/installer/productNames'; -import { ProductService } from '../../client/common/installer/productService'; -import { IConfigurationService, Product, ProductType } from '../../client/common/types'; -import { getNamesAndValues } from '../../client/common/utils/enum'; -import { ServiceContainer } from '../../client/ioc/container'; -import { LinterInfo } from '../../client/linters/linterInfo'; -import { LinterManager } from '../../client/linters/linterManager'; -import { LintingEngine } from '../../client/linters/lintingEngine'; -import { ILinterInfo, ILintingEngine } from '../../client/linters/types'; - -suite('Linting - Linter Manager', () => { - let linterManager: LinterManagerTest; - let shell: IApplicationShell; - let docManager: IDocumentManager; - let cmdManager: ICommandManager; - let lintingEngine: ILintingEngine; - let configService: IConfigurationService; - let workspaceService: IWorkspaceService; - class LinterManagerTest extends LinterManager { - // Override base class property to make it public. - public linters!: ILinterInfo[]; - } - setup(() => { - const svcContainer = mock(ServiceContainer); - shell = mock(ApplicationShell); - docManager = mock(DocumentManager); - cmdManager = mock(CommandManager); - lintingEngine = mock(LintingEngine); - configService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - when(svcContainer.get(IApplicationShell)).thenReturn(instance(shell)); - when(svcContainer.get(IDocumentManager)).thenReturn(instance(docManager)); - when(svcContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - when(svcContainer.get(ILintingEngine)).thenReturn(instance(lintingEngine)); - when(svcContainer.get(IConfigurationService)).thenReturn(instance(configService)); - when(svcContainer.get(IWorkspaceService)).thenReturn(instance(workspaceService)); - linterManager = new LinterManagerTest(instance(configService)); - }); - - test('Get all linters will return a list of all linters', () => { - const linters = linterManager.getAllLinterInfos(); - - expect(linters).to.be.lengthOf(8); - - const productService = new ProductService(); - const linterProducts = getNamesAndValues(Product) - .filter((product) => productService.getProductType(product.value) === ProductType.Linter) - .map((item) => ProductNames.get(item.value)); - expect(linters.map((item) => item.id).sort()).to.be.deep.equal(linterProducts.sort()); - }); - - test('Get linter info for non-linter product should throw an exception', () => { - const productService = new ProductService(); - getNamesAndValues(Product).forEach((prod) => { - if (productService.getProductType(prod.value) === ProductType.Linter) { - const info = linterManager.getLinterInfo(prod.value); - expect(info.id).to.equal(ProductNames.get(prod.value)); - expect(info).not.to.be.equal(undefined, 'should not be unedfined'); - } else { - expect(() => linterManager.getLinterInfo(prod.value)).to.throw(); - } - }); - }); - test('Pylint configuration file watch', async () => { - const pylint = linterManager.getLinterInfo(Product.pylint); - assert.strictEqual(pylint.configFileNames.length, 2, 'Pylint configuration file count is incorrect.'); - assert.notStrictEqual( - pylint.configFileNames.indexOf('pylintrc'), - -1, - 'Pylint configuration files miss pylintrc.', - ); - assert.notStrictEqual( - pylint.configFileNames.indexOf('.pylintrc'), - -1, - 'Pylint configuration files miss .pylintrc.', - ); - }); - - [undefined, Uri.parse('something')].forEach((resource) => { - const testResourceSuffix = `(${resource ? 'with a resource' : 'without a resource'})`; - [true, false].forEach((enabled) => { - const testSuffix = `(${enabled ? 'enable' : 'disable'}) & ${testResourceSuffix}`; - test(`Enable linting should update config ${testSuffix}`, async () => { - when(configService.updateSetting('linting.enabled', enabled, resource)).thenResolve(); - - await linterManager.enableLintingAsync(enabled, resource); - - verify(configService.updateSetting('linting.enabled', enabled, resource)).once(); - }); - }); - test(`getActiveLinters will check if linter is enabled and in silent mode ${testResourceSuffix}`, async () => { - const linterInfo = mock(LinterInfo); - const instanceOfLinterInfo = instance(linterInfo); - linterManager.linters = [instanceOfLinterInfo]; - when(linterInfo.isEnabled(resource)).thenReturn(true); - - const linters = await linterManager.getActiveLinters(resource); - - verify(linterInfo.isEnabled(resource)).once(); - expect(linters[0]).to.deep.equal(instanceOfLinterInfo); - }); - - test(`setActiveLintersAsync with invalid products does nothing ${testResourceSuffix}`, async () => { - let getActiveLintersInvoked = false; - linterManager.getActiveLinters = async () => { - getActiveLintersInvoked = true; - return []; - }; - - await linterManager.setActiveLintersAsync([Product.pytest], resource); - - expect(getActiveLintersInvoked).to.be.equal(false, 'Should not be invoked'); - }); - test(`setActiveLintersAsync with single product will disable it then enable it ${testResourceSuffix}`, async () => { - const linterInfo = mock(LinterInfo); - const instanceOfLinterInfo = instance(linterInfo); - linterManager.linters = [instanceOfLinterInfo]; - when(linterInfo.product).thenReturn(Product.flake8); - when(linterInfo.enableAsync(false, resource)).thenResolve(); - linterManager.getActiveLinters = () => Promise.resolve([instanceOfLinterInfo]); - linterManager.enableLintingAsync = () => Promise.resolve(); - - await linterManager.setActiveLintersAsync([Product.flake8], resource); - - verify(linterInfo.enableAsync(false, resource)).atLeast(1); - verify(linterInfo.enableAsync(true, resource)).atLeast(1); - }); - test(`setActiveLintersAsync with single product will disable all existing then enable the necessary two ${testResourceSuffix}`, async () => { - const linters = new Map(); - const linterInstances = new Map(); - linterManager.linters = []; - [Product.flake8, Product.mypy, Product.prospector, Product.bandit, Product.pydocstyle].forEach( - (product) => { - const linterInfo = mock(LinterInfo); - const instanceOfLinterInfo = instance(linterInfo); - linterManager.linters.push(instanceOfLinterInfo); - linters.set(product, linterInfo); - linterInstances.set(product, instanceOfLinterInfo); - when(linterInfo.product).thenReturn(product); - when(linterInfo.enableAsync(anything(), resource)).thenResolve(); - }, - ); - - linterManager.getActiveLinters = () => Promise.resolve(Array.from(linterInstances.values())); - linterManager.enableLintingAsync = () => Promise.resolve(); - - const lintersToEnable = [Product.flake8, Product.mypy, Product.pydocstyle]; - await linterManager.setActiveLintersAsync([Product.flake8, Product.mypy, Product.pydocstyle], resource); - - linters.forEach((item, product) => { - verify(item.enableAsync(false, resource)).atLeast(1); - if (lintersToEnable.indexOf(product) >= 0) { - verify(item.enableAsync(true, resource)).atLeast(1); - } - }); - }); - }); -}); diff --git a/src/test/linters/mypy.unit.test.ts b/src/test/linters/mypy.unit.test.ts deleted file mode 100644 index b697a719a475..000000000000 --- a/src/test/linters/mypy.unit.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { parseLine } from '../../client/linters/baseLinter'; -import { getRegex } from '../../client/linters/mypy'; -import { ILintMessage, LinterId } from '../../client/linters/types'; - -// This following is a real-world example. See gh=2380. - -const output = ` -provider.pyi:10: error: Incompatible types in assignment (expression has type "str", variable has type "int") -provider.pyi:11: error: Name 'not_declared_var' is not defined -provider.pyi:12:21: error: Expression has type "Any" -`; - -suite('Linting - MyPy', () => { - test('regex', async () => { - const lines = output.split('\n'); - const tests: [string, ILintMessage][] = [ - [ - lines[1], - { - code: undefined, - message: 'Incompatible types in assignment (expression has type "str", variable has type "int")', - column: 0, - line: 10, - type: 'error', - provider: 'mypy', - } as ILintMessage, - ], - [ - lines[2], - { - code: undefined, - message: "Name 'not_declared_var' is not defined", - column: 0, - line: 11, - type: 'error', - provider: 'mypy', - } as ILintMessage, - ], - [ - lines[3], - { - code: undefined, - message: 'Expression has type "Any"', - column: 20, - line: 12, - type: 'error', - provider: 'mypy', - } as ILintMessage, - ], - ]; - for (const [line, expected] of tests) { - const msg = parseLine(line, getRegex('provider.pyi'), LinterId.MyPy, 1); - - expect(msg).to.deep.equal(expected); - } - }); - test('regex excludes unexpected files', () => { - // mypy run against `foo/bar.py` returning errors for foo/__init__.py - const outputWithUnexpectedFile = `\ -foo/__init__.py:4:5: error: Statement is unreachable [unreachable] -foo/bar.py:2:14: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] -Found 2 errors in 2 files (checked 1 source file) -`; - - const lines = outputWithUnexpectedFile.split('\n'); - const tests: [string, ILintMessage | undefined][] = [ - [lines[0], undefined], - [ - lines[1], - { - code: undefined, - message: - 'Incompatible types in assignment (expression has type "str", variable has type "int") [assignment]', - column: 13, - line: 2, - type: 'error', - provider: 'mypy', - }, - ], - [lines[2], undefined], - ]; - for (const [line, expected] of tests) { - const msg = parseLine(line, getRegex('foo/bar.py'), LinterId.MyPy, 1); - - expect(msg).to.deep.equal(expected); - } - }); - test('getRegex escapes filename correctly', () => { - expect(getRegex('foo/bar.py')).to.eql( - String.raw`foo/bar\.py:(?\d+)(:(?\d+))?: (?\w+): (?.*)\r?(\n|$)`, - ); - }); -}); diff --git a/src/test/linters/prompts/flake8Prompt.unit.test.ts b/src/test/linters/prompts/flake8Prompt.unit.test.ts deleted file mode 100644 index 7bbe52ae6d96..000000000000 --- a/src/test/linters/prompts/flake8Prompt.unit.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { IApplicationEnvironment } from '../../../client/common/application/types'; -import { IPersistentState } from '../../../client/common/types'; -import { Common, ToolsExtensions } from '../../../client/common/utils/localize'; -import * as commandApis from '../../../client/common/vscodeApis/commandApis'; -import * as windowsApis from '../../../client/common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../../client/ioc/types'; -import * as promptCommons from '../../../client/linters/prompts/common'; -import { Flake8ExtensionPrompt, FLAKE8_EXTENSION } from '../../../client/linters/prompts/flake8Prompt'; -import { IToolsExtensionPrompt } from '../../../client/linters/prompts/types'; - -suite('Flake8 Extension prompt tests', () => { - let isExtensionEnabledStub: sinon.SinonStub; - let isExtensionDisabledStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let inToolsExtensionsExperimentStub: sinon.SinonStub; - let showInformationMessageStub: sinon.SinonStub; - let executeCommandStub: sinon.SinonStub; - let serviceContainer: TypeMoq.IMock; - let doNotState: TypeMoq.IMock>; - let appEnv: TypeMoq.IMock; - let prompt: IToolsExtensionPrompt; - - setup(() => { - isExtensionEnabledStub = sinon.stub(promptCommons, 'isExtensionEnabled'); - isExtensionDisabledStub = sinon.stub(promptCommons, 'isExtensionDisabled'); - doNotShowPromptStateStub = sinon.stub(promptCommons, 'doNotShowPromptState'); - inToolsExtensionsExperimentStub = sinon.stub(promptCommons, 'inToolsExtensionsExperiment'); - showInformationMessageStub = sinon.stub(windowsApis, 'showInformationMessage'); - executeCommandStub = sinon.stub(commandApis, 'executeCommand'); - - appEnv = TypeMoq.Mock.ofType(); - serviceContainer = TypeMoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(IApplicationEnvironment)) - .returns(() => appEnv.object); - - doNotState = TypeMoq.Mock.ofType>(); - prompt = new Flake8ExtensionPrompt(serviceContainer.object); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Extension already installed and enabled', async () => { - isExtensionEnabledStub.returns(true); - - assert.isTrue(await prompt.showPrompt()); - }); - - test('Extension already installed, but disabled', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(true); - - assert.isTrue(await prompt.showPrompt()); - }); - - test('Test do not show again persistent state', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => true); - doNotShowPromptStateStub.returns(doNotState.object); - - assert.isFalse(await prompt.showPrompt()); - }); - - test('User not in experiment', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(false); - - assert.isFalse(await prompt.showPrompt()); - }); - - test('User selected: install extension (insiders)', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - appEnv.setup((a) => a.extensionChannel).returns(() => 'insiders'); - executeCommandStub.resolves(undefined); - - showInformationMessageStub.resolves(ToolsExtensions.installFlake8Extension); - assert.isTrue(await prompt.showPrompt()); - - executeCommandStub.calledOnceWith('workbench.extensions.installExtension', FLAKE8_EXTENSION, { - installPreReleaseVersion: true, - }); - }); - - test('User selected: install extension (stable)', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - appEnv.setup((a) => a.extensionChannel).returns(() => 'stable'); - executeCommandStub.resolves(undefined); - - showInformationMessageStub.resolves(ToolsExtensions.installFlake8Extension); - assert.isTrue(await prompt.showPrompt()); - - executeCommandStub.calledOnceWith('workbench.extensions.installExtension', FLAKE8_EXTENSION, { - installPreReleaseVersion: false, - }); - }); - - test('User selected: do not show again', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - doNotState - .setup((d) => d.updateValue(true)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - showInformationMessageStub.resolves(Common.doNotShowAgain); - assert.isFalse(await prompt.showPrompt()); - - doNotState.verifyAll(); - }); - - test('User selected: close', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - showInformationMessageStub.resolves(undefined); - assert.isFalse(await prompt.showPrompt()); - }); -}); diff --git a/src/test/linters/prompts/pylintPrompt.unit.test.ts b/src/test/linters/prompts/pylintPrompt.unit.test.ts deleted file mode 100644 index 65b579f258af..000000000000 --- a/src/test/linters/prompts/pylintPrompt.unit.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import * as TypeMoq from 'typemoq'; -import { IApplicationEnvironment } from '../../../client/common/application/types'; -import { IPersistentState } from '../../../client/common/types'; -import { Common, ToolsExtensions } from '../../../client/common/utils/localize'; -import * as commandApis from '../../../client/common/vscodeApis/commandApis'; -import * as windowsApis from '../../../client/common/vscodeApis/windowApis'; -import { IServiceContainer } from '../../../client/ioc/types'; -import * as promptCommons from '../../../client/linters/prompts/common'; -import { PylintExtensionPrompt, PYLINT_EXTENSION } from '../../../client/linters/prompts/pylintPrompt'; -import { IToolsExtensionPrompt } from '../../../client/linters/prompts/types'; - -suite('Pylint Extension prompt tests', () => { - let isExtensionEnabledStub: sinon.SinonStub; - let isExtensionDisabledStub: sinon.SinonStub; - let doNotShowPromptStateStub: sinon.SinonStub; - let inToolsExtensionsExperimentStub: sinon.SinonStub; - let showInformationMessageStub: sinon.SinonStub; - let executeCommandStub: sinon.SinonStub; - let serviceContainer: TypeMoq.IMock; - let doNotState: TypeMoq.IMock>; - let appEnv: TypeMoq.IMock; - let prompt: IToolsExtensionPrompt; - - setup(() => { - isExtensionEnabledStub = sinon.stub(promptCommons, 'isExtensionEnabled'); - isExtensionDisabledStub = sinon.stub(promptCommons, 'isExtensionDisabled'); - doNotShowPromptStateStub = sinon.stub(promptCommons, 'doNotShowPromptState'); - inToolsExtensionsExperimentStub = sinon.stub(promptCommons, 'inToolsExtensionsExperiment'); - showInformationMessageStub = sinon.stub(windowsApis, 'showInformationMessage'); - executeCommandStub = sinon.stub(commandApis, 'executeCommand'); - - appEnv = TypeMoq.Mock.ofType(); - serviceContainer = TypeMoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(IApplicationEnvironment)) - .returns(() => appEnv.object); - - doNotState = TypeMoq.Mock.ofType>(); - prompt = new PylintExtensionPrompt(serviceContainer.object); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Extension already installed and enabled', async () => { - isExtensionEnabledStub.returns(true); - - assert.isTrue(await prompt.showPrompt()); - }); - - test('Extension already installed, but disabled', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(true); - - assert.isTrue(await prompt.showPrompt()); - }); - - test('User not in experiment', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(false); - - assert.isFalse(await prompt.showPrompt()); - }); - - test('User selected: install extension (insiders)', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - appEnv.setup((a) => a.extensionChannel).returns(() => 'insiders'); - executeCommandStub.resolves(undefined); - - showInformationMessageStub.resolves(ToolsExtensions.installPylintExtension); - assert.isTrue(await prompt.showPrompt()); - - executeCommandStub.calledOnceWith('workbench.extensions.installExtension', PYLINT_EXTENSION, { - installPreReleaseVersion: true, - }); - }); - - test('User selected: install extension (stable)', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - appEnv.setup((a) => a.extensionChannel).returns(() => 'stable'); - executeCommandStub.resolves(undefined); - - showInformationMessageStub.resolves(ToolsExtensions.installPylintExtension); - assert.isTrue(await prompt.showPrompt()); - - executeCommandStub.calledOnceWith('workbench.extensions.installExtension', PYLINT_EXTENSION, { - installPreReleaseVersion: false, - }); - }); - - test('User selected: do not show again', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - doNotState - .setup((d) => d.updateValue(true)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - showInformationMessageStub.resolves(Common.doNotShowAgain); - assert.isFalse(await prompt.showPrompt()); - - doNotState.verifyAll(); - }); - - test('User selected: close', async () => { - isExtensionEnabledStub.returns(false); - isExtensionDisabledStub.returns(false); - - doNotState.setup((d) => d.value).returns(() => false); - doNotShowPromptStateStub.returns(doNotState.object); - inToolsExtensionsExperimentStub.resolves(true); - - showInformationMessageStub.resolves(undefined); - assert.isFalse(await prompt.showPrompt()); - }); -}); diff --git a/src/test/linters/pylint.test.ts b/src/test/linters/pylint.test.ts deleted file mode 100644 index e1cec249c662..000000000000 --- a/src/test/linters/pylint.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import { Container } from 'inversify'; -import * as os from 'os'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { - CancellationTokenSource, - DiagnosticSeverity, - TextDocument, - Uri, - WorkspaceConfiguration, - WorkspaceFolder, -} from 'vscode'; -import { LanguageServerType } from '../../client/activation/types'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { IPythonToolExecutionService } from '../../client/common/process/types'; -import { IConfigurationService, IExtensions, IInstaller, IPythonSettings } from '../../client/common/types'; -import { - IInterpreterAutoSelectionService, - IInterpreterAutoSelectionProxyService, -} from '../../client/interpreter/autoSelection/types'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { LinterManager } from '../../client/linters/linterManager'; -import { Pylint } from '../../client/linters/pylint'; -import { ILinterManager } from '../../client/linters/types'; -import { MockLintingSettings } from '../mockClasses'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; - -suite('Linting - Pylint', () => { - let fileSystem: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - let workspace: TypeMoq.IMock; - let execService: TypeMoq.IMock; - let config: TypeMoq.IMock; - let workspaceConfig: TypeMoq.IMock; - let pythonSettings: TypeMoq.IMock; - let serviceContainer: ServiceContainer; - let extensionsService: TypeMoq.IMock; - - setup(() => { - fileSystem = TypeMoq.Mock.ofType(); - fileSystem - .setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) - .returns((a, b) => a === b); - - platformService = TypeMoq.Mock.ofType(); - platformService.setup((x) => x.isWindows).returns(() => false); - - extensionsService = TypeMoq.Mock.ofType(); - extensionsService.setup((e) => e.getExtension(TypeMoq.It.isAny())).returns(() => undefined); - - workspace = TypeMoq.Mock.ofType(); - execService = TypeMoq.Mock.ofType(); - - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - serviceContainer = new ServiceContainer(cont); - - serviceManager.addSingletonInstance(IFileSystem, fileSystem.object); - serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); - serviceManager.addSingletonInstance( - IPythonToolExecutionService, - execService.object, - ); - serviceManager.addSingletonInstance(IPlatformService, platformService.object); - serviceManager.addSingleton( - IInterpreterAutoSelectionService, - MockAutoSelectionService, - ); - serviceManager.addSingleton( - IInterpreterAutoSelectionProxyService, - MockAutoSelectionService, - ); - serviceManager.addSingletonInstance(IExtensions, extensionsService.object); - - pythonSettings = TypeMoq.Mock.ofType(); - pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Jedi); - - config = TypeMoq.Mock.ofType(); - config.setup((c) => c.getSettings()).returns(() => pythonSettings.object); - - workspaceConfig = TypeMoq.Mock.ofType(); - workspace.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); - - serviceManager.addSingletonInstance(IConfigurationService, config.object); - const linterManager = new LinterManager(config.object); - serviceManager.addSingletonInstance(ILinterManager, linterManager); - const installer = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IInstaller, installer.object); - }); - - test('Negative column numbers should be treated 0', async () => { - const fileFolder = '/user/a/b/c'; - const pylinter = new Pylint(serviceContainer, { showPrompt: () => Promise.resolve(false) }); - - const document = TypeMoq.Mock.ofType(); - document.setup((x) => x.uri).returns(() => Uri.file(path.join(fileFolder, 'test.py'))); - - const wsf = TypeMoq.Mock.ofType(); - wsf.setup((x) => x.uri).returns(() => Uri.file(fileFolder)); - - workspace.setup((x) => x.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => wsf.object); - - const linterOutput = [ - '[', - ' {', - ' "type": "convention",', - ' "module": "test",', - ' "obj": "",', - ' "line": 1,', - ' "column": 1,', - ` "path": "${fileFolder}/test.py",`, - ' "symbol": "missing-module-docstring",', - ' "message": "Missing module docstring",', - ' "message-id": "C0114",', - ' "endLine": null,', - ' "endColumn": null', - ' },', - ' {', - ' "type": "error",', - ' "module": "test",', - ' "obj": "",', - ' "line": 3,', - ' "column": -1,', - ` "path": "${fileFolder}/test.py",`, - ' "symbol": "too-many-format-args",', - ' "message": "Too many arguments for format string",', - ' "message-id": "E1305"', - ' }', - ']', - ].join(os.EOL); - execService - .setup((x) => x.execForLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: linterOutput, stderr: '' })); - - const lintSettings = new MockLintingSettings(); - lintSettings.maxNumberOfProblems = 1000; - lintSettings.pylintPath = 'pyLint'; - lintSettings.pylintEnabled = true; - lintSettings.pylintCategorySeverity = { - convention: DiagnosticSeverity.Hint, - error: DiagnosticSeverity.Error, - fatal: DiagnosticSeverity.Error, - refactor: DiagnosticSeverity.Hint, - warning: DiagnosticSeverity.Warning, - }; - - const settings = TypeMoq.Mock.ofType(); - settings.setup((x) => x.linting).returns(() => lintSettings); - settings.setup((x) => x.languageServer).returns(() => LanguageServerType.Jedi); - config.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - - const messages = await pylinter.lint(document.object, new CancellationTokenSource().token); - expect(messages).to.be.lengthOf(2); - expect(messages[0].column).to.be.equal(1); - expect(messages[1].column).to.be.equal(0); - }); -}); diff --git a/src/test/linters/pylint.unit.test.ts b/src/test/linters/pylint.unit.test.ts deleted file mode 100644 index ee6954e870a5..000000000000 --- a/src/test/linters/pylint.unit.test.ts +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import { mock } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import * as vscode from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { IConfigurationService, IExtensions, IPythonSettings } from '../../client/common/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { IToolsExtensionPrompt } from '../../client/linters/prompts/types'; -import { Pylint } from '../../client/linters/pylint'; -import { ILinterInfo, ILinterManager, ILintMessage, LinterId, LintMessageSeverity } from '../../client/linters/types'; - -suite('Pylint - Function runLinter()', () => { - let fileSystem: TypeMoq.IMock; - let serviceContainer: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let manager: TypeMoq.IMock; - let _info: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - let extensionsService: TypeMoq.IMock; - let run: sinon.SinonStub; - let parseMessagesSeverity: sinon.SinonStub; - let extensionPrompt: TypeMoq.IMock; - const doc = { - uri: vscode.Uri.file('path/to/doc'), - }; - const args = [doc.uri.fsPath]; - class PylintTest extends Pylint { - // eslint-disable-next-line class-methods-use-this - public async run( - _args: string[], - _document: vscode.TextDocument, - _cancellation: vscode.CancellationToken, - _regEx: string, - ): Promise { - return []; - } - - // eslint-disable-next-line class-methods-use-this - public parseMessagesSeverity(_error: string, _categorySeverity: unknown): LintMessageSeverity { - return ('Severity' as unknown) as LintMessageSeverity; - } - - // eslint-disable-next-line class-methods-use-this - public get info(): ILinterInfo { - return _info.object; - } - - public async runLinter( - document: vscode.TextDocument, - cancellation: vscode.CancellationToken, - ): Promise { - return super.runLinter(document, cancellation); - } - - // eslint-disable-next-line class-methods-use-this - public getWorkingDirectoryPath(_document: vscode.TextDocument): string { - return 'path/to/workspaceRoot'; - } - - public async parseMessages( - output: string, - _document: vscode.TextDocument, - _token: vscode.CancellationToken, - ): Promise { - return super.parseMessages(output, _document, _token, ''); - } - } - - setup(() => { - platformService = TypeMoq.Mock.ofType(); - _info = TypeMoq.Mock.ofType(); - serviceContainer = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); - configService = TypeMoq.Mock.ofType(); - extensionsService = TypeMoq.Mock.ofType(); - manager = TypeMoq.Mock.ofType(); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ILinterManager))).returns(() => manager.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) - .returns(() => configService.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) - .returns(() => platformService.object); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IExtensions))).returns(() => extensionsService.object); - fileSystem = TypeMoq.Mock.ofType(); - fileSystem - .setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) - .returns((a, b) => a === b); - manager.setup((m) => m.getLinterInfo(TypeMoq.It.isAny())).returns(() => (undefined as unknown) as ILinterInfo); - _info.setup((x) => x.id).returns(() => LinterId.PyLint); - extensionPrompt = TypeMoq.Mock.ofType(); - extensionPrompt.setup((e) => e.showPrompt()).returns(() => Promise.resolve(false)); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Test pylint with default settings.', async () => { - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - _info.setup((info) => info.linterArgs(doc.uri)).returns(() => []); - run = sinon.stub(PylintTest.prototype, 'run'); - run.callsFake(() => Promise.resolve([])); - parseMessagesSeverity = sinon.stub(PylintTest.prototype, 'parseMessagesSeverity'); - parseMessagesSeverity.callsFake(() => 'Severity'); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - await pylint.runLinter(doc as vscode.TextDocument, mock(vscode.CancellationTokenSource).token); - assert.deepEqual(run.args[0][0], args); - assert.ok(parseMessagesSeverity.notCalled); - assert.ok(run.calledOnce); - }); - - test('Message returned by runLinter() is as expected', async () => { - const message = [ - { - type: 'messageType', - }, - ]; - const expectedResult = [ - { - type: 'messageType', - severity: 'LintMessageSeverity', - }, - ]; - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - _info.setup((info) => info.linterArgs(doc.uri)).returns(() => []); - run = sinon.stub(PylintTest.prototype, 'run'); - run.callsFake(() => Promise.resolve(message)); - parseMessagesSeverity = sinon.stub(PylintTest.prototype, 'parseMessagesSeverity'); - parseMessagesSeverity.callsFake(() => 'LintMessageSeverity'); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - const result = await pylint.runLinter(doc as vscode.TextDocument, mock(vscode.CancellationTokenSource).token); - assert.deepEqual(result, (expectedResult as unknown) as ILintMessage[]); - assert.ok(parseMessagesSeverity.calledOnce); - assert.ok(run.calledOnce); - }); - - test('Parse json output', async () => { - // If 'endLine' and 'endColumn' are missing in JSON output, - // both should be set to 'undefined' - const jsonOutput = `[ - { - "type": "error", - "module": "file", - "obj": "Foo.meth3", - "line": 26, - "column": 15, - "path": "file.py", - "symbol": "no-member", - "message": "Instance of 'Foo' has no 'blop' member", - "message-id": "E1101" - } -]`; - const expectedMessages: ILintMessage[] = [ - { - code: 'no-member', - message: "Instance of 'Foo' has no 'blop' member", - column: 15, - line: 26, - type: 'error', - provider: LinterId.PyLint, - endLine: undefined, - endColumn: undefined, - }, - ]; - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - const result = await pylint.parseMessages( - jsonOutput, - doc as vscode.TextDocument, - mock(vscode.CancellationTokenSource).token, - ); - assert.deepEqual(result, expectedMessages); - }); - - test('Parse json output with endLine', async () => { - const jsonOutput = `[ - { - "type": "error", - "module": "file", - "obj": "Foo.meth3", - "line": 26, - "column": 15, - "endLine": 26, - "endColumn": 24, - "path": "file.py", - "symbol": "no-member", - "message": "Instance of 'Foo' has no 'blop' member", - "message-id": "E1101" - } -]`; - const expectedMessages: ILintMessage[] = [ - { - code: 'no-member', - message: "Instance of 'Foo' has no 'blop' member", - column: 15, - line: 26, - type: 'error', - provider: LinterId.PyLint, - endLine: 26, - endColumn: 24, - }, - ]; - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - const result = await pylint.parseMessages( - jsonOutput, - doc as vscode.TextDocument, - mock(vscode.CancellationTokenSource).token, - ); - assert.deepEqual(result, expectedMessages); - }); - - test('Parse json output with unknown endLine', async () => { - // If 'endLine' and 'endColumn' are present in JSON output - // but 'null', 'endLine' should be set to 'undefined'. - // 'endColumn' defaults to 0. - const jsonOutput = `[ - { - "type": "error", - "module": "file", - "obj": "Foo.meth3", - "line": 26, - "column": 15, - "endLine": null, - "endColumn": null, - "path": "file.py", - "symbol": "no-member", - "message": "Instance of 'Foo' has no 'blop' member", - "message-id": "E1101" - } -]`; - const expectedMessages: ILintMessage[] = [ - { - code: 'no-member', - message: "Instance of 'Foo' has no 'blop' member", - column: 15, - line: 26, - type: 'error', - provider: LinterId.PyLint, - endLine: undefined, - endColumn: undefined, - }, - ]; - const settings = { - linting: { - pylintEnabled: true, - }, - }; - configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as IPythonSettings); - const pylint = new PylintTest(serviceContainer.object, extensionPrompt.object); - const result = await pylint.parseMessages( - jsonOutput, - doc as vscode.TextDocument, - mock(vscode.CancellationTokenSource).token, - ); - assert.deepEqual(result, expectedMessages); - }); -}); diff --git a/src/test/linters/serviceRegistry.unit.test.ts b/src/test/linters/serviceRegistry.unit.test.ts deleted file mode 100644 index a27c244af344..000000000000 --- a/src/test/linters/serviceRegistry.unit.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { instance, mock, verify } from 'ts-mockito'; -import { IExtensionActivationService } from '../../client/activation/types'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { IServiceManager } from '../../client/ioc/types'; -import { LinterManager } from '../../client/linters/linterManager'; -import { LintingEngine } from '../../client/linters/lintingEngine'; -import { registerTypes } from '../../client/linters/serviceRegistry'; -import { ILinterManager, ILintingEngine } from '../../client/linters/types'; -import { LinterProvider } from '../../client/providers/linterProvider'; - -suite('Linters Service Registry', () => { - let serviceManager: IServiceManager; - - setup(() => { - serviceManager = mock(ServiceManager); - }); - - test('Ensure services are registered', async () => { - registerTypes(instance(serviceManager)); - verify(serviceManager.addSingleton(ILintingEngine, LintingEngine)).once(); - verify(serviceManager.addSingleton(ILinterManager, LinterManager)).once(); - verify( - serviceManager.addSingleton(IExtensionActivationService, LinterProvider), - ).once(); - }); -}); diff --git a/src/test/mockClasses.ts b/src/test/mockClasses.ts index c962c4d67ca4..e2de7e649b87 100644 --- a/src/test/mockClasses.ts +++ b/src/test/mockClasses.ts @@ -1,12 +1,5 @@ import * as vscode from 'vscode'; import * as util from 'util'; -import { - Flake8CategorySeverity, - ILintingSettings, - IMypyCategorySeverity, - IPycodestyleCategorySeverity, - IPylintCategorySeverity, -} from '../client/common/types'; export class MockOutputChannel implements vscode.LogOutputChannel { public name: string; @@ -79,39 +72,3 @@ export class MockStatusBarItem implements vscode.StatusBarItem { public dispose(): void {} } - -export class MockLintingSettings implements ILintingSettings { - public enabled!: boolean; - public cwd?: string; - public ignorePatterns!: string[]; - public prospectorEnabled!: boolean; - public prospectorArgs!: string[]; - public pylintEnabled!: boolean; - public pylintArgs!: string[]; - public pycodestyleEnabled!: boolean; - public pycodestyleArgs!: string[]; - public pylamaEnabled!: boolean; - public pylamaArgs!: string[]; - public flake8Enabled!: boolean; - public flake8Args!: string[]; - public pydocstyleEnabled!: boolean; - public pydocstyleArgs!: string[]; - public lintOnSave!: boolean; - public maxNumberOfProblems!: number; - public pylintCategorySeverity!: IPylintCategorySeverity; - public pycodestyleCategorySeverity!: IPycodestyleCategorySeverity; - public flake8CategorySeverity!: Flake8CategorySeverity; - public mypyCategorySeverity!: IMypyCategorySeverity; - public prospectorPath!: string; - public pylintPath!: string; - public pycodestylePath!: string; - public pylamaPath!: string; - public flake8Path!: string; - public pydocstylePath!: string; - public mypyEnabled!: boolean; - public mypyArgs!: string[]; - public mypyPath!: string; - public banditEnabled!: boolean; - public banditArgs!: string[]; - public banditPath!: string; -} diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index e7b11d2b745b..a175b3303223 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -45,7 +45,6 @@ import { registerInterpreterTypes } from '../client/interpreter/serviceRegistry' import { ServiceContainer } from '../client/ioc/container'; import { ServiceManager } from '../client/ioc/serviceManager'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; -import { registerTypes as lintersRegisterTypes } from '../client/linters/serviceRegistry'; import { registerTypes as unittestsRegisterTypes } from '../client/testing/serviceRegistry'; import { LegacyFileSystem } from './legacyFileSystem'; import { MockOutputChannel } from './mockClasses'; @@ -142,10 +141,6 @@ export class IocContainer { unittestsRegisterTypes(this.serviceManager); } - public registerLinterTypes(): void { - lintersRegisterTypes(this.serviceManager); - } - public registerPlatformTypes(): void { platformRegisterTypes(this.serviceManager); } From a55484d3c3ccadfc5144c5aa48bdefcb803a1f97 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 19 Oct 2023 17:41:43 -0700 Subject: [PATCH 0264/1136] Fix for stack overflow on dispose (#22263) Closes https://github.com/microsoft/vscode-python/issues/22261 --- src/client/tensorBoard/tensorBoardUsageTracker.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/client/tensorBoard/tensorBoardUsageTracker.ts b/src/client/tensorBoard/tensorBoardUsageTracker.ts index b88e416a113f..d1b21473677f 100644 --- a/src/client/tensorBoard/tensorBoardUsageTracker.ts +++ b/src/client/tensorBoard/tensorBoardUsageTracker.ts @@ -27,9 +27,7 @@ export class TensorBoardUsageTracker implements IExtensionSingleActivationServic @inject(IDisposableRegistry) private disposables: IDisposableRegistry, @inject(TensorBoardPrompt) private prompt: TensorBoardPrompt, @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, - ) { - disposables.push(this); - } + ) {} public dispose(): void { Disposable.from(...this.disposables).dispose(); From 043881397910818cf43b94b85fb3692a4735a14b Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 19 Oct 2023 22:39:22 -0700 Subject: [PATCH 0265/1136] Guide users to install workaround when deactivate command is run (#22223) --- package.json | 3 +- pythonFiles/deactivate | 33 +++ pythonFiles/deactivate.csh | 6 + pythonFiles/deactivate.fish | 36 +++ pythonFiles/deactivate.ps1 | 31 ++ .../common/application/applicationShell.ts | 12 +- .../common/application/progressService.ts | 32 +++ src/client/common/application/types.ts | 18 ++ src/client/common/experiments/helpers.ts | 4 +- src/client/common/platform/fs-paths.ts | 20 ++ src/client/common/utils/localize.ts | 6 + src/client/interpreter/activation/types.ts | 8 - src/client/interpreter/serviceRegistry.ts | 13 +- .../virtualEnvs/activatedEnvLaunch.ts | 5 +- .../deactivatePrompt.ts | 177 ++++++++++++ .../deactivateScripts.ts | 108 +++++++ .../indicatorPrompt.ts} | 12 +- .../envCollectionActivation/service.ts} | 104 +++---- .../shellIntegration.ts | 13 + src/client/terminals/serviceRegistry.ts | 38 ++- src/client/terminals/types.ts | 8 + .../application/progressService.unit.test.ts | 55 ++++ .../platform/fs-temp.functional.test.ts | 17 +- .../terminalActivation.testvirtualenvs.ts | 3 +- ...erminalEnvVarCollectionPrompt.unit.test.ts | 8 +- ...rminalEnvVarCollectionService.unit.test.ts | 6 +- .../deactivatePrompt.unit.test.ts | 271 ++++++++++++++++++ .../terminals/serviceRegistry.unit.test.ts | 16 ++ ...scode.proposed.terminalDataWriteEvent.d.ts | 31 ++ 29 files changed, 979 insertions(+), 115 deletions(-) create mode 100644 pythonFiles/deactivate create mode 100644 pythonFiles/deactivate.csh create mode 100644 pythonFiles/deactivate.fish create mode 100644 pythonFiles/deactivate.ps1 create mode 100644 src/client/common/application/progressService.ts create mode 100644 src/client/terminals/envCollectionActivation/deactivatePrompt.ts create mode 100644 src/client/terminals/envCollectionActivation/deactivateScripts.ts rename src/client/{interpreter/activation/terminalEnvVarCollectionPrompt.ts => terminals/envCollectionActivation/indicatorPrompt.ts} (90%) rename src/client/{interpreter/activation/terminalEnvVarCollectionService.ts => terminals/envCollectionActivation/service.ts} (87%) create mode 100644 src/client/terminals/envCollectionActivation/shellIntegration.ts create mode 100644 src/test/common/application/progressService.unit.test.ts create mode 100644 src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts create mode 100644 typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts diff --git a/package.json b/package.json index 6342e1327205..37a3a4a5d139 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "quickPickSortByLabel", "testObserver", "quickPickItemTooltip", - "saveEditor" + "saveEditor", + "terminalDataWriteEvent" ], "author": { "name": "Microsoft Corporation" diff --git a/pythonFiles/deactivate b/pythonFiles/deactivate new file mode 100644 index 000000000000..6ede3da311a9 --- /dev/null +++ b/pythonFiles/deactivate @@ -0,0 +1,33 @@ +# Same as deactivate in "/bin/activate" +deactivate () { + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + unset -f deactivate + fi +} + +# Initialize the variables required by deactivate function +_OLD_VIRTUAL_PS1="${PS1:-}" +_OLD_VIRTUAL_PATH="$PATH" +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" +fi diff --git a/pythonFiles/deactivate.csh b/pythonFiles/deactivate.csh new file mode 100644 index 000000000000..ef4d0d393897 --- /dev/null +++ b/pythonFiles/deactivate.csh @@ -0,0 +1,6 @@ +# Same as deactivate in "/bin/activate.csh" +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Initialize the variables required by deactivate function +set _OLD_VIRTUAL_PROMPT="$prompt" +set _OLD_VIRTUAL_PATH="$PATH" diff --git a/pythonFiles/deactivate.fish b/pythonFiles/deactivate.fish new file mode 100644 index 000000000000..c652a8c1e3d7 --- /dev/null +++ b/pythonFiles/deactivate.fish @@ -0,0 +1,36 @@ +# Same as deactivate in "/bin/activate.fish" +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$vscode_python_old_fish_prompt_OVERRIDE" + set -e vscode_python_old_fish_prompt_OVERRIDE + if functions -q vscode_python_old_fish_prompt + functions -e fish_prompt + functions -c vscode_python_old_fish_prompt fish_prompt + functions -e vscode_python_old_fish_prompt + end + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + functions -e deactivate + end +end + +# Initialize the variables required by deactivate function +set -gx _OLD_VIRTUAL_PATH $PATH +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + functions -c fish_prompt vscode_python_old_fish_prompt +end +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME +end diff --git a/pythonFiles/deactivate.ps1 b/pythonFiles/deactivate.ps1 new file mode 100644 index 000000000000..65dd80907d90 --- /dev/null +++ b/pythonFiles/deactivate.ps1 @@ -0,0 +1,31 @@ +# Same as deactivate in "Activate.ps1" +function global:deactivate ([switch]$NonDestructive) { + if (Test-Path function:_OLD_VIRTUAL_PROMPT) { + copy-item function:_OLD_VIRTUAL_PROMPT function:prompt + remove-item function:_OLD_VIRTUAL_PROMPT + } + if (Test-Path env:_OLD_VIRTUAL_PYTHONHOME) { + copy-item env:_OLD_VIRTUAL_PYTHONHOME env:PYTHONHOME + remove-item env:_OLD_VIRTUAL_PYTHONHOME + } + if (Test-Path env:_OLD_VIRTUAL_PATH) { + copy-item env:_OLD_VIRTUAL_PATH env:PATH + remove-item env:_OLD_VIRTUAL_PATH + } + if (Test-Path env:VIRTUAL_ENV) { + remove-item env:VIRTUAL_ENV + } + if (!$NonDestructive) { + remove-item function:deactivate + } +} + +# Initialize the variables required by deactivate function +if (! $env:VIRTUAL_ENV_DISABLE_PROMPT) { + function global:_OLD_VIRTUAL_PROMPT {""} + copy-item function:prompt function:_OLD_VIRTUAL_PROMPT +} +if (Test-Path env:PYTHONHOME) { + copy-item env:PYTHONHOME env:_OLD_VIRTUAL_PYTHONHOME +} +copy-item env:PATH env:_OLD_VIRTUAL_PATH diff --git a/src/client/common/application/applicationShell.ts b/src/client/common/application/applicationShell.ts index 454662472010..aadf80186900 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -10,6 +10,7 @@ import { DocumentSelector, env, Event, + EventEmitter, InputBox, InputBoxOptions, languages, @@ -37,7 +38,8 @@ import { WorkspaceFolder, WorkspaceFolderPickOptions, } from 'vscode'; -import { IApplicationShell } from './types'; +import { traceError } from '../../logging'; +import { IApplicationShell, TerminalDataWriteEvent } from './types'; @injectable() export class ApplicationShell implements IApplicationShell { @@ -172,4 +174,12 @@ export class ApplicationShell implements IApplicationShell { public createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem { return languages.createLanguageStatusItem(id, selector); } + public get onDidWriteTerminalData(): Event { + try { + return window.onDidWriteTerminalData; + } catch (ex) { + traceError('Failed to get proposed API onDidWriteTerminalData', ex); + return new EventEmitter().event; + } + } } diff --git a/src/client/common/application/progressService.ts b/src/client/common/application/progressService.ts new file mode 100644 index 000000000000..fb19cad1136c --- /dev/null +++ b/src/client/common/application/progressService.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ProgressOptions } from 'vscode'; +import { Deferred, createDeferred } from '../utils/async'; +import { IApplicationShell } from './types'; + +export class ProgressService { + private deferred: Deferred | undefined; + + constructor(private readonly shell: IApplicationShell) {} + + public showProgress(options: ProgressOptions): void { + if (!this.deferred) { + this.createProgress(options); + } + } + + public hideProgress(): void { + if (this.deferred) { + this.deferred.resolve(); + this.deferred = undefined; + } + } + + private createProgress(options: ProgressOptions) { + this.shell.withProgress(options, () => { + this.deferred = createDeferred(); + return this.deferred.promise; + }); + } +} diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index fa2ced6c45da..863f5e4651b2 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -67,6 +67,17 @@ import { Resource } from '../types'; import { ICommandNameArgumentTypeMapping } from './commands'; import { ExtensionContextKey } from './contextKeys'; +export interface TerminalDataWriteEvent { + /** + * The {@link Terminal} for which the data was written. + */ + readonly terminal: Terminal; + /** + * The data being written. + */ + readonly data: string; +} + export const IApplicationShell = Symbol('IApplicationShell'); export interface IApplicationShell { /** @@ -75,6 +86,13 @@ export interface IApplicationShell { */ readonly onDidChangeWindowState: Event; + /** + * An event which fires when the terminal's child pseudo-device is written to (the shell). + * In other words, this provides access to the raw data stream from the process running + * within the terminal, including VT sequences. + */ + readonly onDidWriteTerminalData: Event; + showInformationMessage(message: string, ...items: string[]): Thenable; /** diff --git a/src/client/common/experiments/helpers.ts b/src/client/common/experiments/helpers.ts index bae96b222eb6..f6ae39d260f5 100644 --- a/src/client/common/experiments/helpers.ts +++ b/src/client/common/experiments/helpers.ts @@ -7,10 +7,12 @@ import { env, workspace } from 'vscode'; import { IExperimentService } from '../types'; import { TerminalEnvVarActivation } from './groups'; import { isTestExecution } from '../constants'; +import { traceInfo } from '../../logging'; export function inTerminalEnvVarExperiment(experimentService: IExperimentService): boolean { - if (!isTestExecution() && workspace.workspaceFile && env.remoteName) { + if (!isTestExecution() && env.remoteName && workspace.workspaceFolders && workspace.workspaceFolders.length > 1) { // TODO: Remove this if statement once https://github.com/microsoft/vscode/issues/180486 is fixed. + traceInfo('Not enabling terminal env var experiment in multiroot remote workspaces'); return false; } if (!experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)) { diff --git a/src/client/common/platform/fs-paths.ts b/src/client/common/platform/fs-paths.ts index 2d46fca98526..17df7507f7d9 100644 --- a/src/client/common/platform/fs-paths.ts +++ b/src/client/common/platform/fs-paths.ts @@ -3,6 +3,7 @@ import * as nodepath from 'path'; import { getSearchPathEnvVarNames } from '../utils/exec'; +import * as fs from 'fs-extra'; import { getOSType, OSType } from '../utils/platform'; import { IExecutables, IFileSystemPaths, IFileSystemPathUtils } from './types'; @@ -170,3 +171,22 @@ export function isParentPath(filePath: string, parentPath: string): boolean { export function arePathsSame(path1: string, path2: string): boolean { return normCasePath(path1) === normCasePath(path2); } + +export async function copyFile(src: string, dest: string): Promise { + const destDir = nodepath.dirname(dest); + if (!(await fs.pathExists(destDir))) { + await fs.mkdirp(destDir); + } + + await fs.copy(src, dest, { + overwrite: true, + }); +} + +export function pathExists(absPath: string): Promise { + return fs.pathExists(absPath); +} + +export function createFile(filename: string): Promise { + return fs.createFile(filename); +} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index b5d1721d14fa..56818afa376d 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -61,6 +61,7 @@ export namespace Common { export const noIWillDoItLater = l10n.t('No, I will do it later'); export const notNow = l10n.t('Not now'); export const doNotShowAgain = l10n.t("Don't show again"); + export const editSomething = l10n.t('Edit {0}'); export const reload = l10n.t('Reload'); export const moreInfo = l10n.t('More Info'); export const learnMore = l10n.t('Learn more'); @@ -198,6 +199,11 @@ export namespace Interpreters { export const terminalEnvVarCollectionPrompt = l10n.t( 'The Python extension automatically activates all terminals using the selected environment, even when the name of the environment{0} is not present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', ); + export const terminalDeactivateProgress = l10n.t('Editing {0}...'); + export const restartingTerminal = l10n.t('Restarting terminal and deactivating...'); + export const terminalDeactivatePrompt = l10n.t( + 'Deactivating virtual environments may not work by default due to a technical limitation in our activation approach, but it can be resolved by appending a line to "{0}". Be sure to restart the shell afterward. [Learn more](https://aka.ms/AAmx2ft).', + ); export const activatedCondaEnvLaunch = l10n.t( 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', ); diff --git a/src/client/interpreter/activation/types.ts b/src/client/interpreter/activation/types.ts index 2b364cbeb862..e00ef9b62b3f 100644 --- a/src/client/interpreter/activation/types.ts +++ b/src/client/interpreter/activation/types.ts @@ -21,11 +21,3 @@ export interface IEnvironmentActivationService { interpreter?: PythonEnvironment, ): Promise; } - -export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); -export interface ITerminalEnvVarCollectionService { - /** - * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. - */ - isTerminalPromptSetCorrectly(resource?: Resource): boolean; -} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 018e7abfdc46..422776bd5e43 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -6,9 +6,7 @@ import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; import { EnvironmentActivationService } from './activation/service'; -import { TerminalEnvVarCollectionPrompt } from './activation/terminalEnvVarCollectionPrompt'; -import { TerminalEnvVarCollectionService } from './activation/terminalEnvVarCollectionService'; -import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './activation/types'; +import { IEnvironmentActivationService } from './activation/types'; import { InterpreterAutoSelectionService } from './autoSelection/index'; import { InterpreterAutoSelectionProxyService } from './autoSelection/proxy'; import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './autoSelection/types'; @@ -110,13 +108,4 @@ export function registerTypes(serviceManager: IServiceManager): void { IEnvironmentActivationService, EnvironmentActivationService, ); - serviceManager.addSingleton( - ITerminalEnvVarCollectionService, - TerminalEnvVarCollectionService, - ); - serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); - serviceManager.addSingleton( - IExtensionSingleActivationService, - TerminalEnvVarCollectionPrompt, - ); } diff --git a/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts b/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts index 468c2dc72a01..b4dcfe36e095 100644 --- a/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts +++ b/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts @@ -91,7 +91,10 @@ export class ActivatedEnvironmentLaunch implements IActivatedEnvironmentLaunch { } if (process.env.VSCODE_CLI !== '1') { // We only want to select the interpreter if VS Code was launched from the command line. - traceVerbose('VS Code was not launched from the command line, not selecting activated interpreter'); + traceVerbose( + 'VS Code was not launched from the command line, not selecting activated interpreter', + JSON.stringify(process.env, undefined, 4), + ); return undefined; } const prefix = await this.getPrefixOfSelectedActivatedEnv(); diff --git a/src/client/terminals/envCollectionActivation/deactivatePrompt.ts b/src/client/terminals/envCollectionActivation/deactivatePrompt.ts new file mode 100644 index 000000000000..43d1e77957bc --- /dev/null +++ b/src/client/terminals/envCollectionActivation/deactivatePrompt.ts @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Position, Uri, WorkspaceEdit, Range, TextEditorRevealType, ProgressLocation, Terminal } from 'vscode'; +import { + IApplicationEnvironment, + IApplicationShell, + IDocumentManager, + ITerminalManager, +} from '../../common/application/types'; +import { IDisposableRegistry, IExperimentService, IPersistentStateFactory } from '../../common/types'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { TerminalShellType } from '../../common/terminal/types'; +import { traceError } from '../../logging'; +import { shellExec } from '../../common/process/rawProcessApis'; +import { sleep } from '../../common/utils/async'; +import { getDeactivateShellInfo } from './deactivateScripts'; +import { isTestExecution } from '../../common/constants'; +import { ProgressService } from '../../common/application/progressService'; +import { copyFile, createFile, pathExists } from '../../common/platform/fs-paths'; +import { getOSType, OSType } from '../../common/utils/platform'; + +export const terminalDeactivationPromptKey = 'TERMINAL_DEACTIVATION_PROMPT_KEY'; +@injectable() +export class TerminalDeactivateLimitationPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + private terminalProcessId: number | undefined; + + private readonly progressService: ProgressService; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, + @inject(IDocumentManager) private readonly documentManager: IDocumentManager, + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + ) { + this.progressService = new ProgressService(this.appShell); + } + + public async activate(): Promise { + if (!inTerminalEnvVarExperiment(this.experimentService)) { + return; + } + if (!isTestExecution()) { + // Avoid showing prompt until startup completes. + await sleep(5000); + } + this.disposableRegistry.push( + this.appShell.onDidWriteTerminalData(async (e) => { + if (!e.data.includes('deactivate')) { + return; + } + let shellType = identifyShellFromShellPath(this.appEnvironment.shell); + if (shellType === TerminalShellType.commandPrompt) { + return; + } + if (getOSType() === OSType.OSX && shellType === TerminalShellType.bash) { + // On macOS, sometimes bash is overriden by OS to actually launch zsh, so we need to execute inside + // the shell to get the correct shell type. + const shell = await shellExec('echo $SHELL', { shell: this.appEnvironment.shell }).then((output) => + output.stdout.trim(), + ); + shellType = identifyShellFromShellPath(shell); + } + const { terminal } = e; + const cwd = + 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd + ? terminal.creationOptions.cwd + : undefined; + const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.type !== PythonEnvType.Virtual) { + return; + } + await this._notifyUsers(shellType, terminal).catch((ex) => traceError('Deactivate prompt failed', ex)); + }), + ); + } + + public async _notifyUsers(shellType: TerminalShellType, terminal: Terminal): Promise { + const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + `${terminalDeactivationPromptKey}-${shellType}`, + true, + ); + if (!notificationPromptEnabled.value) { + const processId = await terminal.processId; + if (processId && this.terminalProcessId === processId) { + // Existing terminal needs to be restarted for changes to take effect. + await this.forceRestartShell(terminal); + } + return; + } + const scriptInfo = getDeactivateShellInfo(shellType); + if (!scriptInfo) { + // Shell integration is not supported for these shells, in which case this workaround won't work. + return; + } + const { initScript, source, destination } = scriptInfo; + const prompts = [Common.editSomething.format(initScript.displayName), Common.doNotShowAgain]; + const selection = await this.appShell.showWarningMessage( + Interpreters.terminalDeactivatePrompt.format(initScript.displayName), + ...prompts, + ); + if (!selection) { + return; + } + if (selection === prompts[0]) { + this.progressService.showProgress({ + location: ProgressLocation.Window, + title: Interpreters.terminalDeactivateProgress.format(initScript.displayName), + }); + await copyFile(source, destination); + await this.openScriptWithEdits(initScript.command, initScript.contents); + await notificationPromptEnabled.updateValue(false); + this.progressService.hideProgress(); + this.terminalProcessId = await terminal.processId; + } + if (selection === prompts[1]) { + await notificationPromptEnabled.updateValue(false); + } + } + + private async openScriptWithEdits(command: string, content: string) { + const document = await this.openScript(command); + const hookMarker = 'VSCode venv deactivate hook'; + content = ` +# >>> ${hookMarker} >>> +${content} +# <<< ${hookMarker} <<<`; + // If script already has the hook, don't add it again. + const editor = await this.documentManager.showTextDocument(document); + if (document.getText().includes(hookMarker)) { + return; + } + const editorEdit = new WorkspaceEdit(); + editorEdit.insert(document.uri, new Position(document.lineCount, 0), content); + await this.documentManager.applyEdit(editorEdit); + // Reveal the edits. + editor.revealRange( + new Range(new Position(document.lineCount - 3, 0), new Position(document.lineCount, 0)), + TextEditorRevealType.AtTop, + ); + } + + private async openScript(command: string) { + const initScriptPath = await this.getPathToScript(command); + if (!(await pathExists(initScriptPath))) { + await createFile(initScriptPath); + } + const document = await this.documentManager.openTextDocument(initScriptPath); + return document; + } + + private async getPathToScript(command: string) { + return shellExec(command, { shell: this.appEnvironment.shell }).then((output) => output.stdout.trim()); + } + + public async forceRestartShell(terminal: Terminal): Promise { + terminal.dispose(); + terminal = this.terminalManager.createTerminal({ + message: Interpreters.restartingTerminal, + }); + terminal.show(true); + terminal.sendText('deactivate'); + } +} diff --git a/src/client/terminals/envCollectionActivation/deactivateScripts.ts b/src/client/terminals/envCollectionActivation/deactivateScripts.ts new file mode 100644 index 000000000000..34917e44bbdf --- /dev/null +++ b/src/client/terminals/envCollectionActivation/deactivateScripts.ts @@ -0,0 +1,108 @@ +/* eslint-disable no-case-declarations */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { _SCRIPTS_DIR } from '../../common/process/internal/scripts/constants'; +import { TerminalShellType } from '../../common/terminal/types'; + +type DeactivateShellInfo = { + /** + * Full path to source deactivate script to copy. + */ + source: string; + /** + * Full path to destination to copy deactivate script to. + */ + destination: string; + initScript: { + /** + * Display name of init script for the shell. + */ + displayName: string; + /** + * Command to run in shell to output the full path to init script. + */ + command: string; + /** + * Contents to add to init script. + */ + contents: string; + }; +}; + +// eslint-disable-next-line global-require +const untildify: (value: string) => string = require('untildify'); + +export function getDeactivateShellInfo(shellType: TerminalShellType): DeactivateShellInfo | undefined { + switch (shellType) { + case TerminalShellType.bash: + return buildInfo( + 'deactivate', + { + displayName: '~/.bashrc', + path: '~/.bashrc', + }, + `source {0}`, + ); + case TerminalShellType.powershellCore: + case TerminalShellType.powershell: + return buildInfo( + 'deactivate.ps1', + { + displayName: 'Powershell Profile', + path: '$Profile', + }, + `& "{0}"`, + ); + case TerminalShellType.zsh: + return buildInfo( + 'deactivate', + { + displayName: '~/.zshrc', + path: '~/.zshrc', + }, + `source {0}`, + ); + case TerminalShellType.fish: + return buildInfo( + 'deactivate.fish', + { + displayName: 'config.fish', + path: '$__fish_config_dir/config.fish', + }, + `source {0}`, + ); + case TerminalShellType.cshell: + return buildInfo( + 'deactivate.csh', + { + displayName: '~/.cshrc', + path: '~/.cshrc', + }, + `source {0}`, + ); + default: + return undefined; + } +} + +function buildInfo( + deactivate: string, + initScript: { + path: string; + displayName: string; + }, + scriptCommandFormat: string, +) { + const scriptPath = path.join('~', '.vscode-python', deactivate); + return { + source: path.join(_SCRIPTS_DIR, deactivate), + destination: untildify(scriptPath), + initScript: { + displayName: initScript.displayName, + command: `echo ${initScript.path}`, + contents: scriptCommandFormat.format(scriptPath), + }, + }; +} diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts b/src/client/terminals/envCollectionActivation/indicatorPrompt.ts similarity index 90% rename from src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts rename to src/client/terminals/envCollectionActivation/indicatorPrompt.ts index c8aea205a32a..bc4c3cc90fc0 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionPrompt.ts +++ b/src/client/terminals/envCollectionActivation/indicatorPrompt.ts @@ -14,15 +14,17 @@ import { } from '../../common/types'; import { Common, Interpreters } from '../../common/utils/localize'; import { IExtensionSingleActivationService } from '../../activation/types'; -import { ITerminalEnvVarCollectionService } from './types'; import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; -import { IInterpreterService } from '../contracts'; +import { IInterpreterService } from '../../interpreter/contracts'; import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { ITerminalEnvVarCollectionService } from '../types'; +import { sleep } from '../../common/utils/async'; +import { isTestExecution } from '../../common/constants'; export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; @injectable() -export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivationService { +export class TerminalIndicatorPrompt implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; constructor( @@ -42,6 +44,10 @@ export class TerminalEnvVarCollectionPrompt implements IExtensionSingleActivatio if (!inTerminalEnvVarExperiment(this.experimentService)) { return; } + if (!isTestExecution()) { + // Avoid showing prompt until startup completes. + await sleep(5000); + } this.disposableRegistry.push( this.terminalManager.onDidOpenTerminal(async (terminal) => { const cwd = diff --git a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts b/src/client/terminals/envCollectionActivation/service.ts similarity index 87% rename from src/client/interpreter/activation/terminalEnvVarCollectionService.ts rename to src/client/terminals/envCollectionActivation/service.ts index 92e97c95e468..e08a1f7e72c3 100644 --- a/src/client/interpreter/activation/terminalEnvVarCollectionService.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -4,12 +4,12 @@ import * as path from 'path'; import { inject, injectable } from 'inversify'; import { - ProgressOptions, - ProgressLocation, MarkdownString, WorkspaceFolder, GlobalEnvironmentVariableCollection, EnvironmentVariableScope, + EnvironmentVariableMutatorOptions, + ProgressLocation, } from 'vscode'; import { pathExists } from 'fs-extra'; import { IExtensionActivationService } from '../../activation/types'; @@ -25,12 +25,11 @@ import { IConfigurationService, IPathUtils, } from '../../common/types'; -import { Deferred, createDeferred } from '../../common/utils/async'; import { Interpreters } from '../../common/utils/localize'; -import { traceDecoratorVerbose, traceError, traceVerbose, traceWarn } from '../../logging'; -import { IInterpreterService } from '../contracts'; -import { defaultShells } from './service'; -import { IEnvironmentActivationService, ITerminalEnvVarCollectionService } from './types'; +import { traceError, traceVerbose, traceWarn } from '../../logging'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { defaultShells } from '../../interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../interpreter/activation/types'; import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; import { getSearchPathEnvVarNames } from '../../common/utils/exec'; import { EnvironmentVariables } from '../../common/variables/types'; @@ -38,6 +37,9 @@ import { TerminalShellType } from '../../common/terminal/types'; import { OSType } from '../../common/utils/platform'; import { normCase } from '../../common/platform/fs-paths'; import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { ITerminalEnvVarCollectionService } from '../types'; +import { ShellIntegrationShells } from './shellIntegration'; +import { ProgressService } from '../../common/application/progressService'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { @@ -55,8 +57,6 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ TerminalShellType.fish, ]; - private deferred: Deferred | undefined; - private registeredOnce = false; /** @@ -64,6 +64,8 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ */ private processEnvVars: EnvironmentVariables | undefined; + private readonly progressService: ProgressService; + private separator: string; constructor( @@ -80,6 +82,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ @inject(IPathUtils) private readonly pathUtils: IPathUtils, ) { this.separator = platform.osType === OSType.Windows ? ';' : ':'; + this.progressService = new ProgressService(this.shell); } public async activate(resource: Resource): Promise { @@ -126,9 +129,12 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } public async _applyCollection(resource: Resource, shell?: string): Promise { - this.showProgress(); + this.progressService.showProgress({ + location: ProgressLocation.Window, + title: Interpreters.activatingTerminals, + }); await this._applyCollectionImpl(resource, shell); - this.hideProgress(); + this.progressService.hideProgress(); } private async _applyCollectionImpl(resource: Resource, shell = this.applicationEnvironment.shell): Promise { @@ -171,6 +177,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ // PS1 in some cases is a shell variable (not an env variable) so "env" might not contain it, calculate it in that case. env.PS1 = await this.getPS1(shell, resource, env); + const prependOptions = this.getPrependOptions(); // Clear any previously set env vars from collection envVarCollection.clear(); @@ -185,10 +192,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ if (key === 'PS1') { // We cannot have the full PS1 without executing in terminal, which we do not. Hence prepend it. traceVerbose(`Prepending environment variable ${key} in collection with ${value}`); - envVarCollection.prepend(key, value, { - applyAtShellIntegration: true, - applyAtProcessCreation: false, - }); + envVarCollection.prepend(key, value, prependOptions); return; } if (key === 'PATH') { @@ -198,19 +202,13 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ const prependedPart = env.PATH.slice(0, -processEnv.PATH.length); value = prependedPart; traceVerbose(`Prepending environment variable ${key} in collection with ${value}`); - envVarCollection.prepend(key, value, { - applyAtShellIntegration: true, - applyAtProcessCreation: true, - }); + envVarCollection.prepend(key, value, prependOptions); } else { if (!value.endsWith(this.separator)) { value = value.concat(this.separator); } traceVerbose(`Prepending environment variable ${key} in collection to ${value}`); - envVarCollection.prepend(key, value, { - applyAtShellIntegration: true, - applyAtProcessCreation: true, - }); + envVarCollection.prepend(key, value, prependOptions); } return; } @@ -272,9 +270,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ // PS1 should be set but no PS1 was set. return; } - const config = this.workspaceService - .getConfiguration('terminal') - .get('integrated.shellIntegration.enabled'); + const config = this.isShellIntegrationActive(); if (!config) { traceVerbose('PS1 is not set when shell integration is disabled.'); return; @@ -329,6 +325,36 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } } + private getPrependOptions(): EnvironmentVariableMutatorOptions { + const isActive = this.isShellIntegrationActive(); + // Ideally we would want to prepend exactly once, either at shell integration or process creation. + // TODO: Stop prepending altogether once https://github.com/microsoft/vscode/issues/145234 is available. + return isActive + ? { + applyAtShellIntegration: true, + applyAtProcessCreation: false, + } + : { + applyAtShellIntegration: true, // Takes care of false negatives in case manual integration is being used. + applyAtProcessCreation: true, + }; + } + + private isShellIntegrationActive(): boolean { + const isEnabled = this.workspaceService + .getConfiguration('terminal') + .get('integrated.shellIntegration.enabled')!; + if ( + isEnabled && + ShellIntegrationShells.includes(identifyShellFromShellPath(this.applicationEnvironment.shell)) + ) { + // Unfortunately shell integration could still've failed in remote scenarios, we can't know for sure: + // https://code.visualstudio.com/docs/terminal/shell-integration#_automatic-script-injection + return true; + } + return false; + } + private getEnvironmentVariableCollection(scope: EnvironmentVariableScope = {}) { const envVarCollection = this.context.environmentVariableCollection as GlobalEnvironmentVariableCollection; return envVarCollection.getScoped(scope); @@ -345,32 +371,6 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ } return workspaceFolder; } - - @traceDecoratorVerbose('Display activating terminals') - private showProgress(): void { - if (!this.deferred) { - this.createProgress(); - } - } - - @traceDecoratorVerbose('Hide activating terminals') - private hideProgress(): void { - if (this.deferred) { - this.deferred.resolve(); - this.deferred = undefined; - } - } - - private createProgress() { - const progressOptions: ProgressOptions = { - location: ProgressLocation.Window, - title: Interpreters.activatingTerminals, - }; - this.shell.withProgress(progressOptions, () => { - this.deferred = createDeferred(); - return this.deferred.promise; - }); - } } function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariables): boolean { diff --git a/src/client/terminals/envCollectionActivation/shellIntegration.ts b/src/client/terminals/envCollectionActivation/shellIntegration.ts new file mode 100644 index 000000000000..1be2501595a4 --- /dev/null +++ b/src/client/terminals/envCollectionActivation/shellIntegration.ts @@ -0,0 +1,13 @@ +import { TerminalShellType } from '../../common/terminal/types'; + +/** + * This is a list of shells which support shell integration: + * https://code.visualstudio.com/docs/terminal/shell-integration + */ +export const ShellIntegrationShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.bash, + TerminalShellType.zsh, + TerminalShellType.fish, +]; diff --git a/src/client/terminals/serviceRegistry.ts b/src/client/terminals/serviceRegistry.ts index a39ef31a8fe4..a9da776d011a 100644 --- a/src/client/terminals/serviceRegistry.ts +++ b/src/client/terminals/serviceRegistry.ts @@ -1,25 +1,26 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { interfaces } from 'inversify'; -import { ClassType } from '../ioc/types'; +import { IServiceManager } from '../ioc/types'; import { TerminalAutoActivation } from './activation'; import { CodeExecutionManager } from './codeExecution/codeExecutionManager'; import { DjangoShellCodeExecutionProvider } from './codeExecution/djangoShellCodeExecution'; import { CodeExecutionHelper } from './codeExecution/helper'; import { ReplProvider } from './codeExecution/repl'; import { TerminalCodeExecutionProvider } from './codeExecution/terminalCodeExecution'; -import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, ITerminalAutoActivation } from './types'; +import { + ICodeExecutionHelper, + ICodeExecutionManager, + ICodeExecutionService, + ITerminalAutoActivation, + ITerminalEnvVarCollectionService, +} from './types'; +import { TerminalEnvVarCollectionService } from './envCollectionActivation/service'; +import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; +import { TerminalDeactivateLimitationPrompt } from './envCollectionActivation/deactivatePrompt'; +import { TerminalIndicatorPrompt } from './envCollectionActivation/indicatorPrompt'; -interface IServiceRegistry { - addSingleton( - serviceIdentifier: interfaces.ServiceIdentifier, - constructor: ClassType, - name?: string | number | symbol, - ): void; -} - -export function registerTypes(serviceManager: IServiceRegistry): void { +export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ICodeExecutionHelper, CodeExecutionHelper); serviceManager.addSingleton(ICodeExecutionManager, CodeExecutionManager); @@ -37,4 +38,17 @@ export function registerTypes(serviceManager: IServiceRegistry): void { serviceManager.addSingleton(ICodeExecutionService, ReplProvider, 'repl'); serviceManager.addSingleton(ITerminalAutoActivation, TerminalAutoActivation); + serviceManager.addSingleton( + ITerminalEnvVarCollectionService, + TerminalEnvVarCollectionService, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalIndicatorPrompt, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalDeactivateLimitationPrompt, + ); + serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); } diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index 48e39d4e1c81..ba30b8f6d47d 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -33,3 +33,11 @@ export interface ITerminalAutoActivation extends IDisposable { register(): void; disableAutoActivation(terminal: Terminal): void; } + +export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); +export interface ITerminalEnvVarCollectionService { + /** + * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. + */ + isTerminalPromptSetCorrectly(resource?: Resource): boolean; +} diff --git a/src/test/common/application/progressService.unit.test.ts b/src/test/common/application/progressService.unit.test.ts new file mode 100644 index 000000000000..b9c49ccb4060 --- /dev/null +++ b/src/test/common/application/progressService.unit.test.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import { anything, capture, instance, mock, when } from 'ts-mockito'; +import { CancellationToken, Progress, ProgressLocation, ProgressOptions } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { ProgressService } from '../../../client/common/application/progressService'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { createDeferred, createDeferredFromPromise, Deferred, sleep } from '../../../client/common/utils/async'; + +type ProgressTask = ( + progress: Progress<{ message?: string; increment?: number }>, + token: CancellationToken, +) => Thenable; + +suite('Progress Service', () => { + let refreshDeferred: Deferred; + let shell: ApplicationShell; + let progressService: ProgressService; + setup(() => { + refreshDeferred = createDeferred(); + shell = mock(); + progressService = new ProgressService(instance(shell)); + }); + teardown(() => { + refreshDeferred.resolve(); + }); + test('Display discovering message when refreshing interpreters for the first time', async () => { + when(shell.withProgress(anything(), anything())).thenResolve(); + const expectedOptions = { title: 'message', location: ProgressLocation.Window }; + + progressService.showProgress(expectedOptions); + + const options = capture(shell.withProgress as never).last()[0] as ProgressOptions; + assert.deepEqual(options, expectedOptions); + }); + + test('Progress message is hidden when loading has completed', async () => { + when(shell.withProgress(anything(), anything())).thenResolve(); + const options = { title: 'message', location: ProgressLocation.Window }; + progressService.showProgress(options); + + const callback = capture(shell.withProgress as never).last()[1] as ProgressTask; + const promise = callback(undefined as never, undefined as never); + const deferred = createDeferredFromPromise(promise as Promise); + await sleep(1); + expect(deferred.completed).to.be.equal(false, 'Progress disappeared before hiding it'); + progressService.hideProgress(); + await sleep(1); + expect(deferred.completed).to.be.equal(true, 'Progress did not disappear'); + }); +}); diff --git a/src/test/common/platform/fs-temp.functional.test.ts b/src/test/common/platform/fs-temp.functional.test.ts index 256d52a81cf0..9fb4fe189b96 100644 --- a/src/test/common/platform/fs-temp.functional.test.ts +++ b/src/test/common/platform/fs-temp.functional.test.ts @@ -5,7 +5,7 @@ import { expect, use } from 'chai'; import * as fs from 'fs-extra'; import { TemporaryFileSystem } from '../../../client/common/platform/fs-temp'; import { TemporaryFile } from '../../../client/common/platform/types'; -import { assertDoesNotExist, assertExists, FSFixture, WINDOWS } from './utils'; +import { assertDoesNotExist, assertExists, FSFixture } from './utils'; const assertArrays = require('chai-arrays'); use(require('chai-as-promised')); @@ -56,21 +56,6 @@ suite('FileSystem - TemporaryFileSystem', () => { expect(filename1).to.not.equal(filename2); }); - test('Ensure writing to a temp file is supported via file stream', async function () { - if (WINDOWS) { - this.skip(); - } - const tempfile = await createFile('.tmp'); - const stream = fs.createWriteStream(tempfile.filePath); - fix.addCleanup(() => stream.destroy()); - const data = '...'; - - stream.write(data, 'utf8'); - - const actual = await fs.readFile(tempfile.filePath, 'utf8'); - expect(actual).to.equal(data); - }); - test('Ensure chmod works against a temporary file', async () => { // Note that on Windows chmod is a noop. const tempfile = await createFile('.tmp'); diff --git a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts index cabf293ba958..58ae464d0113 100644 --- a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts +++ b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts @@ -63,7 +63,8 @@ suite('Activation of Environments in Terminal', () => { await terminalSettings.update('integrated.defaultProfile.linux', 'bash', vscode.ConfigurationTarget.Global); }); - setup(async () => { + setup(async function () { + this.skip(); // https://github.com/microsoft/vscode-python/issues/22264 await initializeTest(); outputFile = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts index baa83c8b11c5..5d4da49ebb45 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionPrompt.unit.test.ts @@ -13,13 +13,13 @@ import { IPersistentStateFactory, IPythonSettings, } from '../../../client/common/types'; -import { TerminalEnvVarCollectionPrompt } from '../../../client/interpreter/activation/terminalEnvVarCollectionPrompt'; -import { ITerminalEnvVarCollectionService } from '../../../client/interpreter/activation/types'; +import { TerminalIndicatorPrompt } from '../../../client/terminals/envCollectionActivation/indicatorPrompt'; import { Common, Interpreters } from '../../../client/common/utils/localize'; import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; import { sleep } from '../../core'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { ITerminalEnvVarCollectionService } from '../../../client/terminals/types'; suite('Terminal Environment Variable Collection Prompt', () => { let shell: IApplicationShell; @@ -28,7 +28,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { let activeResourceService: IActiveResourceService; let terminalEnvVarCollectionService: ITerminalEnvVarCollectionService; let persistentStateFactory: IPersistentStateFactory; - let terminalEnvVarCollectionPrompt: TerminalEnvVarCollectionPrompt; + let terminalEnvVarCollectionPrompt: TerminalIndicatorPrompt; let terminalEventEmitter: EventEmitter; let notificationEnabled: IPersistentState; let configurationService: IConfigurationService; @@ -61,7 +61,7 @@ suite('Terminal Environment Variable Collection Prompt', () => { ); when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); when(terminalManager.onDidOpenTerminal).thenReturn(terminalEventEmitter.event); - terminalEnvVarCollectionPrompt = new TerminalEnvVarCollectionPrompt( + terminalEnvVarCollectionPrompt = new TerminalIndicatorPrompt( instance(shell), instance(persistentStateFactory), instance(terminalManager), diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index e41d6ce4d53c..88b9c978854c 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -32,7 +32,7 @@ import { import { Interpreters } from '../../../client/common/utils/localize'; import { OSType, getOSType } from '../../../client/common/utils/platform'; import { defaultShells } from '../../../client/interpreter/activation/service'; -import { TerminalEnvVarCollectionService } from '../../../client/interpreter/activation/terminalEnvVarCollectionService'; +import { TerminalEnvVarCollectionService } from '../../../client/terminals/envCollectionActivation/service'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PathUtils } from '../../../client/common/platform/pathUtils'; @@ -331,7 +331,7 @@ suite('Terminal Environment Variable Collection Service', () => { verify(collection.clear()).once(); verify(collection.prepend('PATH', prependedPart, anything())).once(); verify(collection.replace('PATH', anything(), anything())).never(); - assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); test('Prepend full PATH with separator otherwise', async () => { @@ -364,7 +364,7 @@ suite('Terminal Environment Variable Collection Service', () => { verify(collection.clear()).once(); verify(collection.prepend('PATH', `${finalPath}${separator}`, anything())).once(); verify(collection.replace('PATH', anything(), anything())).never(); - assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); }); test('Verify envs are not applied if env activation is disabled', async () => { diff --git a/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts b/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts new file mode 100644 index 000000000000..f775241abb32 --- /dev/null +++ b/src/test/terminals/envCollectionActivation/deactivatePrompt.unit.test.ts @@ -0,0 +1,271 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; +import { EventEmitter, Terminal, TerminalDataWriteEvent, TextDocument, TextEditor, Uri } from 'vscode'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import { + IApplicationEnvironment, + IApplicationShell, + IDocumentManager, + ITerminalManager, +} from '../../../client/common/application/types'; +import { IExperimentService, IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; +import { Common, Interpreters } from '../../../client/common/utils/localize'; +import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; +import { sleep } from '../../core'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { TerminalDeactivateLimitationPrompt } from '../../../client/terminals/envCollectionActivation/deactivatePrompt'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; +import { TerminalShellType } from '../../../client/common/terminal/types'; +import * as processApi from '../../../client/common/process/rawProcessApis'; +import * as fsapi from '../../../client/common/platform/fs-paths'; +import { noop } from '../../../client/common/utils/misc'; + +suite('Terminal Deactivation Limitation Prompt', () => { + let shell: IApplicationShell; + let experimentService: IExperimentService; + let persistentStateFactory: IPersistentStateFactory; + let appEnvironment: IApplicationEnvironment; + let deactivatePrompt: TerminalDeactivateLimitationPrompt; + let terminalWriteEvent: EventEmitter; + let notificationEnabled: IPersistentState; + let interpreterService: IInterpreterService; + let terminalManager: ITerminalManager; + let documentManager: IDocumentManager; + const prompts = [Common.editSomething.format('~/.bashrc'), Common.doNotShowAgain]; + const expectedMessage = Interpreters.terminalDeactivatePrompt.format('~/.bashrc'); + const initScriptPath = 'home/node/.bashrc'; + const resource = Uri.file('a'); + let terminal: Terminal; + + setup(async () => { + const activeEditorEvent = new EventEmitter(); + const document = ({ + uri: Uri.file(''), + getText: () => '', + } as unknown) as TextDocument; + sinon.stub(processApi, 'shellExec').callsFake(async (command: string) => { + if (command !== 'echo ~/.bashrc') { + throw new Error(`Unexpected command: ${command}`); + } + await sleep(1500); + return { stdout: initScriptPath }; + }); + documentManager = mock(); + terminalManager = mock(); + terminal = ({ + creationOptions: { cwd: resource }, + processId: Promise.resolve(1), + dispose: noop, + show: noop, + sendText: noop, + } as unknown) as Terminal; + when(terminalManager.createTerminal(anything())).thenReturn(terminal); + when(documentManager.openTextDocument(initScriptPath)).thenReturn(Promise.resolve(document)); + when(documentManager.onDidChangeActiveTextEditor).thenReturn(activeEditorEvent.event); + shell = mock(); + interpreterService = mock(); + experimentService = mock(); + persistentStateFactory = mock(); + appEnvironment = mock(); + when(appEnvironment.shell).thenReturn('bash'); + notificationEnabled = mock>(); + terminalWriteEvent = new EventEmitter(); + when(persistentStateFactory.createGlobalPersistentState(anything(), true)).thenReturn( + instance(notificationEnabled), + ); + when(shell.onDidWriteTerminalData).thenReturn(terminalWriteEvent.event); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); + deactivatePrompt = new TerminalDeactivateLimitationPrompt( + instance(shell), + instance(persistentStateFactory), + [], + instance(interpreterService), + instance(appEnvironment), + instance(documentManager), + instance(terminalManager), + instance(experimentService), + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Show notification when "deactivate" command is run when a virtual env is selected', async () => { + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).once(); + }); + + test('When using cmd, do not show notification for the same', async () => { + reset(appEnvironment); + when(appEnvironment.shell).thenReturn(TerminalShellType.commandPrompt); + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); + }); + + test('When not in experiment, do not show notification for the same', async () => { + reset(experimentService); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); + + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); + }); + + test('Do not show notification if notification is disabled', async () => { + when(notificationEnabled.value).thenReturn(false); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); + }); + + test('Do not show notification when virtual env is not activated for terminal', async () => { + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Conda, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(shell.showWarningMessage(expectedMessage, ...prompts)).never(); + }); + + test("Disable notification if `Don't show again` is clicked", async () => { + when(notificationEnabled.value).thenReturn(true); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(shell.showWarningMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(Common.doNotShowAgain)); + + await deactivatePrompt.activate(); + terminalWriteEvent.fire({ data: 'Please deactivate me', terminal }); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).once(); + }); + + test('Edit script correctly if `Edit - - - - `; - } } diff --git a/src/client/tensorBoard/tensorBoardSessionProvider.ts b/src/client/tensorBoard/tensorBoardSessionProvider.ts deleted file mode 100644 index ec52b9ef94dc..000000000000 --- a/src/client/tensorBoard/tensorBoardSessionProvider.ts +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { Disposable, l10n, ViewColumn } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; -import { Commands } from '../common/constants'; -import { ContextKey } from '../common/contextKey'; -import { IPythonExecutionFactory } from '../common/process/types'; -import { - IDisposableRegistry, - IInstaller, - IPersistentState, - IPersistentStateFactory, - IConfigurationService, - IDisposable, -} from '../common/types'; -import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; -import { IInterpreterService } from '../interpreter/contracts'; -import { traceError, traceVerbose } from '../logging'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; -import { TensorBoardSession } from './tensorBoardSession'; -import { TensorboardExperiment } from './tensorboarExperiment'; - -export const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup'; - -@injectable() -export class TensorBoardSessionProvider implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private knownSessions: TensorBoardSession[] = []; - - private preferredViewGroupMemento: IPersistentState; - - private hasActiveTensorBoardSessionContext: ContextKey; - - private readonly disposables: IDisposable[] = []; - - constructor( - @inject(IInstaller) private readonly installer: IInstaller, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IDisposableRegistry) disposables: IDisposableRegistry, - @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, - @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, - @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, - ) { - disposables.push(this); - this.preferredViewGroupMemento = this.stateFactory.createGlobalPersistentState( - PREFERRED_VIEWGROUP, - ViewColumn.Active, - ); - this.hasActiveTensorBoardSessionContext = new ContextKey( - 'python.hasActiveTensorBoardSession', - this.commandManager, - ); - } - - public dispose(): void { - Disposable.from(...this.disposables).dispose(); - } - - public async activate(): Promise { - if (TensorboardExperiment.isTensorboardExtensionInstalled) { - return; - } - this.experiment.disposeOnInstallingTensorboard(this); - - this.disposables.push( - this.commandManager.registerCommand( - Commands.LaunchTensorBoard, - ( - entrypoint: TensorBoardEntrypoint = TensorBoardEntrypoint.palette, - trigger: TensorBoardEntrypointTrigger = TensorBoardEntrypointTrigger.palette, - ): void => { - sendTelemetryEvent(EventName.TENSORBOARD_SESSION_LAUNCH, undefined, { - trigger, - entrypoint, - }); - if (this.experiment.recommendAndUseNewExtension() === 'continueWithPythonExtension') { - void this.createNewSession(); - } - }, - ), - this.commandManager.registerCommand(Commands.RefreshTensorBoard, () => - this.experiment.recommendAndUseNewExtension() === 'continueWithPythonExtension' - ? this.knownSessions.map((w) => w.refresh()) - : undefined, - ), - ); - } - - private async updateTensorBoardSessionContext() { - let hasActiveTensorBoardSession = false; - this.knownSessions.forEach((viewer) => { - if (viewer.active) { - hasActiveTensorBoardSession = true; - } - }); - await this.hasActiveTensorBoardSessionContext.set(hasActiveTensorBoardSession); - } - - private async didDisposeSession(session: TensorBoardSession) { - this.knownSessions = this.knownSessions.filter((s) => s !== session); - this.updateTensorBoardSessionContext(); - } - - private async createNewSession(): Promise { - traceVerbose('Starting new TensorBoard session...'); - try { - const newSession = new TensorBoardSession( - this.installer, - this.interpreterService, - this.workspaceService, - this.pythonExecFactory, - this.commandManager, - this.disposables, - this.applicationShell, - this.preferredViewGroupMemento, - this.multiStepFactory, - this.configurationService, - ); - newSession.onDidChangeViewState(() => this.updateTensorBoardSessionContext(), this, this.disposables); - newSession.onDidDispose((e) => this.didDisposeSession(e), this, this.disposables); - this.knownSessions.push(newSession); - await newSession.initialize(); - return newSession; - } catch (e) { - traceError(`Encountered error while starting new TensorBoard session: ${e}`); - await this.applicationShell.showErrorMessage( - l10n.t( - 'We failed to start a TensorBoard session due to the following error: {0}', - (e as Error).message, - ), - ); - } - return undefined; - } -} diff --git a/src/client/tensorBoard/tensorBoardUsageTracker.ts b/src/client/tensorBoard/tensorBoardUsageTracker.ts deleted file mode 100644 index d1b21473677f..000000000000 --- a/src/client/tensorBoard/tensorBoardUsageTracker.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Disposable, TextEditor } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { IDocumentManager } from '../common/application/types'; -import { isTestExecution } from '../common/constants'; -import { IDisposableRegistry } from '../common/types'; -import { getDocumentLines } from '../telemetry/importTracker'; -import { TensorBoardEntrypointTrigger } from './constants'; -import { containsTensorBoardImport } from './helpers'; -import { TensorBoardPrompt } from './tensorBoardPrompt'; -import { TensorboardExperiment } from './tensorboarExperiment'; - -const testExecution = isTestExecution(); - -// Prompt the user to start an integrated TensorBoard session whenever the active Python file or Python notebook -// contains a valid TensorBoard import. -@injectable() -export class TensorBoardUsageTracker implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(IDocumentManager) private documentManager: IDocumentManager, - @inject(IDisposableRegistry) private disposables: IDisposableRegistry, - @inject(TensorBoardPrompt) private prompt: TensorBoardPrompt, - @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, - ) {} - - public dispose(): void { - Disposable.from(...this.disposables).dispose(); - } - - public async activate(): Promise { - if (TensorboardExperiment.isTensorboardExtensionInstalled) { - return; - } - this.experiment.disposeOnInstallingTensorboard(this); - if (testExecution) { - await this.activateInternal(); - } else { - this.activateInternal().ignoreErrors(); - } - } - - private async activateInternal() { - // Process currently active text editor - this.onChangedActiveTextEditor(this.documentManager.activeTextEditor); - // Process changes to active text editor as well - this.documentManager.onDidChangeActiveTextEditor( - (e) => this.onChangedActiveTextEditor(e), - this, - this.disposables, - ); - } - - private onChangedActiveTextEditor(editor: TextEditor | undefined): void { - if (!editor || !editor.document) { - return; - } - const { document } = editor; - const extName = path.extname(document.fileName).toLowerCase(); - if (extName === '.py' || (extName === '.ipynb' && document.languageId === 'python')) { - const lines = getDocumentLines(document); - if (containsTensorBoardImport(lines)) { - this.prompt.showNativeTensorBoardPrompt(TensorBoardEntrypointTrigger.fileimport).ignoreErrors(); - } - } - } -} diff --git a/src/client/tensorBoard/tensorboarExperiment.ts b/src/client/tensorBoard/tensorboarExperiment.ts deleted file mode 100644 index 3cf4cb3c779a..000000000000 --- a/src/client/tensorBoard/tensorboarExperiment.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { Disposable, EventEmitter, commands, extensions, l10n, window } from 'vscode'; -import { inject, injectable } from 'inversify'; -import { IDisposable, IDisposableRegistry, IExperimentService } from '../common/types'; -import { RecommendTensobardExtension } from '../common/experiments/groups'; -import { TENSORBOARD_EXTENSION_ID } from '../common/constants'; - -@injectable() -export class TensorboardExperiment { - private readonly _onDidChange = new EventEmitter(); - - public readonly onDidChange = this._onDidChange.event; - - private readonly toDisposeWhenTensobardIsInstalled: IDisposable[] = []; - - public static get isTensorboardExtensionInstalled(): boolean { - return !!extensions.getExtension(TENSORBOARD_EXTENSION_ID); - } - - private readonly isExperimentEnabled: boolean; - - constructor( - @inject(IDisposableRegistry) disposables: IDisposableRegistry, - @inject(IExperimentService) experiments: IExperimentService, - ) { - this.isExperimentEnabled = experiments.inExperimentSync(RecommendTensobardExtension.experiment); - disposables.push(this._onDidChange); - extensions.onDidChange( - () => - TensorboardExperiment.isTensorboardExtensionInstalled - ? Disposable.from(...this.toDisposeWhenTensobardIsInstalled).dispose() - : undefined, - this, - disposables, - ); - } - - public recommendAndUseNewExtension(): 'continueWithPythonExtension' | 'usingTensorboardExtension' { - if (!this.isExperimentEnabled) { - return 'continueWithPythonExtension'; - } - if (TensorboardExperiment.isTensorboardExtensionInstalled) { - return 'usingTensorboardExtension'; - } - const install = l10n.t('Install Tensorboard Extension'); - window - .showInformationMessage( - l10n.t( - 'Install the TensorBoard extension to use the this functionality. Once installed, select the command `Launch Tensorboard`.', - ), - { modal: true }, - install, - ) - .then((result): void => { - if (result === install) { - void commands.executeCommand('workbench.extensions.installExtension', TENSORBOARD_EXTENSION_ID); - } - }); - return 'usingTensorboardExtension'; - } - - public disposeOnInstallingTensorboard(disposabe: IDisposable): void { - this.toDisposeWhenTensobardIsInstalled.push(disposabe); - } -} diff --git a/src/client/tensorBoard/tensorboardDependencyChecker.ts b/src/client/tensorBoard/tensorboardDependencyChecker.ts index 5c377e1d2455..995344284eec 100644 --- a/src/client/tensorBoard/tensorboardDependencyChecker.ts +++ b/src/client/tensorBoard/tensorboardDependencyChecker.ts @@ -2,59 +2,29 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Uri, ViewColumn } from 'vscode'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; -import { IPythonExecutionFactory } from '../common/process/types'; -import { - IInstaller, - IPersistentState, - IPersistentStateFactory, - IConfigurationService, - IDisposable, -} from '../common/types'; -import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; +import { Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../common/application/types'; +import { IInstaller } from '../common/types'; import { IInterpreterService } from '../interpreter/contracts'; import { TensorBoardSession } from './tensorBoardSession'; -import { disposeAll } from '../common/utils/resourceLifecycle'; -import { PREFERRED_VIEWGROUP } from './tensorBoardSessionProvider'; @injectable() export class TensorboardDependencyChecker { - private preferredViewGroupMemento: IPersistentState; - constructor( @inject(IInstaller) private readonly installer: IInstaller, @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, - @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, - @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - ) { - this.preferredViewGroupMemento = this.stateFactory.createGlobalPersistentState( - PREFERRED_VIEWGROUP, - ViewColumn.Active, - ); - } + ) {} public async ensureDependenciesAreInstalled(resource?: Uri): Promise { - const disposables: IDisposable[] = []; const newSession = new TensorBoardSession( this.installer, this.interpreterService, - this.workspaceService, - this.pythonExecFactory, this.commandManager, - disposables, this.applicationShell, - this.preferredViewGroupMemento, - this.multiStepFactory, - this.configurationService, ); const result = await newSession.ensurePrerequisitesAreInstalled(resource); - disposeAll(disposables); return result; } } diff --git a/src/client/tensorBoard/tensorboardIntegration.ts b/src/client/tensorBoard/tensorboardIntegration.ts index 22d590d6ee65..f3cbad59977b 100644 --- a/src/client/tensorBoard/tensorboardIntegration.ts +++ b/src/client/tensorBoard/tensorboardIntegration.ts @@ -5,10 +5,10 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Extension, Uri, commands } from 'vscode'; +import { Extension, Uri } from 'vscode'; import { IWorkspaceService } from '../common/application/types'; import { TENSORBOARD_EXTENSION_ID } from '../common/constants'; -import { IDisposableRegistry, IExtensions, Resource } from '../common/types'; +import { IExtensions, Resource } from '../common/types'; import { IEnvironmentActivationService } from '../interpreter/activation/types'; import { TensorBoardPrompt } from './tensorBoardPrompt'; import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; @@ -45,14 +45,9 @@ export class TensorboardExtensionIntegration { @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(TensorboardDependencyChecker) private readonly dependencyChcker: TensorboardDependencyChecker, @inject(TensorBoardPrompt) private readonly tensorBoardPrompt: TensorBoardPrompt, - @inject(IDisposableRegistry) disposables: IDisposableRegistry, - ) { - this.hideCommands(); - extensions.onDidChange(this.hideCommands, this, disposables); - } + ) {} public registerApi(tensorboardExtensionApi: TensorboardExtensionApi): TensorboardExtensionApi | undefined { - this.hideCommands(); if (!this.workspaceService.isTrusted) { this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(tensorboardExtensionApi)); return undefined; @@ -67,12 +62,6 @@ export class TensorboardExtensionIntegration { return undefined; } - public hideCommands(): void { - if (this.extensions.getExtension(TENSORBOARD_EXTENSION_ID)) { - void commands.executeCommand('setContext', 'python.tensorboardExtInstalled', true); - } - } - public async integrateWithTensorboardExtension(): Promise { const api = await this.getExtensionApi(); if (api) { diff --git a/src/client/tensorBoard/terminalWatcher.ts b/src/client/tensorBoard/terminalWatcher.ts deleted file mode 100644 index 5f48def54e43..000000000000 --- a/src/client/tensorBoard/terminalWatcher.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { inject, injectable } from 'inversify'; -import { window } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { IDisposable, IDisposableRegistry } from '../common/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { TensorboardExperiment } from './tensorboarExperiment'; - -// Every 5 min look, through active terminals to see if any are running `tensorboard` -@injectable() -export class TerminalWatcher implements IExtensionSingleActivationService, IDisposable { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private handle: NodeJS.Timeout | undefined; - - constructor( - @inject(IDisposableRegistry) private disposables: IDisposableRegistry, - @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, - ) { - disposables.push(this); - } - - public async activate(): Promise { - if (TensorboardExperiment.isTensorboardExtensionInstalled) { - return; - } - this.experiment.disposeOnInstallingTensorboard(this); - const handle = setInterval(() => { - // When user runs a command in VSCode terminal, the terminal's name - // becomes the program that is currently running. Since tensorboard - // stays running in the terminal while the webapp is running and - // until the user kills it, the terminal with the updated name should - // stick around for long enough that we only need to run this check - // every 5 min or so - const matches = window.terminals.filter((terminal) => terminal.name === 'tensorboard'); - if (matches.length > 0) { - sendTelemetryEvent(EventName.TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL); - clearInterval(handle); // Only need telemetry sent once per VS Code session - } - }, 300_000); - this.handle = handle; - this.disposables.push(this); - } - - public dispose(): void { - if (this.handle) { - clearInterval(this.handle); - } - } -} diff --git a/src/client/tensorBoard/types.ts b/src/client/tensorBoard/types.ts deleted file mode 100644 index a11659015da8..000000000000 --- a/src/client/tensorBoard/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Event, Uri } from 'vscode'; - -export const ITensorBoardImportTracker = Symbol('ITensorBoardImportTracker'); -export interface ITensorBoardImportTracker { - onDidImportTensorBoard: Event; -} - -export const ITensorboardDependencyChecker = Symbol('ITensorboardDependencyChecker'); -export interface ITensorboardDependencyChecker { - ensureDependenciesAreInstalled(resource?: Uri): Promise; -} diff --git a/src/test/tensorBoard/helpers.ts b/src/test/tensorBoard/helpers.ts deleted file mode 100644 index b9f90226b28e..000000000000 --- a/src/test/tensorBoard/helpers.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as TypeMoq from 'typemoq'; -import { IApplicationShell, ICommandManager } from '../../client/common/application/types'; -import { IPersistentStateFactory } from '../../client/common/types'; -import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; -import { MockState } from '../interpreters/mocks'; - -export function createTensorBoardPromptWithMocks(): TensorBoardPrompt { - const appShell = TypeMoq.Mock.ofType(); - const commandManager = TypeMoq.Mock.ofType(); - const persistentStateFactory = TypeMoq.Mock.ofType(); - const persistentState = new MockState(true); - persistentStateFactory - .setup((factory) => { - factory.createWorkspacePersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny()); - }) - .returns(() => persistentState); - return new TensorBoardPrompt(appShell.object, commandManager.object, persistentStateFactory.object); -} diff --git a/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts b/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts deleted file mode 100644 index d4339a4af61b..000000000000 --- a/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as sinon from 'sinon'; -import { assert } from 'chai'; -import { CancellationTokenSource } from 'vscode'; -import { instance, mock } from 'ts-mockito'; -import { TensorBoardNbextensionCodeLensProvider } from '../../client/tensorBoard/nbextensionCodeLensProvider'; -import { MockDocument } from '../mocks/mockDocument'; -import { TensorboardExperiment } from '../../client/tensorBoard/tensorboarExperiment'; - -[true, false].forEach((tbExtensionInstalled) => { - suite(`Tensorboard Extension is ${tbExtensionInstalled ? 'installed' : 'not installed'}`, () => { - suite('TensorBoard nbextension code lens provider', () => { - let experiment: TensorboardExperiment; - let codeLensProvider: TensorBoardNbextensionCodeLensProvider; - let cancelTokenSource: CancellationTokenSource; - - setup(() => { - sinon.stub(TensorboardExperiment, 'isTensorboardExtensionInstalled').returns(tbExtensionInstalled); - experiment = mock(); - codeLensProvider = new TensorBoardNbextensionCodeLensProvider([], instance(experiment)); - cancelTokenSource = new CancellationTokenSource(); - }); - teardown(() => { - sinon.restore(); - cancelTokenSource.dispose(); - }); - - test('Provide code lens for Python notebook loading tensorboard nbextension', async () => { - const document = new MockDocument('a=1\n%load_ext tensorboard', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length > 0, 'Failed to provide code lens for file loading tensorboard nbextension'); - }); - test('Provide code lens for Python notebook launching tensorboard nbextension', async () => { - const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length > 0, 'Failed to provide code lens for file loading tensorboard nbextension'); - }); - test('Fails when cancellation is signaled', () => { - const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.ipynb', async () => true); - cancelTokenSource.cancel(); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided codelens even after cancellation was requested'); - }); - // Can't verify these cases without running in vscode as we depend on vscode to not call us - // based on the DocumentSelector we provided. See nbExtensionCodeLensProvider.test.ts for that. - // test('Does not provide code lens for Python file loading tensorboard nbextension', async () => { - // const document = new MockDocument('a=1\n%load_ext tensorboard', 'foo.py', async () => true); - // const codeLens = codeLensProvider.provideCodeLenses(document); - // assert.ok(codeLens.length === 0, 'Provided code lens for Python file loading tensorboard nbextension'); - // }); - // test('Does not provide code lens for Python file launching tensorboard nbextension', async () => { - // const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.py', async () => true); - // const codeLens = codeLensProvider.provideCodeLenses(document); - // assert.ok(codeLens.length === 0, 'Provided code lens for Python file loading tensorboard nbextension'); - // }); - }); - }); -}); diff --git a/src/test/tensorBoard/tensorBoardFileWatcher.test.ts b/src/test/tensorBoard/tensorBoardFileWatcher.test.ts deleted file mode 100644 index 3ad9ada21bdb..000000000000 --- a/src/test/tensorBoard/tensorBoardFileWatcher.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { assert } from 'chai'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import * as fse from '../../client/common/platform/fs-paths'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { IExperimentService } from '../../client/common/types'; -import { TensorBoardFileWatcher } from '../../client/tensorBoard/tensorBoardFileWatcher'; -import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; -import { waitForCondition } from '../common'; -import { initialize } from '../initialize'; - -suite('TensorBoard file system watcher', async () => { - const tfeventfileName = 'events.out.tfevents.1606887221.24672.162.v2'; - const currentDirectory = process.env.CODE_TESTS_WORKSPACE ?? path.join(__dirname, '..', '..', '..', 'src', 'test'); - let showNativeTensorBoardPrompt: sinon.SinonSpy; - const sandbox = sinon.createSandbox(); - let eventFile: string | undefined; - let eventFileDirectory: string | undefined; - - async function createFiles(directory: string) { - eventFileDirectory = directory; - await fse.ensureDir(directory); - eventFile = path.join(directory, tfeventfileName); - await fse.writeFile(eventFile, ''); - } - - async function configureStubsAndActivate() { - const { serviceManager } = await initialize(); - // Stub the prompt show method so we can verify that it was called - const prompt = serviceManager.get(TensorBoardPrompt); - showNativeTensorBoardPrompt = sandbox.stub(prompt, 'showNativeTensorBoardPrompt'); - serviceManager.rebindInstance(TensorBoardPrompt, prompt); - const experimentService = serviceManager.get(IExperimentService); - sandbox.stub(experimentService, 'inExperiment').resolves(true); - const fileWatcher = serviceManager.get(TensorBoardFileWatcher); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (fileWatcher as any).activateInternal(); - } - - teardown(async () => { - sandbox.restore(); - if (eventFile) { - await fse.unlink(eventFile); - eventFile = undefined; - } - }); - - suiteTeardown(async () => { - if (eventFileDirectory && eventFileDirectory !== currentDirectory) { - await fse.rmdir(eventFileDirectory); - eventFileDirectory = undefined; - } - }); - - test('Creating tfeventfile one directory down results in prompt being shown', async () => { - const dir1 = path.join(currentDirectory, '1'); - await configureStubsAndActivate(); - await createFiles(dir1); - await waitForCondition(async () => showNativeTensorBoardPrompt.called, 5000, 'Prompt not shown'); - }); - - test('Creating tfeventfile two directories down results in prompt being called', async () => { - const dir2 = path.join(currentDirectory, '1', '2'); - await configureStubsAndActivate(); - await createFiles(dir2); - await waitForCondition(async () => showNativeTensorBoardPrompt.called, 5000, 'Prompt not shown'); - }); - - test('Creating tfeventfile three directories down does not result in prompt being called', async () => { - const dir3 = path.join(currentDirectory, '1', '2', '3'); - await configureStubsAndActivate(); - await createFiles(dir3); - await waitForCondition(async () => showNativeTensorBoardPrompt.notCalled, 5000, 'Prompt shown'); - }); - - test('No workspace folder open, prompt is not called', async () => { - const { serviceManager } = await initialize(); - - // Stub the prompt show method so we can verify that it was called - const prompt = serviceManager.get(TensorBoardPrompt); - showNativeTensorBoardPrompt = sandbox.stub(prompt, 'showNativeTensorBoardPrompt'); - serviceManager.rebindInstance(TensorBoardPrompt, prompt); - - // Pretend there are no open folders - const workspaceService = serviceManager.get(IWorkspaceService); - sandbox.stub(workspaceService, 'workspaceFolders').get(() => undefined); - serviceManager.rebindInstance(IWorkspaceService, workspaceService); - const fileWatcher = serviceManager.get(TensorBoardFileWatcher); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (fileWatcher as any).activateInternal(); - - assert.ok(showNativeTensorBoardPrompt.notCalled); - }); -}); diff --git a/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts b/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts deleted file mode 100644 index 8b16301753a6..000000000000 --- a/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as sinon from 'sinon'; -import { assert } from 'chai'; -import { CancellationTokenSource } from 'vscode'; -import { instance, mock } from 'ts-mockito'; -import { TensorBoardImportCodeLensProvider } from '../../client/tensorBoard/tensorBoardImportCodeLensProvider'; -import { MockDocument } from '../mocks/mockDocument'; -import { TensorboardExperiment } from '../../client/tensorBoard/tensorboarExperiment'; - -[true, false].forEach((tbExtensionInstalled) => { - suite(`Tensorboard Extension is ${tbExtensionInstalled ? 'installed' : 'not installed'}`, () => { - suite('TensorBoard import code lens provider', () => { - let experiment: TensorboardExperiment; - let codeLensProvider: TensorBoardImportCodeLensProvider; - let cancelTokenSource: CancellationTokenSource; - - setup(() => { - sinon.stub(TensorboardExperiment, 'isTensorboardExtensionInstalled').returns(tbExtensionInstalled); - experiment = mock(); - codeLensProvider = new TensorBoardImportCodeLensProvider([], instance(experiment)); - cancelTokenSource = new CancellationTokenSource(); - }); - teardown(() => { - sinon.restore(); - cancelTokenSource.dispose(); - }); - [ - 'import tensorboard', - 'import foo, tensorboard', - 'import foo, tensorboard, bar', - 'import tensorboardX', - 'import tensorboardX, bar', - 'import torch.profiler', - 'import foo, torch.profiler', - 'from torch.utils import tensorboard', - 'from torch.utils import foo, tensorboard', - 'import torch.utils.tensorboard, foo', - 'from torch import profiler', - ].forEach((importStatement) => { - test(`Provides code lens for Python files containing ${importStatement}`, () => { - const document = new MockDocument(importStatement, 'foo.py', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok( - codeLens.length > 0, - `Failed to provide code lens for file containing ${importStatement} import`, - ); - }); - test(`Provides code lens for Python ipynbs containing ${importStatement}`, () => { - const document = new MockDocument(importStatement, 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok( - codeLens.length > 0, - `Failed to provide code lens for ipynb containing ${importStatement} import`, - ); - }); - test('Fails when cancellation is signaled', () => { - const document = new MockDocument(importStatement, 'foo.py', async () => true); - cancelTokenSource.cancel(); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided codelens even after cancellation was requested'); - }); - }); - test('Does not provide code lens if no matching import', () => { - const document = new MockDocument('import foo', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided code lens for file without tensorboard import'); - }); - }); - }); -}); diff --git a/src/test/tensorBoard/tensorBoardPrompt.unit.test.ts b/src/test/tensorBoard/tensorBoardPrompt.unit.test.ts deleted file mode 100644 index 6f096e560d70..000000000000 --- a/src/test/tensorBoard/tensorBoardPrompt.unit.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { CommandManager } from '../../client/common/application/commandManager'; -import { Commands } from '../../client/common/constants'; -import { PersistentState, PersistentStateFactory } from '../../client/common/persistentState'; -import { Common } from '../../client/common/utils/localize'; -import { TensorBoardEntrypointTrigger } from '../../client/tensorBoard/constants'; -import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; - -suite('TensorBoard prompt', () => { - let applicationShell: ApplicationShell; - let commandManager: CommandManager; - let persistentState: PersistentState; - let persistentStateFactory: PersistentStateFactory; - let prompt: TensorBoardPrompt; - - async function setupPromptWithOptions(persistentStateValue = true, selection = 'Yes') { - applicationShell = mock(ApplicationShell); - when(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).thenReturn( - Promise.resolve(selection), - ); - - commandManager = mock(CommandManager); - when(commandManager.executeCommand(Commands.LaunchTensorBoard, anything(), anything())).thenResolve(); - - persistentStateFactory = mock(PersistentStateFactory); - persistentState = mock(PersistentState) as PersistentState; - when(persistentState.value).thenReturn(persistentStateValue); - when(persistentState.updateValue(anything())).thenResolve(); - when(persistentStateFactory.createWorkspacePersistentState(anything(), anything())).thenReturn( - instance(persistentState), - ); - - prompt = new TensorBoardPrompt( - instance(applicationShell), - instance(commandManager), - instance(persistentStateFactory), - ); - await prompt.showNativeTensorBoardPrompt(TensorBoardEntrypointTrigger.palette); - } - - test('Show prompt if user is in experiment, and prompt has not previously been disabled or shown', async () => { - await setupPromptWithOptions(); - verify(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).once(); - verify(commandManager.executeCommand(Commands.LaunchTensorBoard, anything(), anything())).once(); - }); - - test('Disable prompt if user selects "Do not show again"', async () => { - await setupPromptWithOptions(true, Common.doNotShowAgain); - verify(persistentState.updateValue(false)).once(); - }); - - test('Do not show prompt if user has previously disabled prompt', async () => { - await setupPromptWithOptions(false); - verify(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).never(); - verify(commandManager.executeCommand(Commands.LaunchTensorBoard, anything(), anything())).never(); - }); - - test('Do not show prompt more than once per session', async () => { - await setupPromptWithOptions(); - verify(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).once(); - await prompt.showNativeTensorBoardPrompt(TensorBoardEntrypointTrigger.palette); - verify(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).once(); - }); -}); diff --git a/src/test/tensorBoard/tensorBoardSession.test.ts b/src/test/tensorBoard/tensorBoardSession.test.ts deleted file mode 100644 index 626740f4f530..000000000000 --- a/src/test/tensorBoard/tensorBoardSession.test.ts +++ /dev/null @@ -1,510 +0,0 @@ -import * as path from 'path'; -import { assert } from 'chai'; -import Sinon, * as sinon from 'sinon'; -import { SemVer } from 'semver'; -import { Uri, ViewColumn, window, workspace, WorkspaceConfiguration } from 'vscode'; -import { - IExperimentService, - IInstaller, - InstallerResponse, - Product, - ProductInstallStatus, -} from '../../client/common/types'; -import { Common, TensorBoard } from '../../client/common/utils/localize'; -import { IApplicationShell, ICommandManager } from '../../client/common/application/types'; -import { IServiceManager } from '../../client/ioc/types'; -import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from '../../client/tensorBoard/constants'; -import { TensorBoardSession } from '../../client/tensorBoard/tensorBoardSession'; -import { closeActiveWindows, EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../initialize'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { Architecture } from '../../client/common/utils/platform'; -import { PythonEnvironment, EnvironmentType } from '../../client/pythonEnvironments/info'; -import { PYTHON_PATH } from '../common'; -import { ImportTracker } from '../../client/telemetry/importTracker'; -import { IMultiStepInput, IMultiStepInputFactory } from '../../client/common/utils/multiStepInput'; -import { ModuleInstallFlags } from '../../client/common/installer/types'; - -// Class methods exposed just for testing purposes -interface ITensorBoardSessionTestAPI { - jumpToSource(fsPath: string, line: number): Promise; -} - -const info: PythonEnvironment = { - architecture: Architecture.Unknown, - companyDisplayName: '', - displayName: '', - envName: '', - path: '', - envType: EnvironmentType.Unknown, - version: new SemVer('0.0.0-alpha'), - sysPrefix: '', - sysVersion: '', -}; - -const interpreter: PythonEnvironment = { - ...info, - envType: EnvironmentType.Unknown, - path: PYTHON_PATH, -}; - -suite('TensorBoard session creation', async () => { - let serviceManager: IServiceManager; - let errorMessageStub: Sinon.SinonStub; - let sandbox: Sinon.SinonSandbox; - let applicationShell: IApplicationShell; - let commandManager: ICommandManager; - let experimentService: IExperimentService; - let installer: IInstaller; - let initialValue: string | undefined; - let workspaceConfiguration: WorkspaceConfiguration; - - suiteSetup(function () { - if (process.env.CI_PYTHON_VERSION === '2.7') { - // TensorBoard 2.4.1 not available for Python 2.7 - this.skip(); - } - - // See: https://github.com/microsoft/vscode-python/issues/18130 - this.skip(); - }); - - setup(async () => { - sandbox = sinon.createSandbox(); - ({ serviceManager } = await initialize()); - - experimentService = serviceManager.get(IExperimentService); - const interpreterService = serviceManager.get(IInterpreterService); - sandbox.stub(interpreterService, 'getActiveInterpreter').resolves(interpreter); - - applicationShell = serviceManager.get(IApplicationShell); - commandManager = serviceManager.get(ICommandManager); - installer = serviceManager.get(IInstaller); - workspaceConfiguration = workspace.getConfiguration('python.tensorBoard'); - initialValue = workspaceConfiguration.get('logDirectory'); - await workspaceConfiguration.update('logDirectory', undefined, true); - }); - - teardown(async () => { - await workspaceConfiguration.update('logDirectory', initialValue, true); - await closeActiveWindows(); - sandbox.restore(); - }); - - function configureStubs( - hasTorchImports: boolean, - tensorBoardInstallStatus: ProductInstallStatus, - torchProfilerPackageInstallStatus: ProductInstallStatus, - installPromptSelection: 'Yes' | 'No', - ) { - sandbox.stub(ImportTracker, 'hasModuleImport').withArgs('torch').returns(hasTorchImports); - const isProductVersionCompatible = sandbox.stub(installer, 'isProductVersionCompatible'); - isProductVersionCompatible - .withArgs(Product.tensorboard, '>= 2.4.1', interpreter) - .resolves(tensorBoardInstallStatus); - isProductVersionCompatible - .withArgs(Product.torchProfilerImportName, '>= 0.2.0', interpreter) - .resolves(torchProfilerPackageInstallStatus); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - errorMessageStub.resolves(installPromptSelection); - } - async function createSession() { - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - // Stub user selections - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - - assert.ok(session.panel?.viewColumn === ViewColumn.One, 'Panel opened in wrong group'); - assert.ok(session.panel?.visible, 'Webview panel not shown on session creation golden path'); - assert.ok(errorMessageStub.notCalled, 'Error message shown on session creation golden path'); - return session; - } - suite('Core functionality', async () => { - test('Golden path: TensorBoard session starts successfully and webview is shown', async () => { - await createSession(); - }); - test('When webview is closed, session is killed', async () => { - const session = await createSession(); - const { daemon, panel } = session; - assert.ok(panel?.visible, 'Webview panel not shown'); - panel?.dispose(); - assert.ok(session.panel === undefined, 'Webview still visible'); - assert.ok(daemon?.killed, 'TensorBoard session process not killed after webview closed'); - }); - test('When user selects file picker, display file picker', async () => { - // Stub user selections - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.selectAnotherFolder }); - const filePickerStub = sandbox.stub(applicationShell, 'showOpenDialog'); - - // Create session - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(filePickerStub.called, 'User requests to select another folder and file picker was not shown'); - }); - test('When user selects remote URL, display input box', async () => { - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.enterRemoteUrl }); - const inputBoxStub = sandbox.stub(applicationShell, 'showInputBox'); - - // Create session - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok( - inputBoxStub.called, - 'User requested to enter remote URL and input box to enter URL was not shown', - ); - }); - }); - suite('Installation prompt message', async () => { - async function createSessionAndVerifyMessage(message: string) { - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - assert.ok( - errorMessageStub.calledOnceWith(message, Common.bannerLabelYes, Common.bannerLabelNo), - 'Wrong error message shown', - ); - } - suite('Install profiler package + upgrade tensorboard', async () => { - async function runTest(expectTensorBoardUpgrade: boolean) { - const installStub = sandbox.stub(installer, 'install').resolves(InstallerResponse.Installed); - await createSessionAndVerifyMessage(TensorBoard.installTensorBoardAndProfilerPluginPrompt); - assert.ok(installStub.calledTwice, `Expected 2 installs but got ${installStub.callCount} calls`); - assert.ok(installStub.calledWith(Product.torchProfilerInstallName)); - assert.ok( - installStub.calledWith( - Product.tensorboard, - sinon.match.any, - sinon.match.any, - expectTensorBoardUpgrade ? ModuleInstallFlags.upgrade : undefined, - ), - ); - } - test('Has torch imports: true, is profiler package installed: false, TensorBoard needs upgrade', async () => { - configureStubs(true, ProductInstallStatus.NeedsUpgrade, ProductInstallStatus.NotInstalled, 'Yes'); - await runTest(true); - }); - test('Has torch imports: true, is profiler package installed: false, TensorBoard not installed', async () => { - configureStubs(true, ProductInstallStatus.NotInstalled, ProductInstallStatus.NotInstalled, 'Yes'); - await runTest(false); - }); - }); - suite('Install profiler only', async () => { - test('Has torch imports: true, is profiler package installed: false, TensorBoard installed', async () => { - configureStubs(true, ProductInstallStatus.Installed, ProductInstallStatus.NotInstalled, 'Yes'); - sandbox - .stub(applicationShell, 'showQuickPick') - .resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - // Ensure we ask to install the profiler package and that it resolves to a cancellation - sandbox - .stub(installer, 'install') - .withArgs(Product.torchProfilerInstallName, sinon.match.any, sinon.match.any) - .resolves(InstallerResponse.Ignore); - - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - - assert.ok(session.panel?.visible, 'Webview panel not shown, expected successful session creation'); - assert.ok( - errorMessageStub.calledOnceWith( - TensorBoard.installProfilerPluginPrompt, - Common.bannerLabelYes, - Common.bannerLabelNo, - ), - 'Wrong error message shown', - ); - }); - }); - suite('Install tensorboard only', async () => { - [false, true].forEach(async (hasTorchImports) => { - [ - ProductInstallStatus.Installed, - ProductInstallStatus.NotInstalled, - ProductInstallStatus.NeedsUpgrade, - ].forEach(async (torchProfilerInstallStatus) => { - const isTorchProfilerPackageInstalled = - torchProfilerInstallStatus === ProductInstallStatus.Installed; - if (!(hasTorchImports && !isTorchProfilerPackageInstalled)) { - test(`Has torch imports: ${hasTorchImports}, is profiler package installed: ${isTorchProfilerPackageInstalled}, TensorBoard not installed`, async () => { - configureStubs( - hasTorchImports, - ProductInstallStatus.NotInstalled, - torchProfilerInstallStatus, - 'No', - ); - await createSessionAndVerifyMessage(TensorBoard.installPrompt); - }); - } - }); - }); - }); - suite('Upgrade tensorboard only', async () => { - async function runTest() { - const installStub = sandbox.stub(installer, 'install').resolves(InstallerResponse.Installed); - await createSessionAndVerifyMessage(TensorBoard.upgradePrompt); - - assert.ok(installStub.calledOnce, `Expected 1 install but got ${installStub.callCount} installs`); - assert.ok(installStub.args[0][0] === Product.tensorboard, 'Did not install tensorboard'); - assert.ok( - installStub.args.filter((argsList) => argsList[0] === Product.torchProfilerInstallName).length === - 0, - 'Unexpected attempt to install profiler package', - ); - } - - [false, true].forEach(async (hasTorchImports) => { - [ - ProductInstallStatus.Installed, - ProductInstallStatus.NotInstalled, - ProductInstallStatus.NeedsUpgrade, - ].forEach(async (torchProfilerInstallStatus) => { - const isTorchProfilerPackageInstalled = - torchProfilerInstallStatus === ProductInstallStatus.Installed; - if (!(hasTorchImports && !isTorchProfilerPackageInstalled)) { - test(`Has torch imports: ${hasTorchImports}, is profiler package installed: ${isTorchProfilerPackageInstalled}, TensorBoard needs upgrade`, async () => { - configureStubs( - hasTorchImports, - ProductInstallStatus.NeedsUpgrade, - torchProfilerInstallStatus, - 'Yes', - ); - await runTest(); - }); - } - }); - }); - }); - suite('No prompt', async () => { - async function runTest() { - sandbox - .stub(applicationShell, 'showQuickPick') - .resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - assert.ok(errorMessageStub.notCalled, 'Prompt was unexpectedly shown'); - } - - [false, true].forEach(async (hasTorchImports) => { - [ - ProductInstallStatus.Installed, - ProductInstallStatus.NotInstalled, - ProductInstallStatus.NeedsUpgrade, - ].forEach(async (torchProfilerInstallStatus) => { - const isTorchProfilerPackageInstalled = - torchProfilerInstallStatus === ProductInstallStatus.Installed; - if (!(hasTorchImports && !isTorchProfilerPackageInstalled)) { - test(`Has torch imports: ${hasTorchImports}, is profiler package installed: ${isTorchProfilerPackageInstalled}, TensorBoard installed`, async () => { - configureStubs( - hasTorchImports, - ProductInstallStatus.Installed, - torchProfilerInstallStatus, - 'Yes', - ); - await runTest(); - }); - } - }); - }); - }); - }); - suite('Error messages', async () => { - test('If user cancels starting TensorBoard session, do not show error', async () => { - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - sandbox.stub(applicationShell, 'withProgress').resolves('canceled'); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(errorMessageStub.notCalled, 'User canceled session start and error was shown'); - }); - test('If existing install of TensorBoard is outdated and user cancels installation, do not show error', async () => { - sandbox.stub(experimentService, 'inExperiment').resolves(true); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - sandbox.stub(installer, 'isProductVersionCompatible').resolves(ProductInstallStatus.NeedsUpgrade); - sandbox.stub(installer, 'install').resolves(InstallerResponse.Ignore); - const quickPickStub = sandbox.stub(applicationShell, 'showQuickPick'); - - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(quickPickStub.notCalled, 'User opted not to upgrade and we proceeded to create session'); - }); - test('If TensorBoard is not installed and user chooses not to install, do not show error', async () => { - configureStubs(true, ProductInstallStatus.NotInstalled, ProductInstallStatus.NotInstalled, 'Yes'); - sandbox.stub(installer, 'install').resolves(InstallerResponse.Ignore); - - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok( - errorMessageStub.calledOnceWith( - TensorBoard.installTensorBoardAndProfilerPluginPrompt, - Common.bannerLabelYes, - Common.bannerLabelNo, - ), - 'User opted not to install and error was shown', - ); - }); - test('If user does not select a logdir, do not show error', async () => { - sandbox.stub(experimentService, 'inExperiment').resolves(true); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - // Stub user selections - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.selectAFolder }); - sandbox.stub(applicationShell, 'showOpenDialog').resolves(undefined); - - // Create session - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(errorMessageStub.notCalled, 'User opted not to select a logdir and error was shown'); - }); - test('If starting TensorBoard times out, show error', async () => { - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - sandbox.stub(applicationShell, 'withProgress').resolves(60_000); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(errorMessageStub.called, 'TensorBoard timed out but no error was shown'); - }); - test('If installing the profiler package fails, do not show error, continue to create session', async () => { - configureStubs(true, ProductInstallStatus.Installed, ProductInstallStatus.NotInstalled, 'Yes'); - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - // Ensure we ask to install the profiler package and that it resolves to a cancellation - sandbox - .stub(installer, 'install') - .withArgs(Product.torchProfilerInstallName, sinon.match.any, sinon.match.any) - .resolves(InstallerResponse.Ignore); - - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - - assert.ok(session.panel?.visible, 'Webview panel not shown, expected successful session creation'); - }); - test('If user opts not to install profiler package and tensorboard is already installed, continue to create session', async () => { - configureStubs(true, ProductInstallStatus.Installed, ProductInstallStatus.NotInstalled, 'No'); - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - assert.ok(session.panel?.visible, 'Webview panel not shown, expected successful session creation'); - }); - }); - test('If python.tensorBoard.logDirectory is provided, do not prompt user to pick a log directory', async () => { - const selectDirectoryStub = sandbox - .stub(applicationShell, 'showQuickPick') - .resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - await workspaceConfiguration.update('logDirectory', 'logs/fit', true); - - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - - assert.ok(session.panel?.visible, 'Expected successful session creation but webpanel not shown'); - assert.ok(errorMessageStub.notCalled, 'Expected successful session creation but error message was shown'); - assert.ok( - selectDirectoryStub.notCalled, - 'Prompted user to select log directory although setting was specified', - ); - }); - suite('Jump to source', async () => { - // We can't test a full E2E scenario with the TB profiler plugin because we can't - // accurately target simulated clicks at iframed content. This only tests - // code from the moment that the VS Code webview posts a message back - // to the extension. - const fsPath = path.join( - EXTENSION_ROOT_DIR_FOR_TESTS, - 'src', - 'test', - 'python_files', - 'tensorBoard', - 'sourcefile.py', - ); - teardown(() => { - sandbox.restore(); - }); - function setupStubsForMultiStepInput() { - // Stub the factory to return our stubbed multistep input when it's asked to create one - const multiStepFactory = serviceManager.get(IMultiStepInputFactory); - const inputInstance = multiStepFactory.create(); - // Create a multistep input with stubs for methods - const showQuickPickStub = sandbox.stub(inputInstance, 'showQuickPick').resolves({ - label: TensorBoard.selectMissingSourceFile, - description: TensorBoard.selectMissingSourceFileDescription, - }); - const createInputStub = sandbox - .stub(multiStepFactory, 'create') - .returns(inputInstance as IMultiStepInput); - // Stub the system file picker - const filePickerStub = sandbox.stub(applicationShell, 'showOpenDialog').resolves([Uri.file(fsPath)]); - return [showQuickPickStub, createInputStub, filePickerStub]; - } - test('Resolves filepaths without displaying prompt', async () => { - const session = ((await createSession()) as unknown) as ITensorBoardSessionTestAPI; - const stubs = setupStubsForMultiStepInput(); - await session.jumpToSource(fsPath, 0); - assert.ok(window.activeTextEditor !== undefined, 'Source file not resolved'); - assert.ok(window.activeTextEditor?.document.uri.fsPath === fsPath, 'Wrong source file opened'); - assert.ok( - stubs.reduce((prev, current) => current.notCalled && prev, true), - 'Stubs were called when file is present', - ); - }); - test('Display quickpick to user if filepath is not on disk', async () => { - const session = ((await createSession()) as unknown) as ITensorBoardSessionTestAPI; - const stubs = setupStubsForMultiStepInput(); - await session.jumpToSource('/nonexistent/file/path.py', 0); - assert.ok(window.activeTextEditor !== undefined, 'Source file not resolved'); - assert.ok(window.activeTextEditor?.document.uri.fsPath === fsPath, 'Wrong source file opened'); - assert.ok( - stubs.reduce((prev, current) => current.calledOnce && prev, true), - 'Stubs called an unexpected number of times', - ); - }); - }); -}); diff --git a/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts b/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts deleted file mode 100644 index 7eba1805c8bf..000000000000 --- a/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import { anything, instance, mock, reset, when } from 'ts-mockito'; -import { TensorBoardUsageTracker } from '../../client/tensorBoard/tensorBoardUsageTracker'; -import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; -import { MockDocumentManager } from '../mocks/mockDocumentManager'; -import { createTensorBoardPromptWithMocks } from './helpers'; -import { mockedVSCodeNamespaces } from '../vscode-mock'; -import { TensorboardExperiment } from '../../client/tensorBoard/tensorboarExperiment'; - -[true, false].forEach((tbExtensionInstalled) => { - suite(`Tensorboard Extension is ${tbExtensionInstalled ? 'installed' : 'not installed'}`, () => { - suite('TensorBoard usage tracker', () => { - let experiment: TensorboardExperiment; - let documentManager: MockDocumentManager; - let tensorBoardImportTracker: TensorBoardUsageTracker; - let prompt: TensorBoardPrompt; - let showNativeTensorBoardPrompt: sinon.SinonSpy; - - suiteSetup(() => { - reset(mockedVSCodeNamespaces.extensions); - when(mockedVSCodeNamespaces.extensions?.getExtension(anything())).thenReturn(undefined); - }); - suiteTeardown(() => reset(mockedVSCodeNamespaces.extensions)); - setup(() => { - sinon.stub(TensorboardExperiment, 'isTensorboardExtensionInstalled').returns(tbExtensionInstalled); - experiment = mock(); - documentManager = new MockDocumentManager(); - prompt = createTensorBoardPromptWithMocks(); - showNativeTensorBoardPrompt = sinon.spy(prompt, 'showNativeTensorBoardPrompt'); - tensorBoardImportTracker = new TensorBoardUsageTracker( - documentManager, - [], - prompt, - instance(experiment), - ); - }); - - test('Simple tensorboard import in Python file', async () => { - const document = documentManager.addDocument('import tensorboard', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Simple tensorboardX import in Python file', async () => { - const document = documentManager.addDocument('import tensorboardX', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Simple tensorboard import in Python ipynb', async () => { - const document = documentManager.addDocument('import tensorboard', 'foo.ipynb'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from x.y.tensorboard import z` import', async () => { - const document = documentManager.addDocument( - 'from torch.utils.tensorboard import SummaryWriter', - 'foo.py', - ); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from x.y import tensorboard` import', async () => { - const document = documentManager.addDocument('from torch.utils import tensorboard', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from tensorboardX import x` import', async () => { - const document = documentManager.addDocument('from tensorboardX import SummaryWriter', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`import x, y` import', async () => { - const document = documentManager.addDocument('import tensorboard, tensorflow', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`import pkg as _` import', async () => { - const document = documentManager.addDocument('import tensorboard as tb', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Show prompt on changed text editor', async () => { - await tensorBoardImportTracker.activate(); - const document = documentManager.addDocument('import tensorboard as tb', 'foo.py'); - await documentManager.showTextDocument(document); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Do not show prompt if no tensorboard import', async () => { - const document = documentManager.addDocument( - 'import tensorflow as tf\nfrom torch.utils import foo', - 'foo.py', - ); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.notCalled); - }); - test('Do not show prompt if language is not Python', async () => { - const document = documentManager.addDocument( - 'import tensorflow as tf\nfrom torch.utils import foo', - 'foo.cpp', - 'cpp', - ); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.notCalled); - }); - }); - }); -}); From 3ccf984f1dc4e849263d749c78ffeb9ab0d72118 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 6 Jan 2025 11:30:00 -0800 Subject: [PATCH 0846/1136] Remove fifo regression (#24685) fixes regression in https://github.com/microsoft/vscode-python/issues/24656 by reverting problem --- noxfile.py | 1 - python_files/testing_tools/socket_manager.py | 56 +++-- python_files/tests/pytestadapter/helpers.py | 33 +-- python_files/unittestadapter/pvsc_utils.py | 11 +- python_files/vscode_pytest/__init__.py | 43 ++-- python_files/vscode_pytest/_common.py | 2 - src/client/common/pipes/namedPipes.ts | 231 +++++------------- .../testing/testController/common/utils.ts | 140 ++++++----- .../pytest/pytestDiscoveryAdapter.ts | 9 +- .../pytest/pytestExecutionAdapter.ts | 29 ++- .../unittest/testDiscoveryAdapter.ts | 4 +- .../unittest/testExecutionAdapter.ts | 24 +- .../testing/common/testingAdapter.test.ts | 87 ++++++- .../pytestDiscoveryAdapter.unit.test.ts | 9 +- .../pytestExecutionAdapter.unit.test.ts | 9 +- .../testCancellationRunAdapters.unit.test.ts | 31 +-- .../testDiscoveryAdapter.unit.test.ts | 9 +- .../testExecutionAdapter.unit.test.ts | 9 +- .../errorWorkspace/test_seg_fault.py | 3 +- 19 files changed, 376 insertions(+), 364 deletions(-) delete mode 100644 python_files/vscode_pytest/_common.py diff --git a/noxfile.py b/noxfile.py index 3991ee8c025a..60e22d461074 100644 --- a/noxfile.py +++ b/noxfile.py @@ -53,7 +53,6 @@ def install_python_libs(session: nox.Session): ) session.install("packaging") - session.install("debugpy") # Download get-pip script session.run( diff --git a/python_files/testing_tools/socket_manager.py b/python_files/testing_tools/socket_manager.py index f143ac111cdb..347453a6ca1a 100644 --- a/python_files/testing_tools/socket_manager.py +++ b/python_files/testing_tools/socket_manager.py @@ -20,24 +20,39 @@ def __exit__(self, *_): self.close() def connect(self): - self._writer = open(self.name, "w", encoding="utf-8") # noqa: SIM115, PTH123 - # reader created in read method + if sys.platform == "win32": + self._writer = open(self.name, "w", encoding="utf-8") # noqa: SIM115, PTH123 + # reader created in read method + else: + self._socket = _SOCKET(socket.AF_UNIX, socket.SOCK_STREAM) + self._socket.connect(self.name) return self def close(self): - self._writer.close() - if hasattr(self, "_reader"): - self._reader.close() + if sys.platform == "win32": + self._writer.close() + else: + # add exception catch + self._socket.close() def write(self, data: str): - try: - # for windows, is should only use \n\n - request = f"""content-length: {len(data)}\ncontent-type: application/json\n\n{data}""" - self._writer.write(request) - self._writer.flush() - except Exception as e: - print("error attempting to write to pipe", e) - raise (e) + if sys.platform == "win32": + try: + # for windows, is should only use \n\n + request = ( + f"""content-length: {len(data)}\ncontent-type: application/json\n\n{data}""" + ) + self._writer.write(request) + self._writer.flush() + except Exception as e: + print("error attempting to write to pipe", e) + raise (e) + else: + # must include the carriage-return defined (as \r\n) for unix systems + request = ( + f"""content-length: {len(data)}\r\ncontent-type: application/json\r\n\r\n{data}""" + ) + self._socket.send(request.encode("utf-8")) def read(self, bufsize=1024) -> str: """Read data from the socket. @@ -48,10 +63,17 @@ def read(self, bufsize=1024) -> str: Returns: data (str): Data received from the socket. """ - # returns a string automatically from read - if not hasattr(self, "_reader"): - self._reader = open(self.name, encoding="utf-8") # noqa: SIM115, PTH123 - return self._reader.read(bufsize) + if sys.platform == "win32": + # returns a string automatically from read + if not hasattr(self, "_reader"): + self._reader = open(self.name, encoding="utf-8") # noqa: SIM115, PTH123 + return self._reader.read(bufsize) + else: + # receive bytes and convert to string + while True: + part: bytes = self._socket.recv(bufsize) + data: str = part.decode("utf-8") + return data class SocketManager: diff --git a/python_files/tests/pytestadapter/helpers.py b/python_files/tests/pytestadapter/helpers.py index 7a75e6248844..7972eedd0919 100644 --- a/python_files/tests/pytestadapter/helpers.py +++ b/python_files/tests/pytestadapter/helpers.py @@ -128,22 +128,6 @@ def parse_rpc_message(data: str) -> Tuple[Dict[str, str], str]: print("json decode error") -def _listen_on_fifo(pipe_name: str, result: List[str], completed: threading.Event): - # Open the FIFO for reading - fifo_path = pathlib.Path(pipe_name) - with fifo_path.open() as fifo: - print("Waiting for data...") - while True: - if completed.is_set(): - break # Exit loop if completed event is set - data = fifo.read() # This will block until data is available - if len(data) == 0: - # If data is empty, assume EOF - break - print(f"Received: {data}") - result.append(data) - - def _listen_on_pipe_new(listener, result: List[str], completed: threading.Event): """Listen on the named pipe or Unix domain socket for JSON data from the server. @@ -323,19 +307,14 @@ def runner_with_cwd_env( # if additional environment variables are passed, add them to the environment if env_add: env.update(env_add) - # server = UnixPipeServer(pipe_name) - # server.start() - ################# - # Create the FIFO (named pipe) if it doesn't exist - # if not pathlib.Path.exists(pipe_name): - os.mkfifo(pipe_name) - ################# + server = UnixPipeServer(pipe_name) + server.start() completed = threading.Event() result = [] # result is a string array to store the data during threading t1: threading.Thread = threading.Thread( - target=_listen_on_fifo, args=(pipe_name, result, completed) + target=_listen_on_pipe_new, args=(server, result, completed) ) t1.start() @@ -385,14 +364,14 @@ def generate_random_pipe_name(prefix=""): # For Windows, named pipes have a specific naming convention. if sys.platform == "win32": - return f"\\\\.\\pipe\\{prefix}-{random_suffix}" + return f"\\\\.\\pipe\\{prefix}-{random_suffix}-sock" # For Unix-like systems, use either the XDG_RUNTIME_DIR or a temporary directory. xdg_runtime_dir = os.getenv("XDG_RUNTIME_DIR") if xdg_runtime_dir: - return os.path.join(xdg_runtime_dir, f"{prefix}-{random_suffix}") # noqa: PTH118 + return os.path.join(xdg_runtime_dir, f"{prefix}-{random_suffix}.sock") # noqa: PTH118 else: - return os.path.join(tempfile.gettempdir(), f"{prefix}-{random_suffix}") # noqa: PTH118 + return os.path.join(tempfile.gettempdir(), f"{prefix}-{random_suffix}.sock") # noqa: PTH118 class UnixPipeServer: diff --git a/python_files/unittestadapter/pvsc_utils.py b/python_files/unittestadapter/pvsc_utils.py index cba3a2d1f59d..09e61ff40518 100644 --- a/python_files/unittestadapter/pvsc_utils.py +++ b/python_files/unittestadapter/pvsc_utils.py @@ -18,6 +18,8 @@ from typing_extensions import NotRequired # noqa: E402 +from testing_tools import socket_manager # noqa: E402 + # Types @@ -329,10 +331,10 @@ def send_post_request( if __writer is None: try: - __writer = open(test_run_pipe, "w", encoding="utf-8", newline="\r\n") # noqa: SIM115, PTH123 + __writer = socket_manager.PipeManager(test_run_pipe) + __writer.connect() except Exception as error: error_msg = f"Error attempting to connect to extension named pipe {test_run_pipe}[vscode-unittest]: {error}" - print(error_msg, file=sys.stderr) __writer = None raise VSCodeUnittestError(error_msg) from error @@ -341,11 +343,10 @@ def send_post_request( "params": payload, } data = json.dumps(rpc) + try: if __writer: - request = f"""content-length: {len(data)}\ncontent-type: application/json\n\n{data}""" - __writer.write(request) - __writer.flush() + __writer.write(data) else: print( f"Connection error[vscode-unittest], writer is None \n[vscode-unittest] data: \n{data} \n", diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 561f6e432341..8a54a7249d71 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -21,6 +21,11 @@ import pytest +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) +from testing_tools import socket_manager # noqa: E402 + if TYPE_CHECKING: from pluggy import Result @@ -166,7 +171,7 @@ def pytest_exception_interact(node, call, report): collected_test = TestRunResultDict() collected_test[node_id] = item_result cwd = pathlib.Path.cwd() - send_execution_message( + execution_post( os.fsdecode(cwd), "success", collected_test if collected_test else None, @@ -290,7 +295,7 @@ def pytest_report_teststatus(report, config): # noqa: ARG001 ) collected_test = TestRunResultDict() collected_test[absolute_node_id] = item_result - send_execution_message( + execution_post( os.fsdecode(cwd), "success", collected_test if collected_test else None, @@ -324,7 +329,7 @@ def pytest_runtest_protocol(item, nextitem): # noqa: ARG001 ) collected_test = TestRunResultDict() collected_test[absolute_node_id] = item_result - send_execution_message( + execution_post( os.fsdecode(cwd), "success", collected_test if collected_test else None, @@ -400,7 +405,7 @@ def pytest_sessionfinish(session, exitstatus): "children": [], "id_": "", } - send_discovery_message(os.fsdecode(cwd), error_node) + post_response(os.fsdecode(cwd), error_node) try: session_node: TestNode | None = build_test_tree(session) if not session_node: @@ -408,7 +413,7 @@ def pytest_sessionfinish(session, exitstatus): "Something went wrong following pytest finish, \ no session node was created" ) - send_discovery_message(os.fsdecode(cwd), session_node) + post_response(os.fsdecode(cwd), session_node) except Exception as e: ERRORS.append( f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" @@ -420,7 +425,7 @@ def pytest_sessionfinish(session, exitstatus): "children": [], "id_": "", } - send_discovery_message(os.fsdecode(cwd), error_node) + post_response(os.fsdecode(cwd), error_node) else: if exitstatus == 0 or exitstatus == 1: exitstatus_bool = "success" @@ -430,7 +435,7 @@ def pytest_sessionfinish(session, exitstatus): ) exitstatus_bool = "error" - send_execution_message( + execution_post( os.fsdecode(cwd), exitstatus_bool, None, @@ -484,7 +489,7 @@ def pytest_sessionfinish(session, exitstatus): result=file_coverage_map, error=None, ) - send_message(payload) + send_post_request(payload) def build_test_tree(session: pytest.Session) -> TestNode: @@ -852,10 +857,8 @@ def get_node_path(node: Any) -> pathlib.Path: atexit.register(lambda: __writer.close() if __writer else None) -def send_execution_message( - cwd: str, status: Literal["success", "error"], tests: TestRunResultDict | None -): - """Sends message execution payload details. +def execution_post(cwd: str, status: Literal["success", "error"], tests: TestRunResultDict | None): + """Sends a POST request with execution payload details. Args: cwd (str): Current working directory. @@ -867,10 +870,10 @@ def send_execution_message( ) if ERRORS: payload["error"] = ERRORS - send_message(payload) + send_post_request(payload) -def send_discovery_message(cwd: str, session_node: TestNode) -> None: +def post_response(cwd: str, session_node: TestNode) -> None: """ Sends a POST request with test session details in payload. @@ -886,7 +889,7 @@ def send_discovery_message(cwd: str, session_node: TestNode) -> None: } if ERRORS is not None: payload["error"] = ERRORS - send_message(payload, cls_encoder=PathEncoder) + send_post_request(payload, cls_encoder=PathEncoder) class PathEncoder(json.JSONEncoder): @@ -898,7 +901,7 @@ def default(self, o): return super().default(o) -def send_message( +def send_post_request( payload: ExecutionPayloadDict | DiscoveryPayloadDict | CoveragePayloadDict, cls_encoder=None, ): @@ -923,7 +926,8 @@ def send_message( if __writer is None: try: - __writer = open(TEST_RUN_PIPE, "w", encoding="utf-8", newline="\r\n") # noqa: SIM115, PTH123 + __writer = socket_manager.PipeManager(TEST_RUN_PIPE) + __writer.connect() except Exception as error: error_msg = f"Error attempting to connect to extension named pipe {TEST_RUN_PIPE}[vscode-pytest]: {error}" print(error_msg, file=sys.stderr) @@ -941,11 +945,10 @@ def send_message( "params": payload, } data = json.dumps(rpc, cls=cls_encoder) + try: if __writer: - request = f"""content-length: {len(data)}\ncontent-type: application/json\n\n{data}""" - __writer.write(request) - __writer.flush() + __writer.write(data) else: print( f"Plugin error connection error[vscode-pytest], writer is None \n[vscode-pytest] data: \n{data} \n", diff --git a/python_files/vscode_pytest/_common.py b/python_files/vscode_pytest/_common.py deleted file mode 100644 index 9f835f555b6e..000000000000 --- a/python_files/vscode_pytest/_common.py +++ /dev/null @@ -1,2 +0,0 @@ -# def send_post_request(): -# return diff --git a/src/client/common/pipes/namedPipes.ts b/src/client/common/pipes/namedPipes.ts index 8cccd4cdcfed..c6010d491822 100644 --- a/src/client/common/pipes/namedPipes.ts +++ b/src/client/common/pipes/namedPipes.ts @@ -1,18 +1,67 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as cp from 'child_process'; import * as crypto from 'crypto'; -import * as fs from 'fs-extra'; import * as net from 'net'; import * as os from 'os'; import * as path from 'path'; import * as rpc from 'vscode-jsonrpc/node'; -import { CancellationError, CancellationToken, Disposable } from 'vscode'; import { traceVerbose } from '../../logging'; -import { isWindows } from '../utils/platform'; -import { createDeferred } from '../utils/async'; -import { noop } from '../utils/misc'; + +export interface ConnectedServerObj { + serverOnClosePromise(): Promise; +} + +export function createNamedPipeServer( + pipeName: string, + onConnectionCallback: (value: [rpc.MessageReader, rpc.MessageWriter]) => void, +): Promise { + traceVerbose(`Creating named pipe server on ${pipeName}`); + + let connectionCount = 0; + return new Promise((resolve, reject) => { + // create a server, resolves and returns server on listen + const server = net.createServer((socket) => { + // this lambda function is called whenever a client connects to the server + connectionCount += 1; + traceVerbose('new client is connected to the socket, connectionCount: ', connectionCount, pipeName); + socket.on('close', () => { + // close event is emitted by client to the server + connectionCount -= 1; + traceVerbose('client emitted close event, connectionCount: ', connectionCount); + if (connectionCount <= 0) { + // if all clients are closed, close the server + traceVerbose('connection count is <= 0, closing the server: ', pipeName); + server.close(); + } + }); + + // upon connection create a reader and writer and pass it to the callback + onConnectionCallback([ + new rpc.SocketMessageReader(socket, 'utf-8'), + new rpc.SocketMessageWriter(socket, 'utf-8'), + ]); + }); + const closedServerPromise = new Promise((resolveOnServerClose) => { + // get executed on connection close and resolves + // implementation of the promise is the arrow function + server.on('close', resolveOnServerClose); + }); + server.on('error', reject); + + server.listen(pipeName, () => { + // this function is called when the server is listening + server.removeListener('error', reject); + const connectedServer = { + // when onClosed event is called, so is closed function + // goes backwards up the chain, when resolve2 is called, so is onClosed that means server.onClosed() on the other end can work + // event C + serverOnClosePromise: () => closedServerPromise, + }; + resolve(connectedServer); + }); + }); +} const { XDG_RUNTIME_DIR } = process.env; export function generateRandomPipeName(prefix: string): string { @@ -23,178 +72,20 @@ export function generateRandomPipeName(prefix: string): string { } if (process.platform === 'win32') { - return `\\\\.\\pipe\\${prefix}-${randomSuffix}`; + return `\\\\.\\pipe\\${prefix}-${randomSuffix}-sock`; } let result; if (XDG_RUNTIME_DIR) { - result = path.join(XDG_RUNTIME_DIR, `${prefix}-${randomSuffix}`); + result = path.join(XDG_RUNTIME_DIR, `${prefix}-${randomSuffix}.sock`); } else { - result = path.join(os.tmpdir(), `${prefix}-${randomSuffix}`); + result = path.join(os.tmpdir(), `${prefix}-${randomSuffix}.sock`); } return result; } -async function mkfifo(fifoPath: string): Promise { - return new Promise((resolve, reject) => { - const proc = cp.spawn('mkfifo', [fifoPath]); - proc.on('error', (err) => { - reject(err); - }); - proc.on('exit', (code) => { - if (code === 0) { - resolve(); - } - }); - }); -} - -export async function createWriterPipe(pipeName: string, token?: CancellationToken): Promise { - // windows implementation of FIFO using named pipes - if (isWindows()) { - const deferred = createDeferred(); - const server = net.createServer((socket) => { - traceVerbose(`Pipe connected: ${pipeName}`); - server.close(); - deferred.resolve(new rpc.SocketMessageWriter(socket, 'utf-8')); - }); - - server.on('error', deferred.reject); - server.listen(pipeName); - if (token) { - token.onCancellationRequested(() => { - if (server.listening) { - server.close(); - } - deferred.reject(new CancellationError()); - }); - } - return deferred.promise; - } - // linux implementation of FIFO - await mkfifo(pipeName); - try { - await fs.chmod(pipeName, 0o666); - } catch { - // Intentionally ignored - } - const writer = fs.createWriteStream(pipeName, { - encoding: 'utf-8', - }); - return new rpc.StreamMessageWriter(writer, 'utf-8'); -} - -class CombinedReader implements rpc.MessageReader { - private _onError = new rpc.Emitter(); - - private _onClose = new rpc.Emitter(); - - private _onPartialMessage = new rpc.Emitter(); - - // eslint-disable-next-line @typescript-eslint/no-empty-function - private _callback: rpc.DataCallback = () => {}; - - private _disposables: rpc.Disposable[] = []; - - private _readers: rpc.MessageReader[] = []; - - constructor() { - this._disposables.push(this._onClose, this._onError, this._onPartialMessage); - } - - onError: rpc.Event = this._onError.event; - - onClose: rpc.Event = this._onClose.event; - - onPartialMessage: rpc.Event = this._onPartialMessage.event; - - listen(callback: rpc.DataCallback): rpc.Disposable { - this._callback = callback; - // eslint-disable-next-line no-return-assign, @typescript-eslint/no-empty-function - return new Disposable(() => (this._callback = () => {})); - } - - add(reader: rpc.MessageReader): void { - this._readers.push(reader); - reader.listen((msg) => { - this._callback(msg as rpc.NotificationMessage); - }); - this._disposables.push(reader); - reader.onClose(() => { - this.remove(reader); - if (this._readers.length === 0) { - this._onClose.fire(); - } - }); - reader.onError((e) => { - this.remove(reader); - this._onError.fire(e); - }); - } - - remove(reader: rpc.MessageReader): void { - const found = this._readers.find((r) => r === reader); - if (found) { - this._readers = this._readers.filter((r) => r !== reader); - reader.dispose(); - } - } - - dispose(): void { - this._readers.forEach((r) => r.dispose()); - this._readers = []; - this._disposables.forEach((disposable) => disposable.dispose()); - this._disposables = []; - } -} - -export async function createReaderPipe(pipeName: string, token?: CancellationToken): Promise { - if (isWindows()) { - // windows implementation of FIFO using named pipes - const deferred = createDeferred(); - const combined = new CombinedReader(); - - let refs = 0; - const server = net.createServer((socket) => { - traceVerbose(`Pipe connected: ${pipeName}`); - refs += 1; - - socket.on('close', () => { - refs -= 1; - if (refs <= 0) { - server.close(); - } - }); - combined.add(new rpc.SocketMessageReader(socket, 'utf-8')); - }); - server.on('error', deferred.reject); - server.listen(pipeName); - if (token) { - token.onCancellationRequested(() => { - if (server.listening) { - server.close(); - } - deferred.reject(new CancellationError()); - }); - } - deferred.resolve(combined); - return deferred.promise; - } - // mac/linux implementation of FIFO - await mkfifo(pipeName); - try { - await fs.chmod(pipeName, 0o666); - } catch { - // Intentionally ignored - } - const fd = await fs.open(pipeName, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK); - const socket = new net.Socket({ fd }); - const reader = new rpc.SocketMessageReader(socket, 'utf-8'); - socket.on('close', () => { - fs.close(fd).catch(noop); - reader.dispose(); - }); - - return reader; +export function namedPipeClient(name: string): [rpc.MessageReader, rpc.MessageWriter] { + const socket = net.connect(name); + return [new rpc.SocketMessageReader(socket, 'utf-8'), new rpc.SocketMessageWriter(socket, 'utf-8')]; } diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index b6848d0245dc..1d2be64f537d 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -16,7 +16,7 @@ import { ITestResultResolver, } from './types'; import { Deferred, createDeferred } from '../../../common/utils/async'; -import { createReaderPipe, generateRandomPipeName } from '../../../common/pipes/namedPipes'; +import { createNamedPipeServer, generateRandomPipeName } from '../../../common/pipes/namedPipes'; import { EXTENSION_ROOT_DIR } from '../../../constants'; export function fixLogLinesNoTrailing(content: string): string { @@ -34,6 +34,28 @@ export function createTestingDeferred(): Deferred { return createDeferred(); } +export async function startTestIdsNamedPipe(testIds: string[]): Promise { + const pipeName: string = generateRandomPipeName('python-test-ids'); + // uses callback so the on connect action occurs after the pipe is created + await createNamedPipeServer(pipeName, ([_reader, writer]) => { + traceVerbose('Test Ids named pipe connected'); + // const num = await + const msg = { + jsonrpc: '2.0', + params: testIds, + } as Message; + writer + .write(msg) + .then(() => { + writer.end(); + }) + .catch((ex) => { + traceError('Failed to write test ids to named pipe', ex); + }); + }); + return pipeName; +} + interface ExecutionResultMessage extends Message { params: ExecutionTestPayload; } @@ -72,47 +94,47 @@ export async function startRunResultNamedPipe( dataReceivedCallback: (payload: ExecutionTestPayload) => void, deferredTillServerClose: Deferred, cancellationToken?: CancellationToken, -): Promise { +): Promise<{ name: string } & Disposable> { traceVerbose('Starting Test Result named pipe'); const pipeName: string = generateRandomPipeName('python-test-results'); - - const reader = await createReaderPipe(pipeName, cancellationToken); - traceVerbose(`Test Results named pipe ${pipeName} connected`); - let disposables: Disposable[] = []; - const disposable = new Disposable(() => { - traceVerbose(`Test Results named pipe ${pipeName} disposed`); - disposables.forEach((d) => d.dispose()); - disposables = []; + let disposeOfServer: () => void = () => { deferredTillServerClose.resolve(); - }); + /* noop */ + }; + const server = await createNamedPipeServer(pipeName, ([reader, _writer]) => { + // this lambda function is: onConnectionCallback + // this is called once per client connecting to the server + traceVerbose(`Test Result named pipe ${pipeName} connected`); + let perConnectionDisposables: (Disposable | undefined)[] = [reader]; - if (cancellationToken) { - disposables.push( + // create a function to dispose of the server + disposeOfServer = () => { + // dispose of all data listeners and cancelation listeners + perConnectionDisposables.forEach((d) => d?.dispose()); + perConnectionDisposables = []; + deferredTillServerClose.resolve(); + }; + perConnectionDisposables.push( + // per connection, add a listener for the cancellation token and the data cancellationToken?.onCancellationRequested(() => { - traceLog(`Test Result named pipe ${pipeName} cancelled`); - disposable.dispose(); + console.log(`Test Result named pipe ${pipeName} cancelled`); + // if cancel is called on one connection, dispose of all connections + disposeOfServer(); + }), + reader.listen((data: Message) => { + traceVerbose(`Test Result named pipe ${pipeName} received data`); + dataReceivedCallback((data as ExecutionResultMessage).params as ExecutionTestPayload); }), ); - } - disposables.push( - reader, - reader.listen((data: Message) => { - traceVerbose(`Test Result named pipe ${pipeName} received data`); - // if EOT, call decrement connection count (callback) - dataReceivedCallback((data as ExecutionResultMessage).params as ExecutionTestPayload); - }), - reader.onClose(() => { + server.serverOnClosePromise().then(() => { // this is called once the server close, once per run instance traceVerbose(`Test Result named pipe ${pipeName} closed. Disposing of listener/s.`); // dispose of all data listeners and cancelation listeners - disposable.dispose(); - }), - reader.onError((error) => { - traceError(`Test Results named pipe ${pipeName} error:`, error); - }), - ); + disposeOfServer(); + }); + }); - return pipeName; + return { name: pipeName, dispose: disposeOfServer }; } interface DiscoveryResultMessage extends Message { @@ -122,44 +144,36 @@ interface DiscoveryResultMessage extends Message { export async function startDiscoveryNamedPipe( callback: (payload: DiscoveredTestPayload) => void, cancellationToken?: CancellationToken, -): Promise { +): Promise<{ name: string } & Disposable> { traceVerbose('Starting Test Discovery named pipe'); - // const pipeName: string = '/Users/eleanorboyd/testingFiles/inc_dec_example/temp33.txt'; const pipeName: string = generateRandomPipeName('python-test-discovery'); - const reader = await createReaderPipe(pipeName, cancellationToken); - - traceVerbose(`Test Discovery named pipe ${pipeName} connected`); - let disposables: Disposable[] = []; - const disposable = new Disposable(() => { - traceVerbose(`Test Discovery named pipe ${pipeName} disposed`); - disposables.forEach((d) => d.dispose()); - disposables = []; - }); - - if (cancellationToken) { + let dispose: () => void = () => { + /* noop */ + }; + await createNamedPipeServer(pipeName, ([reader, _writer]) => { + traceVerbose(`Test Discovery named pipe ${pipeName} connected`); + let disposables: (Disposable | undefined)[] = [reader]; + dispose = () => { + traceVerbose(`Test Discovery named pipe ${pipeName} disposed`); + disposables.forEach((d) => d?.dispose()); + disposables = []; + }; disposables.push( - cancellationToken.onCancellationRequested(() => { + cancellationToken?.onCancellationRequested(() => { traceVerbose(`Test Discovery named pipe ${pipeName} cancelled`); - disposable.dispose(); + dispose(); + }), + reader.listen((data: Message) => { + traceVerbose(`Test Discovery named pipe ${pipeName} received data`); + callback((data as DiscoveryResultMessage).params as DiscoveredTestPayload); + }), + reader.onClose(() => { + traceVerbose(`Test Discovery named pipe ${pipeName} closed`); + dispose(); }), ); - } - - disposables.push( - reader, - reader.listen((data: Message) => { - traceVerbose(`Test Discovery named pipe ${pipeName} received data`); - callback((data as DiscoveryResultMessage).params as DiscoveredTestPayload); - }), - reader.onClose(() => { - traceVerbose(`Test Discovery named pipe ${pipeName} closed`); - disposable.dispose(); - }), - reader.onError((error) => { - traceError(`Test Discovery named pipe ${pipeName} error:`, error); - }), - ); - return pipeName; + }); + return { name: pipeName, dispose }; } export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index ff73b31435a3..4baa76000d14 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -42,12 +42,15 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { executionFactory?: IPythonExecutionFactory, interpreter?: PythonEnvironment, ): Promise { - const name = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { + const { name, dispose } = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { this.resultResolver?.resolveDiscovery(data); }); - await this.runPytestDiscovery(uri, name, executionFactory, interpreter); - + try { + await this.runPytestDiscovery(uri, name, executionFactory, interpreter); + } finally { + dispose(); + } // this is only a placeholder to handle function overloading until rewrite is finished const discoveryPayload: DiscoveredTestPayload = { cwd: uri.fsPath, status: 'success' }; return discoveryPayload; diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index b408280a576e..b108bd876e5a 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationTokenSource, DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import { DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; import * as path from 'path'; import { ChildProcess } from 'child_process'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; @@ -49,16 +49,16 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceError(`No run instance found, cannot resolve execution, for workspace ${uri.fsPath}.`); } }; - const cSource = new CancellationTokenSource(); - runInstance?.token.onCancellationRequested(() => cSource.cancel()); - - const name = await utils.startRunResultNamedPipe( + const { name, dispose: serverDispose } = await utils.startRunResultNamedPipe( dataReceivedCallback, // callback to handle data received deferredTillServerClose, // deferred to resolve when server closes - cSource.token, // token to cancel + runInstance?.token, // token to cancel ); runInstance?.token.onCancellationRequested(() => { traceInfo(`Test run cancelled, resolving 'TillServerClose' deferred for ${uri.fsPath}.`); + // if canceled, stop listening for results + serverDispose(); // this will resolve deferredTillServerClose + const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', @@ -72,7 +72,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { uri, testIds, name, - cSource, + serverDispose, runInstance, profileKind, executionFactory, @@ -97,7 +97,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], resultNamedPipeName: string, - serverCancel: CancellationTokenSource, + serverDispose: () => void, runInstance?: TestRun, profileKind?: TestRunProfileKind, executionFactory?: IPythonExecutionFactory, @@ -174,7 +174,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { await debugLauncher!.launchDebugger( launchOptions, () => { - serverCancel.cancel(); + serverDispose(); // this will resolve the deferredTillAllServerClose }, sessionOptions, ); @@ -196,7 +196,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo(`Test run cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); proc.kill(); deferredTillExecClose.resolve(); - serverCancel.cancel(); + serverDispose(); // this will resolve the deferredTillAllServerClose }); proc.stdout.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); @@ -216,7 +216,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { ); } deferredTillExecClose.resolve(); - serverCancel.cancel(); + serverDispose(); // this will resolve the deferredTillAllServerClose }); await deferredTillExecClose.promise; } else { @@ -239,7 +239,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { resultProc?.kill(); } else { deferredTillExecClose.resolve(); - serverCancel.cancel(); } }); @@ -283,12 +282,12 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { runInstance, ); } + // this doesn't work, it instead directs us to the noop one which is defined first + // potentially this is due to the server already being close, if this is the case? + serverDispose(); // this will resolve deferredTillServerClose } - - // deferredTillEOT is resolved when all data sent on stdout and stderr is received, close event is only called when this occurs // due to the sync reading of the output. deferredTillExecClose.resolve(); - serverCancel.cancel(); }); await deferredTillExecClose.promise; } diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 04518e121651..c04ec4d54b45 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -45,7 +45,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const { unittestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; - const name = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { + const { name, dispose } = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { this.resultResolver?.resolveDiscovery(data); }); @@ -67,7 +67,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { try { await this.runDiscovery(uri, options, name, cwd, executionFactory); } finally { - // none + dispose(); } // placeholder until after the rewrite is adopted // TODO: remove after adoption. diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 6db36d96149f..350709392570 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import * as path from 'path'; -import { CancellationTokenSource, DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import { DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; import { ChildProcess } from 'child_process'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { Deferred, createDeferred } from '../../../common/utils/async'; @@ -59,24 +59,23 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { traceError(`No run instance found, cannot resolve execution, for workspace ${uri.fsPath}.`); } }; - const cSource = new CancellationTokenSource(); - runInstance?.token.onCancellationRequested(() => cSource.cancel()); - const name = await utils.startRunResultNamedPipe( + const { name: resultNamedPipeName, dispose: serverDispose } = await utils.startRunResultNamedPipe( dataReceivedCallback, // callback to handle data received deferredTillServerClose, // deferred to resolve when server closes - cSource.token, // token to cancel + runInstance?.token, // token to cancel ); runInstance?.token.onCancellationRequested(() => { console.log(`Test run cancelled, resolving 'till TillAllServerClose' deferred for ${uri.fsPath}.`); // if canceled, stop listening for results deferredTillServerClose.resolve(); + serverDispose(); }); try { await this.runTestsNew( uri, testIds, - name, - cSource, + resultNamedPipeName, + serverDispose, runInstance, profileKind, executionFactory, @@ -99,7 +98,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], resultNamedPipeName: string, - serverCancel: CancellationTokenSource, + serverDispose: () => void, runInstance?: TestRun, profileKind?: TestRunProfileKind, executionFactory?: IPythonExecutionFactory, @@ -179,7 +178,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { await debugLauncher.launchDebugger( launchOptions, () => { - serverCancel.cancel(); + serverDispose(); // this will resolve the deferredTillAllServerClose }, sessionOptions, ); @@ -198,7 +197,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo(`Test run cancelled, killing unittest subprocess for workspace ${uri.fsPath}`); proc.kill(); deferredTillExecClose.resolve(); - serverCancel.cancel(); + serverDispose(); // this will resolve the deferredTillAllServerClose }); proc.stdout.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); @@ -218,7 +217,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { ); } deferredTillExecClose.resolve(); - serverCancel.cancel(); + serverDispose(); // this will resolve the deferredTillAllServerClose }); await deferredTillExecClose.promise; } else { @@ -239,7 +238,6 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { resultProc?.kill(); } else { deferredTillExecClose?.resolve(); - serverCancel.cancel(); } }); @@ -276,9 +274,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance, ); } + serverDispose(); } deferredTillExecClose.resolve(); - serverCancel.cancel(); }); await deferredTillExecClose.promise; } diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 24a34f8645ed..8a1891962429 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -81,6 +81,8 @@ suite('End to End Tests: test adapters', () => { 'coverageWorkspace', ); suiteSetup(async () => { + serviceContainer = (await initialize()).serviceContainer; + // create symlink for specific symlink test const target = rootPathSmallWorkspace; const dest = rootPathDiscoverySymlink; @@ -105,7 +107,6 @@ suite('End to End Tests: test adapters', () => { }); setup(async () => { - serviceContainer = (await initialize()).serviceContainer; getPixiStub = sinon.stub(pixi, 'getPixi'); getPixiStub.resolves(undefined); @@ -677,7 +678,7 @@ suite('End to End Tests: test adapters', () => { }); test('pytest execution adapter small workspace with correct output', async () => { // result resolver and saved data for assertions - resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); let callCount = 0; let failureOccurred = false; let failureMsg = ''; @@ -874,7 +875,7 @@ suite('End to End Tests: test adapters', () => { }); test('pytest execution adapter large workspace', async () => { // result resolver and saved data for assertions - resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); let callCount = 0; let failureOccurred = false; let failureMsg = ''; @@ -1061,12 +1062,88 @@ suite('End to End Tests: test adapters', () => { assert.strictEqual(failureOccurred, false, failureMsg); }); }); + test('unittest execution adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver._resolveExecution = async (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`unittest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = 'Expected test to have a null pointer'; + } + } else if (data.error.length === 0) { + failureOccurred = true; + failureMsg = "Expected errors in 'error' field"; + } + } else { + const indexOfTest = JSON.stringify(data.result).search('error'); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.'; + } + } + if (data.result === undefined) { + failureOccurred = true; + failureMsg = 'Expected results to be present'; + } + // make sure the testID is found in the results + const indexOfTest = JSON.stringify(data).search('test_seg_fault.TestSegmentationFault.test_segfault'); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = 'Expected testId to be present'; + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + return Promise.resolve(); + }; + + const testId = `test_seg_fault.TestSegmentationFault.test_segfault`; + const testIds: string[] = [testId]; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathErrorWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + // run pytest execution + const executionAdapter = new UnittestTestExecutionAdapter( + configService, + testOutputChannel.object, + resultResolver, + envVarsService, + ); + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests(workspaceUri, testIds, TestRunProfileKind.Run, testRun.object, pythonExecFactory) + .finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); test('pytest execution adapter seg fault error handling', async () => { resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); let callCount = 0; let failureOccurred = false; let failureMsg = ''; - console.log('EFB: beginning function'); resultResolver._resolveExecution = async (data, _token?) => { // do the following asserts for each time resolveExecution is called, should be called once per test. console.log(`pytest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); @@ -1092,7 +1169,7 @@ suite('End to End Tests: test adapters', () => { failureMsg = err ? (err as Error).toString() : ''; failureOccurred = true; } - return Promise.resolve(); + // return Promise.resolve(); }; const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 538b77161483..18680b123b21 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -45,7 +45,14 @@ suite('pytest test discovery adapter', () => { mockExtensionRootDir.setup((m) => m.toString()).returns(() => '/mocked/extension/root/dir'); utilsStartDiscoveryNamedPipeStub = sinon.stub(util, 'startDiscoveryNamedPipe'); - utilsStartDiscoveryNamedPipeStub.callsFake(() => Promise.resolve('discoveryResultPipe-mockName')); + utilsStartDiscoveryNamedPipeStub.callsFake(() => + Promise.resolve({ + name: 'discoveryResultPipe-mockName', + dispose: () => { + /* no-op */ + }, + }), + ); // constants expectedPath = path.join('/', 'my', 'test', 'path'); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 18cabcc96772..b3f226402b72 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -88,7 +88,14 @@ suite('pytest test execution adapter', () => { myTestPath = path.join('/', 'my', 'test', 'path', '/'); utilsStartRunResultNamedPipeStub = sinon.stub(util, 'startRunResultNamedPipe'); - utilsStartRunResultNamedPipeStub.callsFake(() => Promise.resolve('runResultPipe-mockName')); + utilsStartRunResultNamedPipeStub.callsFake(() => + Promise.resolve({ + name: 'runResultPipe-mockName', + dispose: () => { + /* no-op */ + }, + }), + ); }); teardown(() => { sinon.restore(); diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts index 81480d08b2b8..d8b685b13654 100644 --- a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -71,9 +71,6 @@ suite('Execution Flow Run Adapters', () => { const { token } = cancellationToken; testRunMock.setup((t) => t.token).returns(() => token); - // run result pipe mocking and the related server close dispose - let deferredTillServerCloseTester: Deferred | undefined; - // // mock exec service and exec factory execServiceStub .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) @@ -100,13 +97,11 @@ suite('Execution Flow Run Adapters', () => { return Promise.resolve('named-pipe'); }); - utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, token) => { + // run result pipe mocking and the related server close dispose + let deferredTillServerCloseTester: Deferred | undefined; + utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, _token) => { deferredTillServerCloseTester = deferredTillServerClose; - token?.onCancellationRequested(() => { - deferredTillServerCloseTester?.resolve(); - }); - - return Promise.resolve('named-pipes-socket-name'); + return Promise.resolve({ name: 'named-pipes-socket-name', dispose: serverDisposeStub }); }); serverDisposeStub.callsFake(() => { console.log('server disposed'); @@ -132,6 +127,9 @@ suite('Execution Flow Run Adapters', () => { ); // wait for server to start to keep test from failing await deferredStartTestIdsNamedPipe.promise; + + // assert the server dispose function was called correctly + sinon.assert.calledOnce(serverDisposeStub); }); test(`Adapter ${adapter}: token called mid-debug resolves correctly`, async () => { // mock test run and cancelation token @@ -140,9 +138,6 @@ suite('Execution Flow Run Adapters', () => { const { token } = cancellationToken; testRunMock.setup((t) => t.token).returns(() => token); - // run result pipe mocking and the related server close dispose - let deferredTillServerCloseTester: Deferred | undefined; - // // mock exec service and exec factory execServiceStub .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) @@ -169,12 +164,14 @@ suite('Execution Flow Run Adapters', () => { return Promise.resolve('named-pipe'); }); + // run result pipe mocking and the related server close dispose + let deferredTillServerCloseTester: Deferred | undefined; utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, _token) => { deferredTillServerCloseTester = deferredTillServerClose; - token?.onCancellationRequested(() => { - deferredTillServerCloseTester?.resolve(); + return Promise.resolve({ + name: 'named-pipes-socket-name', + dispose: serverDisposeStub, }); - return Promise.resolve('named-pipes-socket-name'); }); serverDisposeStub.callsFake(() => { console.log('server disposed'); @@ -213,6 +210,10 @@ suite('Execution Flow Run Adapters', () => { ); // wait for server to start to keep test from failing await deferredStartTestIdsNamedPipe.promise; + + // TODO: fix the server disposal so it is called once not twice, + // currently not a problem but would be useful to improve clarity + sinon.assert.called(serverDisposeStub); }); }); }); diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index a0ee65d57922..4c92c8c0b682 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -82,7 +82,14 @@ suite('Unittest test discovery adapter', () => { }; utilsStartDiscoveryNamedPipeStub = sinon.stub(util, 'startDiscoveryNamedPipe'); - utilsStartDiscoveryNamedPipeStub.callsFake(() => Promise.resolve('discoveryResultPipe-mockName')); + utilsStartDiscoveryNamedPipeStub.callsFake(() => + Promise.resolve({ + name: 'discoveryResultPipe-mockName', + dispose: () => { + /* no-op */ + }, + }), + ); }); teardown(() => { sinon.restore(); diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index 78dcb0229e45..f2c06cd13496 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -87,7 +87,14 @@ suite('Unittest test execution adapter', () => { myTestPath = path.join('/', 'my', 'test', 'path', '/'); utilsStartRunResultNamedPipeStub = sinon.stub(util, 'startRunResultNamedPipe'); - utilsStartRunResultNamedPipeStub.callsFake(() => Promise.resolve('runResultPipe-mockName')); + utilsStartRunResultNamedPipeStub.callsFake(() => + Promise.resolve({ + name: 'runResultPipe-mockName', + dispose: () => { + /* no-op */ + }, + }), + ); }); teardown(() => { sinon.restore(); diff --git a/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py b/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py index 80be80f023c2..bad7ff8fcbbd 100644 --- a/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py +++ b/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py @@ -7,12 +7,11 @@ class TestSegmentationFault(unittest.TestCase): def cause_segfault(self): - print("Causing a segmentation fault") ctypes.string_at(0) # Dereference a NULL pointer def test_segfault(self): - self.cause_segfault() assert True + self.cause_segfault() if __name__ == "__main__": From ef6ca9f257b6c91e843ca2214627201b6f431031 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 6 Jan 2025 15:24:33 -0800 Subject: [PATCH 0847/1136] Fix fifo communication for large testing projects (#24690) revert the revert of the old commit so now main uses fifo again add a limit of 4096 bytes per communication sent between python subprocess and extension fixes https://github.com/microsoft/vscode-python/issues/24656 --- noxfile.py | 1 + python_files/testing_tools/socket_manager.py | 56 ++--- python_files/tests/pytestadapter/helpers.py | 33 ++- python_files/unittestadapter/pvsc_utils.py | 19 +- python_files/vscode_pytest/__init__.py | 50 ++-- python_files/vscode_pytest/_common.py | 2 + src/client/common/pipes/namedPipes.ts | 231 +++++++++++++----- .../testing/testController/common/utils.ts | 140 +++++------ .../pytest/pytestDiscoveryAdapter.ts | 9 +- .../pytest/pytestExecutionAdapter.ts | 29 +-- .../unittest/testDiscoveryAdapter.ts | 4 +- .../unittest/testExecutionAdapter.ts | 24 +- .../testing/common/testingAdapter.test.ts | 86 +------ .../pytestDiscoveryAdapter.unit.test.ts | 9 +- .../pytestExecutionAdapter.unit.test.ts | 9 +- .../testCancellationRunAdapters.unit.test.ts | 31 ++- .../testDiscoveryAdapter.unit.test.ts | 9 +- .../testExecutionAdapter.unit.test.ts | 9 +- .../errorWorkspace/test_seg_fault.py | 3 +- .../test_parameterized_subtest.py | 103 ++++++++ 20 files changed, 481 insertions(+), 376 deletions(-) create mode 100644 python_files/vscode_pytest/_common.py diff --git a/noxfile.py b/noxfile.py index 60e22d461074..3991ee8c025a 100644 --- a/noxfile.py +++ b/noxfile.py @@ -53,6 +53,7 @@ def install_python_libs(session: nox.Session): ) session.install("packaging") + session.install("debugpy") # Download get-pip script session.run( diff --git a/python_files/testing_tools/socket_manager.py b/python_files/testing_tools/socket_manager.py index 347453a6ca1a..f143ac111cdb 100644 --- a/python_files/testing_tools/socket_manager.py +++ b/python_files/testing_tools/socket_manager.py @@ -20,39 +20,24 @@ def __exit__(self, *_): self.close() def connect(self): - if sys.platform == "win32": - self._writer = open(self.name, "w", encoding="utf-8") # noqa: SIM115, PTH123 - # reader created in read method - else: - self._socket = _SOCKET(socket.AF_UNIX, socket.SOCK_STREAM) - self._socket.connect(self.name) + self._writer = open(self.name, "w", encoding="utf-8") # noqa: SIM115, PTH123 + # reader created in read method return self def close(self): - if sys.platform == "win32": - self._writer.close() - else: - # add exception catch - self._socket.close() + self._writer.close() + if hasattr(self, "_reader"): + self._reader.close() def write(self, data: str): - if sys.platform == "win32": - try: - # for windows, is should only use \n\n - request = ( - f"""content-length: {len(data)}\ncontent-type: application/json\n\n{data}""" - ) - self._writer.write(request) - self._writer.flush() - except Exception as e: - print("error attempting to write to pipe", e) - raise (e) - else: - # must include the carriage-return defined (as \r\n) for unix systems - request = ( - f"""content-length: {len(data)}\r\ncontent-type: application/json\r\n\r\n{data}""" - ) - self._socket.send(request.encode("utf-8")) + try: + # for windows, is should only use \n\n + request = f"""content-length: {len(data)}\ncontent-type: application/json\n\n{data}""" + self._writer.write(request) + self._writer.flush() + except Exception as e: + print("error attempting to write to pipe", e) + raise (e) def read(self, bufsize=1024) -> str: """Read data from the socket. @@ -63,17 +48,10 @@ def read(self, bufsize=1024) -> str: Returns: data (str): Data received from the socket. """ - if sys.platform == "win32": - # returns a string automatically from read - if not hasattr(self, "_reader"): - self._reader = open(self.name, encoding="utf-8") # noqa: SIM115, PTH123 - return self._reader.read(bufsize) - else: - # receive bytes and convert to string - while True: - part: bytes = self._socket.recv(bufsize) - data: str = part.decode("utf-8") - return data + # returns a string automatically from read + if not hasattr(self, "_reader"): + self._reader = open(self.name, encoding="utf-8") # noqa: SIM115, PTH123 + return self._reader.read(bufsize) class SocketManager: diff --git a/python_files/tests/pytestadapter/helpers.py b/python_files/tests/pytestadapter/helpers.py index 7972eedd0919..7a75e6248844 100644 --- a/python_files/tests/pytestadapter/helpers.py +++ b/python_files/tests/pytestadapter/helpers.py @@ -128,6 +128,22 @@ def parse_rpc_message(data: str) -> Tuple[Dict[str, str], str]: print("json decode error") +def _listen_on_fifo(pipe_name: str, result: List[str], completed: threading.Event): + # Open the FIFO for reading + fifo_path = pathlib.Path(pipe_name) + with fifo_path.open() as fifo: + print("Waiting for data...") + while True: + if completed.is_set(): + break # Exit loop if completed event is set + data = fifo.read() # This will block until data is available + if len(data) == 0: + # If data is empty, assume EOF + break + print(f"Received: {data}") + result.append(data) + + def _listen_on_pipe_new(listener, result: List[str], completed: threading.Event): """Listen on the named pipe or Unix domain socket for JSON data from the server. @@ -307,14 +323,19 @@ def runner_with_cwd_env( # if additional environment variables are passed, add them to the environment if env_add: env.update(env_add) - server = UnixPipeServer(pipe_name) - server.start() + # server = UnixPipeServer(pipe_name) + # server.start() + ################# + # Create the FIFO (named pipe) if it doesn't exist + # if not pathlib.Path.exists(pipe_name): + os.mkfifo(pipe_name) + ################# completed = threading.Event() result = [] # result is a string array to store the data during threading t1: threading.Thread = threading.Thread( - target=_listen_on_pipe_new, args=(server, result, completed) + target=_listen_on_fifo, args=(pipe_name, result, completed) ) t1.start() @@ -364,14 +385,14 @@ def generate_random_pipe_name(prefix=""): # For Windows, named pipes have a specific naming convention. if sys.platform == "win32": - return f"\\\\.\\pipe\\{prefix}-{random_suffix}-sock" + return f"\\\\.\\pipe\\{prefix}-{random_suffix}" # For Unix-like systems, use either the XDG_RUNTIME_DIR or a temporary directory. xdg_runtime_dir = os.getenv("XDG_RUNTIME_DIR") if xdg_runtime_dir: - return os.path.join(xdg_runtime_dir, f"{prefix}-{random_suffix}.sock") # noqa: PTH118 + return os.path.join(xdg_runtime_dir, f"{prefix}-{random_suffix}") # noqa: PTH118 else: - return os.path.join(tempfile.gettempdir(), f"{prefix}-{random_suffix}.sock") # noqa: PTH118 + return os.path.join(tempfile.gettempdir(), f"{prefix}-{random_suffix}") # noqa: PTH118 class UnixPipeServer: diff --git a/python_files/unittestadapter/pvsc_utils.py b/python_files/unittestadapter/pvsc_utils.py index 09e61ff40518..34b8553600f1 100644 --- a/python_files/unittestadapter/pvsc_utils.py +++ b/python_files/unittestadapter/pvsc_utils.py @@ -18,8 +18,6 @@ from typing_extensions import NotRequired # noqa: E402 -from testing_tools import socket_manager # noqa: E402 - # Types @@ -331,10 +329,10 @@ def send_post_request( if __writer is None: try: - __writer = socket_manager.PipeManager(test_run_pipe) - __writer.connect() + __writer = open(test_run_pipe, "wb") # noqa: SIM115, PTH123 except Exception as error: error_msg = f"Error attempting to connect to extension named pipe {test_run_pipe}[vscode-unittest]: {error}" + print(error_msg, file=sys.stderr) __writer = None raise VSCodeUnittestError(error_msg) from error @@ -343,10 +341,19 @@ def send_post_request( "params": payload, } data = json.dumps(rpc) - try: if __writer: - __writer.write(data) + request = ( + f"""content-length: {len(data)}\r\ncontent-type: application/json\r\n\r\n{data}""" + ) + size = 4096 + encoded = request.encode("utf-8") + bytes_written = 0 + while bytes_written < len(encoded): + print("writing more bytes!") + segment = encoded[bytes_written : bytes_written + size] + bytes_written += __writer.write(segment) + __writer.flush() else: print( f"Connection error[vscode-unittest], writer is None \n[vscode-unittest] data: \n{data} \n", diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 8a54a7249d71..1e812e41d2ae 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -21,11 +21,6 @@ import pytest -script_dir = pathlib.Path(__file__).parent.parent -sys.path.append(os.fspath(script_dir)) -sys.path.append(os.fspath(script_dir / "lib" / "python")) -from testing_tools import socket_manager # noqa: E402 - if TYPE_CHECKING: from pluggy import Result @@ -171,7 +166,7 @@ def pytest_exception_interact(node, call, report): collected_test = TestRunResultDict() collected_test[node_id] = item_result cwd = pathlib.Path.cwd() - execution_post( + send_execution_message( os.fsdecode(cwd), "success", collected_test if collected_test else None, @@ -295,7 +290,7 @@ def pytest_report_teststatus(report, config): # noqa: ARG001 ) collected_test = TestRunResultDict() collected_test[absolute_node_id] = item_result - execution_post( + send_execution_message( os.fsdecode(cwd), "success", collected_test if collected_test else None, @@ -329,7 +324,7 @@ def pytest_runtest_protocol(item, nextitem): # noqa: ARG001 ) collected_test = TestRunResultDict() collected_test[absolute_node_id] = item_result - execution_post( + send_execution_message( os.fsdecode(cwd), "success", collected_test if collected_test else None, @@ -405,7 +400,7 @@ def pytest_sessionfinish(session, exitstatus): "children": [], "id_": "", } - post_response(os.fsdecode(cwd), error_node) + send_discovery_message(os.fsdecode(cwd), error_node) try: session_node: TestNode | None = build_test_tree(session) if not session_node: @@ -413,7 +408,7 @@ def pytest_sessionfinish(session, exitstatus): "Something went wrong following pytest finish, \ no session node was created" ) - post_response(os.fsdecode(cwd), session_node) + send_discovery_message(os.fsdecode(cwd), session_node) except Exception as e: ERRORS.append( f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" @@ -425,7 +420,7 @@ def pytest_sessionfinish(session, exitstatus): "children": [], "id_": "", } - post_response(os.fsdecode(cwd), error_node) + send_discovery_message(os.fsdecode(cwd), error_node) else: if exitstatus == 0 or exitstatus == 1: exitstatus_bool = "success" @@ -435,7 +430,7 @@ def pytest_sessionfinish(session, exitstatus): ) exitstatus_bool = "error" - execution_post( + send_execution_message( os.fsdecode(cwd), exitstatus_bool, None, @@ -489,7 +484,7 @@ def pytest_sessionfinish(session, exitstatus): result=file_coverage_map, error=None, ) - send_post_request(payload) + send_message(payload) def build_test_tree(session: pytest.Session) -> TestNode: @@ -857,8 +852,10 @@ def get_node_path(node: Any) -> pathlib.Path: atexit.register(lambda: __writer.close() if __writer else None) -def execution_post(cwd: str, status: Literal["success", "error"], tests: TestRunResultDict | None): - """Sends a POST request with execution payload details. +def send_execution_message( + cwd: str, status: Literal["success", "error"], tests: TestRunResultDict | None +): + """Sends message execution payload details. Args: cwd (str): Current working directory. @@ -870,10 +867,10 @@ def execution_post(cwd: str, status: Literal["success", "error"], tests: TestRun ) if ERRORS: payload["error"] = ERRORS - send_post_request(payload) + send_message(payload) -def post_response(cwd: str, session_node: TestNode) -> None: +def send_discovery_message(cwd: str, session_node: TestNode) -> None: """ Sends a POST request with test session details in payload. @@ -889,7 +886,7 @@ def post_response(cwd: str, session_node: TestNode) -> None: } if ERRORS is not None: payload["error"] = ERRORS - send_post_request(payload, cls_encoder=PathEncoder) + send_message(payload, cls_encoder=PathEncoder) class PathEncoder(json.JSONEncoder): @@ -901,7 +898,7 @@ def default(self, o): return super().default(o) -def send_post_request( +def send_message( payload: ExecutionPayloadDict | DiscoveryPayloadDict | CoveragePayloadDict, cls_encoder=None, ): @@ -926,8 +923,7 @@ def send_post_request( if __writer is None: try: - __writer = socket_manager.PipeManager(TEST_RUN_PIPE) - __writer.connect() + __writer = open(TEST_RUN_PIPE, "wb") # noqa: SIM115, PTH123 except Exception as error: error_msg = f"Error attempting to connect to extension named pipe {TEST_RUN_PIPE}[vscode-pytest]: {error}" print(error_msg, file=sys.stderr) @@ -945,10 +941,18 @@ def send_post_request( "params": payload, } data = json.dumps(rpc, cls=cls_encoder) - try: if __writer: - __writer.write(data) + request = ( + f"""content-length: {len(data)}\r\ncontent-type: application/json\r\n\r\n{data}""" + ) + size = 4096 + encoded = request.encode("utf-8") + bytes_written = 0 + while bytes_written < len(encoded): + segment = encoded[bytes_written : bytes_written + size] + bytes_written += __writer.write(segment) + __writer.flush() else: print( f"Plugin error connection error[vscode-pytest], writer is None \n[vscode-pytest] data: \n{data} \n", diff --git a/python_files/vscode_pytest/_common.py b/python_files/vscode_pytest/_common.py new file mode 100644 index 000000000000..9f835f555b6e --- /dev/null +++ b/python_files/vscode_pytest/_common.py @@ -0,0 +1,2 @@ +# def send_post_request(): +# return diff --git a/src/client/common/pipes/namedPipes.ts b/src/client/common/pipes/namedPipes.ts index c6010d491822..8cccd4cdcfed 100644 --- a/src/client/common/pipes/namedPipes.ts +++ b/src/client/common/pipes/namedPipes.ts @@ -1,67 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as cp from 'child_process'; import * as crypto from 'crypto'; +import * as fs from 'fs-extra'; import * as net from 'net'; import * as os from 'os'; import * as path from 'path'; import * as rpc from 'vscode-jsonrpc/node'; +import { CancellationError, CancellationToken, Disposable } from 'vscode'; import { traceVerbose } from '../../logging'; - -export interface ConnectedServerObj { - serverOnClosePromise(): Promise; -} - -export function createNamedPipeServer( - pipeName: string, - onConnectionCallback: (value: [rpc.MessageReader, rpc.MessageWriter]) => void, -): Promise { - traceVerbose(`Creating named pipe server on ${pipeName}`); - - let connectionCount = 0; - return new Promise((resolve, reject) => { - // create a server, resolves and returns server on listen - const server = net.createServer((socket) => { - // this lambda function is called whenever a client connects to the server - connectionCount += 1; - traceVerbose('new client is connected to the socket, connectionCount: ', connectionCount, pipeName); - socket.on('close', () => { - // close event is emitted by client to the server - connectionCount -= 1; - traceVerbose('client emitted close event, connectionCount: ', connectionCount); - if (connectionCount <= 0) { - // if all clients are closed, close the server - traceVerbose('connection count is <= 0, closing the server: ', pipeName); - server.close(); - } - }); - - // upon connection create a reader and writer and pass it to the callback - onConnectionCallback([ - new rpc.SocketMessageReader(socket, 'utf-8'), - new rpc.SocketMessageWriter(socket, 'utf-8'), - ]); - }); - const closedServerPromise = new Promise((resolveOnServerClose) => { - // get executed on connection close and resolves - // implementation of the promise is the arrow function - server.on('close', resolveOnServerClose); - }); - server.on('error', reject); - - server.listen(pipeName, () => { - // this function is called when the server is listening - server.removeListener('error', reject); - const connectedServer = { - // when onClosed event is called, so is closed function - // goes backwards up the chain, when resolve2 is called, so is onClosed that means server.onClosed() on the other end can work - // event C - serverOnClosePromise: () => closedServerPromise, - }; - resolve(connectedServer); - }); - }); -} +import { isWindows } from '../utils/platform'; +import { createDeferred } from '../utils/async'; +import { noop } from '../utils/misc'; const { XDG_RUNTIME_DIR } = process.env; export function generateRandomPipeName(prefix: string): string { @@ -72,20 +23,178 @@ export function generateRandomPipeName(prefix: string): string { } if (process.platform === 'win32') { - return `\\\\.\\pipe\\${prefix}-${randomSuffix}-sock`; + return `\\\\.\\pipe\\${prefix}-${randomSuffix}`; } let result; if (XDG_RUNTIME_DIR) { - result = path.join(XDG_RUNTIME_DIR, `${prefix}-${randomSuffix}.sock`); + result = path.join(XDG_RUNTIME_DIR, `${prefix}-${randomSuffix}`); } else { - result = path.join(os.tmpdir(), `${prefix}-${randomSuffix}.sock`); + result = path.join(os.tmpdir(), `${prefix}-${randomSuffix}`); } return result; } -export function namedPipeClient(name: string): [rpc.MessageReader, rpc.MessageWriter] { - const socket = net.connect(name); - return [new rpc.SocketMessageReader(socket, 'utf-8'), new rpc.SocketMessageWriter(socket, 'utf-8')]; +async function mkfifo(fifoPath: string): Promise { + return new Promise((resolve, reject) => { + const proc = cp.spawn('mkfifo', [fifoPath]); + proc.on('error', (err) => { + reject(err); + }); + proc.on('exit', (code) => { + if (code === 0) { + resolve(); + } + }); + }); +} + +export async function createWriterPipe(pipeName: string, token?: CancellationToken): Promise { + // windows implementation of FIFO using named pipes + if (isWindows()) { + const deferred = createDeferred(); + const server = net.createServer((socket) => { + traceVerbose(`Pipe connected: ${pipeName}`); + server.close(); + deferred.resolve(new rpc.SocketMessageWriter(socket, 'utf-8')); + }); + + server.on('error', deferred.reject); + server.listen(pipeName); + if (token) { + token.onCancellationRequested(() => { + if (server.listening) { + server.close(); + } + deferred.reject(new CancellationError()); + }); + } + return deferred.promise; + } + // linux implementation of FIFO + await mkfifo(pipeName); + try { + await fs.chmod(pipeName, 0o666); + } catch { + // Intentionally ignored + } + const writer = fs.createWriteStream(pipeName, { + encoding: 'utf-8', + }); + return new rpc.StreamMessageWriter(writer, 'utf-8'); +} + +class CombinedReader implements rpc.MessageReader { + private _onError = new rpc.Emitter(); + + private _onClose = new rpc.Emitter(); + + private _onPartialMessage = new rpc.Emitter(); + + // eslint-disable-next-line @typescript-eslint/no-empty-function + private _callback: rpc.DataCallback = () => {}; + + private _disposables: rpc.Disposable[] = []; + + private _readers: rpc.MessageReader[] = []; + + constructor() { + this._disposables.push(this._onClose, this._onError, this._onPartialMessage); + } + + onError: rpc.Event = this._onError.event; + + onClose: rpc.Event = this._onClose.event; + + onPartialMessage: rpc.Event = this._onPartialMessage.event; + + listen(callback: rpc.DataCallback): rpc.Disposable { + this._callback = callback; + // eslint-disable-next-line no-return-assign, @typescript-eslint/no-empty-function + return new Disposable(() => (this._callback = () => {})); + } + + add(reader: rpc.MessageReader): void { + this._readers.push(reader); + reader.listen((msg) => { + this._callback(msg as rpc.NotificationMessage); + }); + this._disposables.push(reader); + reader.onClose(() => { + this.remove(reader); + if (this._readers.length === 0) { + this._onClose.fire(); + } + }); + reader.onError((e) => { + this.remove(reader); + this._onError.fire(e); + }); + } + + remove(reader: rpc.MessageReader): void { + const found = this._readers.find((r) => r === reader); + if (found) { + this._readers = this._readers.filter((r) => r !== reader); + reader.dispose(); + } + } + + dispose(): void { + this._readers.forEach((r) => r.dispose()); + this._readers = []; + this._disposables.forEach((disposable) => disposable.dispose()); + this._disposables = []; + } +} + +export async function createReaderPipe(pipeName: string, token?: CancellationToken): Promise { + if (isWindows()) { + // windows implementation of FIFO using named pipes + const deferred = createDeferred(); + const combined = new CombinedReader(); + + let refs = 0; + const server = net.createServer((socket) => { + traceVerbose(`Pipe connected: ${pipeName}`); + refs += 1; + + socket.on('close', () => { + refs -= 1; + if (refs <= 0) { + server.close(); + } + }); + combined.add(new rpc.SocketMessageReader(socket, 'utf-8')); + }); + server.on('error', deferred.reject); + server.listen(pipeName); + if (token) { + token.onCancellationRequested(() => { + if (server.listening) { + server.close(); + } + deferred.reject(new CancellationError()); + }); + } + deferred.resolve(combined); + return deferred.promise; + } + // mac/linux implementation of FIFO + await mkfifo(pipeName); + try { + await fs.chmod(pipeName, 0o666); + } catch { + // Intentionally ignored + } + const fd = await fs.open(pipeName, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK); + const socket = new net.Socket({ fd }); + const reader = new rpc.SocketMessageReader(socket, 'utf-8'); + socket.on('close', () => { + fs.close(fd).catch(noop); + reader.dispose(); + }); + + return reader; } diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 1d2be64f537d..b6848d0245dc 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -16,7 +16,7 @@ import { ITestResultResolver, } from './types'; import { Deferred, createDeferred } from '../../../common/utils/async'; -import { createNamedPipeServer, generateRandomPipeName } from '../../../common/pipes/namedPipes'; +import { createReaderPipe, generateRandomPipeName } from '../../../common/pipes/namedPipes'; import { EXTENSION_ROOT_DIR } from '../../../constants'; export function fixLogLinesNoTrailing(content: string): string { @@ -34,28 +34,6 @@ export function createTestingDeferred(): Deferred { return createDeferred(); } -export async function startTestIdsNamedPipe(testIds: string[]): Promise { - const pipeName: string = generateRandomPipeName('python-test-ids'); - // uses callback so the on connect action occurs after the pipe is created - await createNamedPipeServer(pipeName, ([_reader, writer]) => { - traceVerbose('Test Ids named pipe connected'); - // const num = await - const msg = { - jsonrpc: '2.0', - params: testIds, - } as Message; - writer - .write(msg) - .then(() => { - writer.end(); - }) - .catch((ex) => { - traceError('Failed to write test ids to named pipe', ex); - }); - }); - return pipeName; -} - interface ExecutionResultMessage extends Message { params: ExecutionTestPayload; } @@ -94,47 +72,47 @@ export async function startRunResultNamedPipe( dataReceivedCallback: (payload: ExecutionTestPayload) => void, deferredTillServerClose: Deferred, cancellationToken?: CancellationToken, -): Promise<{ name: string } & Disposable> { +): Promise { traceVerbose('Starting Test Result named pipe'); const pipeName: string = generateRandomPipeName('python-test-results'); - let disposeOfServer: () => void = () => { + + const reader = await createReaderPipe(pipeName, cancellationToken); + traceVerbose(`Test Results named pipe ${pipeName} connected`); + let disposables: Disposable[] = []; + const disposable = new Disposable(() => { + traceVerbose(`Test Results named pipe ${pipeName} disposed`); + disposables.forEach((d) => d.dispose()); + disposables = []; deferredTillServerClose.resolve(); - /* noop */ - }; - const server = await createNamedPipeServer(pipeName, ([reader, _writer]) => { - // this lambda function is: onConnectionCallback - // this is called once per client connecting to the server - traceVerbose(`Test Result named pipe ${pipeName} connected`); - let perConnectionDisposables: (Disposable | undefined)[] = [reader]; + }); - // create a function to dispose of the server - disposeOfServer = () => { - // dispose of all data listeners and cancelation listeners - perConnectionDisposables.forEach((d) => d?.dispose()); - perConnectionDisposables = []; - deferredTillServerClose.resolve(); - }; - perConnectionDisposables.push( - // per connection, add a listener for the cancellation token and the data + if (cancellationToken) { + disposables.push( cancellationToken?.onCancellationRequested(() => { - console.log(`Test Result named pipe ${pipeName} cancelled`); - // if cancel is called on one connection, dispose of all connections - disposeOfServer(); - }), - reader.listen((data: Message) => { - traceVerbose(`Test Result named pipe ${pipeName} received data`); - dataReceivedCallback((data as ExecutionResultMessage).params as ExecutionTestPayload); + traceLog(`Test Result named pipe ${pipeName} cancelled`); + disposable.dispose(); }), ); - server.serverOnClosePromise().then(() => { + } + disposables.push( + reader, + reader.listen((data: Message) => { + traceVerbose(`Test Result named pipe ${pipeName} received data`); + // if EOT, call decrement connection count (callback) + dataReceivedCallback((data as ExecutionResultMessage).params as ExecutionTestPayload); + }), + reader.onClose(() => { // this is called once the server close, once per run instance traceVerbose(`Test Result named pipe ${pipeName} closed. Disposing of listener/s.`); // dispose of all data listeners and cancelation listeners - disposeOfServer(); - }); - }); + disposable.dispose(); + }), + reader.onError((error) => { + traceError(`Test Results named pipe ${pipeName} error:`, error); + }), + ); - return { name: pipeName, dispose: disposeOfServer }; + return pipeName; } interface DiscoveryResultMessage extends Message { @@ -144,36 +122,44 @@ interface DiscoveryResultMessage extends Message { export async function startDiscoveryNamedPipe( callback: (payload: DiscoveredTestPayload) => void, cancellationToken?: CancellationToken, -): Promise<{ name: string } & Disposable> { +): Promise { traceVerbose('Starting Test Discovery named pipe'); + // const pipeName: string = '/Users/eleanorboyd/testingFiles/inc_dec_example/temp33.txt'; const pipeName: string = generateRandomPipeName('python-test-discovery'); - let dispose: () => void = () => { - /* noop */ - }; - await createNamedPipeServer(pipeName, ([reader, _writer]) => { - traceVerbose(`Test Discovery named pipe ${pipeName} connected`); - let disposables: (Disposable | undefined)[] = [reader]; - dispose = () => { - traceVerbose(`Test Discovery named pipe ${pipeName} disposed`); - disposables.forEach((d) => d?.dispose()); - disposables = []; - }; + const reader = await createReaderPipe(pipeName, cancellationToken); + + traceVerbose(`Test Discovery named pipe ${pipeName} connected`); + let disposables: Disposable[] = []; + const disposable = new Disposable(() => { + traceVerbose(`Test Discovery named pipe ${pipeName} disposed`); + disposables.forEach((d) => d.dispose()); + disposables = []; + }); + + if (cancellationToken) { disposables.push( - cancellationToken?.onCancellationRequested(() => { + cancellationToken.onCancellationRequested(() => { traceVerbose(`Test Discovery named pipe ${pipeName} cancelled`); - dispose(); - }), - reader.listen((data: Message) => { - traceVerbose(`Test Discovery named pipe ${pipeName} received data`); - callback((data as DiscoveryResultMessage).params as DiscoveredTestPayload); - }), - reader.onClose(() => { - traceVerbose(`Test Discovery named pipe ${pipeName} closed`); - dispose(); + disposable.dispose(); }), ); - }); - return { name: pipeName, dispose }; + } + + disposables.push( + reader, + reader.listen((data: Message) => { + traceVerbose(`Test Discovery named pipe ${pipeName} received data`); + callback((data as DiscoveryResultMessage).params as DiscoveredTestPayload); + }), + reader.onClose(() => { + traceVerbose(`Test Discovery named pipe ${pipeName} closed`); + disposable.dispose(); + }), + reader.onError((error) => { + traceError(`Test Discovery named pipe ${pipeName} error:`, error); + }), + ); + return pipeName; } export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 4baa76000d14..ff73b31435a3 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -42,15 +42,12 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { executionFactory?: IPythonExecutionFactory, interpreter?: PythonEnvironment, ): Promise { - const { name, dispose } = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { + const name = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { this.resultResolver?.resolveDiscovery(data); }); - try { - await this.runPytestDiscovery(uri, name, executionFactory, interpreter); - } finally { - dispose(); - } + await this.runPytestDiscovery(uri, name, executionFactory, interpreter); + // this is only a placeholder to handle function overloading until rewrite is finished const discoveryPayload: DiscoveredTestPayload = { cwd: uri.fsPath, status: 'success' }; return discoveryPayload; diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index b108bd876e5a..b408280a576e 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import { CancellationTokenSource, DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; import * as path from 'path'; import { ChildProcess } from 'child_process'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; @@ -49,16 +49,16 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceError(`No run instance found, cannot resolve execution, for workspace ${uri.fsPath}.`); } }; - const { name, dispose: serverDispose } = await utils.startRunResultNamedPipe( + const cSource = new CancellationTokenSource(); + runInstance?.token.onCancellationRequested(() => cSource.cancel()); + + const name = await utils.startRunResultNamedPipe( dataReceivedCallback, // callback to handle data received deferredTillServerClose, // deferred to resolve when server closes - runInstance?.token, // token to cancel + cSource.token, // token to cancel ); runInstance?.token.onCancellationRequested(() => { traceInfo(`Test run cancelled, resolving 'TillServerClose' deferred for ${uri.fsPath}.`); - // if canceled, stop listening for results - serverDispose(); // this will resolve deferredTillServerClose - const executionPayload: ExecutionTestPayload = { cwd: uri.fsPath, status: 'success', @@ -72,7 +72,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { uri, testIds, name, - serverDispose, + cSource, runInstance, profileKind, executionFactory, @@ -97,7 +97,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], resultNamedPipeName: string, - serverDispose: () => void, + serverCancel: CancellationTokenSource, runInstance?: TestRun, profileKind?: TestRunProfileKind, executionFactory?: IPythonExecutionFactory, @@ -174,7 +174,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { await debugLauncher!.launchDebugger( launchOptions, () => { - serverDispose(); // this will resolve the deferredTillAllServerClose + serverCancel.cancel(); }, sessionOptions, ); @@ -196,7 +196,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo(`Test run cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); proc.kill(); deferredTillExecClose.resolve(); - serverDispose(); // this will resolve the deferredTillAllServerClose + serverCancel.cancel(); }); proc.stdout.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); @@ -216,7 +216,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { ); } deferredTillExecClose.resolve(); - serverDispose(); // this will resolve the deferredTillAllServerClose + serverCancel.cancel(); }); await deferredTillExecClose.promise; } else { @@ -239,6 +239,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { resultProc?.kill(); } else { deferredTillExecClose.resolve(); + serverCancel.cancel(); } }); @@ -282,12 +283,12 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { runInstance, ); } - // this doesn't work, it instead directs us to the noop one which is defined first - // potentially this is due to the server already being close, if this is the case? - serverDispose(); // this will resolve deferredTillServerClose } + + // deferredTillEOT is resolved when all data sent on stdout and stderr is received, close event is only called when this occurs // due to the sync reading of the output. deferredTillExecClose.resolve(); + serverCancel.cancel(); }); await deferredTillExecClose.promise; } diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index c04ec4d54b45..04518e121651 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -45,7 +45,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const { unittestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; - const { name, dispose } = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { + const name = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { this.resultResolver?.resolveDiscovery(data); }); @@ -67,7 +67,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { try { await this.runDiscovery(uri, options, name, cwd, executionFactory); } finally { - dispose(); + // none } // placeholder until after the rewrite is adopted // TODO: remove after adoption. diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 350709392570..6db36d96149f 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import * as path from 'path'; -import { DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import { CancellationTokenSource, DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; import { ChildProcess } from 'child_process'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { Deferred, createDeferred } from '../../../common/utils/async'; @@ -59,23 +59,24 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { traceError(`No run instance found, cannot resolve execution, for workspace ${uri.fsPath}.`); } }; - const { name: resultNamedPipeName, dispose: serverDispose } = await utils.startRunResultNamedPipe( + const cSource = new CancellationTokenSource(); + runInstance?.token.onCancellationRequested(() => cSource.cancel()); + const name = await utils.startRunResultNamedPipe( dataReceivedCallback, // callback to handle data received deferredTillServerClose, // deferred to resolve when server closes - runInstance?.token, // token to cancel + cSource.token, // token to cancel ); runInstance?.token.onCancellationRequested(() => { console.log(`Test run cancelled, resolving 'till TillAllServerClose' deferred for ${uri.fsPath}.`); // if canceled, stop listening for results deferredTillServerClose.resolve(); - serverDispose(); }); try { await this.runTestsNew( uri, testIds, - resultNamedPipeName, - serverDispose, + name, + cSource, runInstance, profileKind, executionFactory, @@ -98,7 +99,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uri: Uri, testIds: string[], resultNamedPipeName: string, - serverDispose: () => void, + serverCancel: CancellationTokenSource, runInstance?: TestRun, profileKind?: TestRunProfileKind, executionFactory?: IPythonExecutionFactory, @@ -178,7 +179,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { await debugLauncher.launchDebugger( launchOptions, () => { - serverDispose(); // this will resolve the deferredTillAllServerClose + serverCancel.cancel(); }, sessionOptions, ); @@ -197,7 +198,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { traceInfo(`Test run cancelled, killing unittest subprocess for workspace ${uri.fsPath}`); proc.kill(); deferredTillExecClose.resolve(); - serverDispose(); // this will resolve the deferredTillAllServerClose + serverCancel.cancel(); }); proc.stdout.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); @@ -217,7 +218,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { ); } deferredTillExecClose.resolve(); - serverDispose(); // this will resolve the deferredTillAllServerClose + serverCancel.cancel(); }); await deferredTillExecClose.promise; } else { @@ -238,6 +239,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { resultProc?.kill(); } else { deferredTillExecClose?.resolve(); + serverCancel.cancel(); } }); @@ -274,9 +276,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance, ); } - serverDispose(); } deferredTillExecClose.resolve(); + serverCancel.cancel(); }); await deferredTillExecClose.promise; } diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 8a1891962429..ec19ce00f13f 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -81,8 +81,6 @@ suite('End to End Tests: test adapters', () => { 'coverageWorkspace', ); suiteSetup(async () => { - serviceContainer = (await initialize()).serviceContainer; - // create symlink for specific symlink test const target = rootPathSmallWorkspace; const dest = rootPathDiscoverySymlink; @@ -107,6 +105,7 @@ suite('End to End Tests: test adapters', () => { }); setup(async () => { + serviceContainer = (await initialize()).serviceContainer; getPixiStub = sinon.stub(pixi, 'getPixi'); getPixiStub.resolves(undefined); @@ -678,7 +677,7 @@ suite('End to End Tests: test adapters', () => { }); test('pytest execution adapter small workspace with correct output', async () => { // result resolver and saved data for assertions - resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); let callCount = 0; let failureOccurred = false; let failureMsg = ''; @@ -875,7 +874,7 @@ suite('End to End Tests: test adapters', () => { }); test('pytest execution adapter large workspace', async () => { // result resolver and saved data for assertions - resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); let callCount = 0; let failureOccurred = false; let failureMsg = ''; @@ -1062,83 +1061,6 @@ suite('End to End Tests: test adapters', () => { assert.strictEqual(failureOccurred, false, failureMsg); }); }); - test('unittest execution adapter seg fault error handling', async () => { - resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); - let callCount = 0; - let failureOccurred = false; - let failureMsg = ''; - resultResolver._resolveExecution = async (data, _token?) => { - // do the following asserts for each time resolveExecution is called, should be called once per test. - callCount = callCount + 1; - traceLog(`unittest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); - try { - if (data.status === 'error') { - if (data.error === undefined) { - // Dereference a NULL pointer - const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); - if (indexOfTest === -1) { - failureOccurred = true; - failureMsg = 'Expected test to have a null pointer'; - } - } else if (data.error.length === 0) { - failureOccurred = true; - failureMsg = "Expected errors in 'error' field"; - } - } else { - const indexOfTest = JSON.stringify(data.result).search('error'); - if (indexOfTest === -1) { - failureOccurred = true; - failureMsg = - 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.'; - } - } - if (data.result === undefined) { - failureOccurred = true; - failureMsg = 'Expected results to be present'; - } - // make sure the testID is found in the results - const indexOfTest = JSON.stringify(data).search('test_seg_fault.TestSegmentationFault.test_segfault'); - if (indexOfTest === -1) { - failureOccurred = true; - failureMsg = 'Expected testId to be present'; - } - } catch (err) { - failureMsg = err ? (err as Error).toString() : ''; - failureOccurred = true; - } - return Promise.resolve(); - }; - - const testId = `test_seg_fault.TestSegmentationFault.test_segfault`; - const testIds: string[] = [testId]; - - // set workspace to test workspace folder - workspaceUri = Uri.parse(rootPathErrorWorkspace); - configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; - - // run pytest execution - const executionAdapter = new UnittestTestExecutionAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); - const testRun = typeMoq.Mock.ofType(); - testRun - .setup((t) => t.token) - .returns( - () => - ({ - onCancellationRequested: () => undefined, - } as any), - ); - await executionAdapter - .runTests(workspaceUri, testIds, TestRunProfileKind.Run, testRun.object, pythonExecFactory) - .finally(() => { - assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); - assert.strictEqual(failureOccurred, false, failureMsg); - }); - }); test('pytest execution adapter seg fault error handling', async () => { resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); let callCount = 0; @@ -1169,7 +1091,7 @@ suite('End to End Tests: test adapters', () => { failureMsg = err ? (err as Error).toString() : ''; failureOccurred = true; } - // return Promise.resolve(); + return Promise.resolve(); }; const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 18680b123b21..538b77161483 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -45,14 +45,7 @@ suite('pytest test discovery adapter', () => { mockExtensionRootDir.setup((m) => m.toString()).returns(() => '/mocked/extension/root/dir'); utilsStartDiscoveryNamedPipeStub = sinon.stub(util, 'startDiscoveryNamedPipe'); - utilsStartDiscoveryNamedPipeStub.callsFake(() => - Promise.resolve({ - name: 'discoveryResultPipe-mockName', - dispose: () => { - /* no-op */ - }, - }), - ); + utilsStartDiscoveryNamedPipeStub.callsFake(() => Promise.resolve('discoveryResultPipe-mockName')); // constants expectedPath = path.join('/', 'my', 'test', 'path'); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index b3f226402b72..18cabcc96772 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -88,14 +88,7 @@ suite('pytest test execution adapter', () => { myTestPath = path.join('/', 'my', 'test', 'path', '/'); utilsStartRunResultNamedPipeStub = sinon.stub(util, 'startRunResultNamedPipe'); - utilsStartRunResultNamedPipeStub.callsFake(() => - Promise.resolve({ - name: 'runResultPipe-mockName', - dispose: () => { - /* no-op */ - }, - }), - ); + utilsStartRunResultNamedPipeStub.callsFake(() => Promise.resolve('runResultPipe-mockName')); }); teardown(() => { sinon.restore(); diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts index d8b685b13654..81480d08b2b8 100644 --- a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -71,6 +71,9 @@ suite('Execution Flow Run Adapters', () => { const { token } = cancellationToken; testRunMock.setup((t) => t.token).returns(() => token); + // run result pipe mocking and the related server close dispose + let deferredTillServerCloseTester: Deferred | undefined; + // // mock exec service and exec factory execServiceStub .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) @@ -97,11 +100,13 @@ suite('Execution Flow Run Adapters', () => { return Promise.resolve('named-pipe'); }); - // run result pipe mocking and the related server close dispose - let deferredTillServerCloseTester: Deferred | undefined; - utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, _token) => { + utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, token) => { deferredTillServerCloseTester = deferredTillServerClose; - return Promise.resolve({ name: 'named-pipes-socket-name', dispose: serverDisposeStub }); + token?.onCancellationRequested(() => { + deferredTillServerCloseTester?.resolve(); + }); + + return Promise.resolve('named-pipes-socket-name'); }); serverDisposeStub.callsFake(() => { console.log('server disposed'); @@ -127,9 +132,6 @@ suite('Execution Flow Run Adapters', () => { ); // wait for server to start to keep test from failing await deferredStartTestIdsNamedPipe.promise; - - // assert the server dispose function was called correctly - sinon.assert.calledOnce(serverDisposeStub); }); test(`Adapter ${adapter}: token called mid-debug resolves correctly`, async () => { // mock test run and cancelation token @@ -138,6 +140,9 @@ suite('Execution Flow Run Adapters', () => { const { token } = cancellationToken; testRunMock.setup((t) => t.token).returns(() => token); + // run result pipe mocking and the related server close dispose + let deferredTillServerCloseTester: Deferred | undefined; + // // mock exec service and exec factory execServiceStub .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) @@ -164,14 +169,12 @@ suite('Execution Flow Run Adapters', () => { return Promise.resolve('named-pipe'); }); - // run result pipe mocking and the related server close dispose - let deferredTillServerCloseTester: Deferred | undefined; utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, _token) => { deferredTillServerCloseTester = deferredTillServerClose; - return Promise.resolve({ - name: 'named-pipes-socket-name', - dispose: serverDisposeStub, + token?.onCancellationRequested(() => { + deferredTillServerCloseTester?.resolve(); }); + return Promise.resolve('named-pipes-socket-name'); }); serverDisposeStub.callsFake(() => { console.log('server disposed'); @@ -210,10 +213,6 @@ suite('Execution Flow Run Adapters', () => { ); // wait for server to start to keep test from failing await deferredStartTestIdsNamedPipe.promise; - - // TODO: fix the server disposal so it is called once not twice, - // currently not a problem but would be useful to improve clarity - sinon.assert.called(serverDisposeStub); }); }); }); diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index 4c92c8c0b682..a0ee65d57922 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -82,14 +82,7 @@ suite('Unittest test discovery adapter', () => { }; utilsStartDiscoveryNamedPipeStub = sinon.stub(util, 'startDiscoveryNamedPipe'); - utilsStartDiscoveryNamedPipeStub.callsFake(() => - Promise.resolve({ - name: 'discoveryResultPipe-mockName', - dispose: () => { - /* no-op */ - }, - }), - ); + utilsStartDiscoveryNamedPipeStub.callsFake(() => Promise.resolve('discoveryResultPipe-mockName')); }); teardown(() => { sinon.restore(); diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index f2c06cd13496..78dcb0229e45 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -87,14 +87,7 @@ suite('Unittest test execution adapter', () => { myTestPath = path.join('/', 'my', 'test', 'path', '/'); utilsStartRunResultNamedPipeStub = sinon.stub(util, 'startRunResultNamedPipe'); - utilsStartRunResultNamedPipeStub.callsFake(() => - Promise.resolve({ - name: 'runResultPipe-mockName', - dispose: () => { - /* no-op */ - }, - }), - ); + utilsStartRunResultNamedPipeStub.callsFake(() => Promise.resolve('runResultPipe-mockName')); }); teardown(() => { sinon.restore(); diff --git a/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py b/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py index bad7ff8fcbbd..80be80f023c2 100644 --- a/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py +++ b/src/testTestingRootWkspc/errorWorkspace/test_seg_fault.py @@ -7,11 +7,12 @@ class TestSegmentationFault(unittest.TestCase): def cause_segfault(self): + print("Causing a segmentation fault") ctypes.string_at(0) # Dereference a NULL pointer def test_segfault(self): - assert True self.cause_segfault() + assert True if __name__ == "__main__": diff --git a/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py index a76856ebb929..40c5de531f7c 100644 --- a/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py +++ b/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py @@ -14,3 +14,106 @@ def test_even(self): for i in range(0, 2000): with self.subTest(i=i): self.assertEqual(i % 2, 0) + + +# The repeated tests below are to test the unittest communication as it hits it maximum limit of bytes. + + +class NumberedTests1(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests2(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests3(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests4(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests5(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests6(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests7(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests8(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests9(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests10(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests11(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests12(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests13(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests14(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests15(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests16(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests17(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests18(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests19(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) + + +class NumberedTests20(unittest.TestCase): + def test_abc(self): + self.assertEqual(1 % 2, 0) From 520f39659d84cdb6242ddecdf3c6aa1bfac3dacd Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 7 Jan 2025 16:15:23 -0800 Subject: [PATCH 0848/1136] switch to use file path as key in building test tree (#24697) precursor for https://github.com/microsoft/vscode-python/issues/23933 --- python_files/vscode_pytest/__init__.py | 37 ++++++++++++++------------ 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 1e812e41d2ae..78526557ef1b 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -495,7 +495,7 @@ def build_test_tree(session: pytest.Session) -> TestNode: """ session_node = create_session_node(session) session_children_dict: dict[str, TestNode] = {} - file_nodes_dict: dict[Any, TestNode] = {} + file_nodes_dict: dict[str, TestNode] = {} class_nodes_dict: dict[str, TestNode] = {} function_nodes_dict: dict[str, TestNode] = {} @@ -544,11 +544,13 @@ def build_test_tree(session: pytest.Session) -> TestNode: function_test_node["children"].append(test_node) # Check if the parent node of the function is file, if so create/add to this file node. if isinstance(test_case.parent, pytest.File): + # calculate the parent path of the test case + parent_path = get_node_path(test_case.parent) try: - parent_test_case = file_nodes_dict[test_case.parent] + parent_test_case = file_nodes_dict[os.fspath(parent_path)] except KeyError: - parent_test_case = create_file_node(test_case.parent) - file_nodes_dict[test_case.parent] = parent_test_case + parent_test_case = create_file_node(parent_path) + file_nodes_dict[os.fspath(parent_path)] = parent_test_case if function_test_node not in parent_test_case["children"]: parent_test_case["children"].append(function_test_node) # If the parent is not a file, it is a class, add the function node as the test node to handle subsequent nesting. @@ -580,22 +582,24 @@ def build_test_tree(session: pytest.Session) -> TestNode: else: ERRORS.append(f"Test class {case_iter} has no parent") break + parent_path = get_node_path(parent_module) # Create a file node that has the last class as a child. try: - test_file_node: TestNode = file_nodes_dict[parent_module] + test_file_node: TestNode = file_nodes_dict[os.fspath(parent_path)] except KeyError: - test_file_node = create_file_node(parent_module) - file_nodes_dict[parent_module] = test_file_node + test_file_node = create_file_node(parent_path) + file_nodes_dict[os.fspath(parent_path)] = test_file_node # Check if the class is already a child of the file node. if test_class_node is not None and test_class_node not in test_file_node["children"]: test_file_node["children"].append(test_class_node) elif not hasattr(test_case, "callspec"): # This includes test cases that are pytest functions or a doctests. + parent_path = get_node_path(test_case.parent) try: - parent_test_case = file_nodes_dict[test_case.parent] + parent_test_case = file_nodes_dict[os.fspath(parent_path)] except KeyError: - parent_test_case = create_file_node(test_case.parent) - file_nodes_dict[test_case.parent] = parent_test_case + parent_test_case = create_file_node(parent_path) + file_nodes_dict[os.fspath(parent_path)] = parent_test_case parent_test_case["children"].append(test_node) created_files_folders_dict: dict[str, TestNode] = {} for file_node in file_nodes_dict.values(): @@ -753,18 +757,17 @@ def create_parameterized_function_node( } -def create_file_node(file_module: Any) -> TestNode: - """Creates a file node from a pytest file module. +def create_file_node(calculated_node_path: pathlib.Path) -> TestNode: + """Creates a file node from a path which has already been calculated using the get_node_path function. Keyword arguments: - file_module -- the pytest file module. + calculated_node_path -- the pytest file path. """ - node_path = get_node_path(file_module) return { - "name": node_path.name, - "path": node_path, + "name": calculated_node_path.name, + "path": calculated_node_path, "type_": "file", - "id_": os.fspath(node_path), + "id_": os.fspath(calculated_node_path), "children": [], } From 2ebfae9b4327c989880fe82dfb0bd03e645f2c8c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 8 Jan 2025 13:34:18 -0800 Subject: [PATCH 0849/1136] remove commands for python.refreshTensorBoard and python.launchTensorBoard (#24702) --- package.json | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/package.json b/package.json index 72e05327d8d4..7f7df96289d7 100644 --- a/package.json +++ b/package.json @@ -1147,12 +1147,6 @@ "command": "python.execInInteractiveWindowEnter", "key": "enter", "when": "!config.interactiveWindow.executeWithShiftEnter && isCompositeNotebook && activeEditor == 'workbench.editor.interactive' && !inlineChatFocused && !notebookCellListFocused" - }, - { - "command": "python.refreshTensorBoard", - "key": "ctrl+r", - "mac": "cmd+r", - "when": "python.hasActiveTensorBoardSession" } ], "languages": [ @@ -1302,20 +1296,6 @@ "title": "%python.command.python.execInREPL.title%", "when": "false" }, - { - "category": "Python", - "command": "python.launchTensorBoard", - "title": "%python.command.python.launchTensorBoard.title%", - "when": "!virtualWorkspace && shellExecutionSupported && !python.tensorboardExtInstalled" - }, - { - "category": "Python", - "command": "python.refreshTensorBoard", - "enablement": "python.hasActiveTensorBoardSession", - "icon": "$(refresh)", - "title": "%python.command.python.refreshTensorBoard.title%", - "when": "!virtualWorkspace && shellExecutionSupported && !python.tensorboardExtInstalled" - }, { "category": "Python", "command": "python.reportIssue", @@ -1414,13 +1394,6 @@ "when": "editorFocus && editorLangId == python && !virtualWorkspace && shellExecutionSupported && config.python.REPL.sendToNativeREPL" } ], - "editor/title": [ - { - "command": "python.refreshTensorBoard", - "group": "navigation@0", - "when": "python.hasActiveTensorBoardSession && !virtualWorkspace && shellExecutionSupported" - } - ], "editor/title/run": [ { "command": "python.execInTerminal-icon", From dcfcdc2c1fbe95ba08c6c2ceef5da250fe050fae Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 8 Jan 2025 14:50:09 -0800 Subject: [PATCH 0850/1136] support pytest-ruff plugin for testing (#24698) half fixes https://github.com/microsoft/vscode-python/issues/23933 --- build/test-requirements.txt | 3 + .../.data/folder_with_script/script_random.py | 7 ++ .../.data/folder_with_script/test_simple.py | 7 ++ .../expected_discovery_test_output.py | 91 +++++++++++++++++++ .../tests/pytestadapter/test_discovery.py | 27 ++++++ python_files/vscode_pytest/__init__.py | 9 +- .../testController/common/resultResolver.ts | 16 ++-- .../testing/testController/common/utils.ts | 8 +- 8 files changed, 150 insertions(+), 18 deletions(-) create mode 100644 python_files/tests/pytestadapter/.data/folder_with_script/script_random.py create mode 100644 python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py diff --git a/build/test-requirements.txt b/build/test-requirements.txt index af19987bc8cb..8b0ea1636157 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -36,3 +36,6 @@ pytest-json # for pytest-describe related tests pytest-describe + +# for pytest-ruff related tests +pytest-ruff diff --git a/python_files/tests/pytestadapter/.data/folder_with_script/script_random.py b/python_files/tests/pytestadapter/.data/folder_with_script/script_random.py new file mode 100644 index 000000000000..d8c32027a9e6 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/folder_with_script/script_random.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# This file has no test, it's just a random script. + +if __name__ == "__main__": + print("Hello World!") diff --git a/python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py b/python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py new file mode 100644 index 000000000000..9f9bfb014f3d --- /dev/null +++ b/python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +# This test passes. +def test_function(): # test_marker--test_function + assert 1 == 1 diff --git a/python_files/tests/pytestadapter/expected_discovery_test_output.py b/python_files/tests/pytestadapter/expected_discovery_test_output.py index aa74a424ea2a..d7e82acc6890 100644 --- a/python_files/tests/pytestadapter/expected_discovery_test_output.py +++ b/python_files/tests/pytestadapter/expected_discovery_test_output.py @@ -1577,3 +1577,94 @@ ], "id_": TEST_DATA_PATH_STR, } +# This is the expected output for the folder_with_script folder when run with ruff +# └── .data +# └── folder_with_script +# └── script_random.py +# └── ruff +# └── test_simple.py +# └── ruff +# └── test_function +ruff_test_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "folder_with_script", + "path": os.fspath(TEST_DATA_PATH / "folder_with_script"), + "type_": "folder", + "id_": os.fspath(TEST_DATA_PATH / "folder_with_script"), + "children": [ + { + "name": "script_random.py", + "path": os.fspath(TEST_DATA_PATH / "folder_with_script" / "script_random.py"), + "type_": "file", + "id_": os.fspath(TEST_DATA_PATH / "folder_with_script" / "script_random.py"), + "children": [ + { + "name": "ruff", + "path": os.fspath( + TEST_DATA_PATH / "folder_with_script" / "script_random.py" + ), + "lineno": "", + "type_": "test", + "id_": get_absolute_test_id( + "folder_with_script/script_random.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "script_random.py", + ), + "runID": get_absolute_test_id( + "folder_with_script/script_random.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "script_random.py", + ), + } + ], + }, + { + "name": "test_simple.py", + "path": os.fspath(TEST_DATA_PATH / "folder_with_script" / "test_simple.py"), + "type_": "file", + "id_": os.fspath(TEST_DATA_PATH / "folder_with_script" / "test_simple.py"), + "children": [ + { + "name": "ruff", + "path": os.fspath( + TEST_DATA_PATH / "folder_with_script" / "test_simple.py" + ), + "lineno": "", + "type_": "test", + "id_": get_absolute_test_id( + "folder_with_script/test_simple.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + "runID": get_absolute_test_id( + "folder_with_script/test_simple.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + }, + { + "name": "test_function", + "path": os.fspath( + TEST_DATA_PATH / "folder_with_script" / "test_simple.py" + ), + "lineno": find_test_line_number( + "test_function", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + "type_": "test", + "id_": get_absolute_test_id( + "folder_with_script/test_simple.py::test_function", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + "runID": get_absolute_test_id( + "folder_with_script/test_simple.py::test_function", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + }, + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} diff --git a/python_files/tests/pytestadapter/test_discovery.py b/python_files/tests/pytestadapter/test_discovery.py index 276753149410..a0ba9c289864 100644 --- a/python_files/tests/pytestadapter/test_discovery.py +++ b/python_files/tests/pytestadapter/test_discovery.py @@ -329,3 +329,30 @@ def test_config_sub_folder(): if actual_item.get("tests") is not None: tests: Any = actual_item.get("tests") assert tests.get("name") == "config_sub_folder" + + +def test_ruff_plugin(): + """Here the session node will be a subfolder of the workspace root and the test are in another subfolder. + + This tests checks to see if test node path are under the session node and if so the + session node is correctly updated to the common path. + """ + file_path = helpers.TEST_DATA_PATH / "folder_with_script" + actual = helpers.runner( + [os.fspath(file_path), "--collect-only", "--ruff"], + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert ( + actual_item.get("status") == "success" + ), f"Status is not 'success', error is: {actual_item.get('error')}" + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) + assert is_same_tree( + actual_item.get("tests"), + expected_discovery_test_output.ruff_test_expected_output, + ["id_", "lineno", "name", "runID"], + ), f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.ruff_test_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 78526557ef1b..0ba5fd62221a 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -10,14 +10,7 @@ import pathlib import sys import traceback -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Generator, - Literal, - TypedDict, -) +from typing import TYPE_CHECKING, Any, Dict, Generator, Literal, TypedDict import pytest diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 2ce6039adba0..80e57edbabd2 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -189,8 +189,10 @@ export class PythonResultResolver implements ITestResultResolver { // search through freshly built array of testItem to find the failed test and update UI. testCases.forEach((indiItem) => { if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { - message.location = new Location(indiItem.uri, indiItem.range); + if (indiItem.uri) { + if (indiItem.range) { + message.location = new Location(indiItem.uri, indiItem.range); + } runInstance.errored(indiItem, message); } } @@ -210,8 +212,10 @@ export class PythonResultResolver implements ITestResultResolver { // search through freshly built array of testItem to find the failed test and update UI. testCases.forEach((indiItem) => { if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { - message.location = new Location(indiItem.uri, indiItem.range); + if (indiItem.uri) { + if (indiItem.range) { + message.location = new Location(indiItem.uri, indiItem.range); + } runInstance.failed(indiItem, message); } } @@ -222,7 +226,7 @@ export class PythonResultResolver implements ITestResultResolver { if (grabTestItem !== undefined) { testCases.forEach((indiItem) => { if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { + if (indiItem.uri) { runInstance.passed(grabTestItem); } } @@ -234,7 +238,7 @@ export class PythonResultResolver implements ITestResultResolver { if (grabTestItem !== undefined) { testCases.forEach((indiItem) => { if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { + if (indiItem.uri) { runInstance.skipped(grabTestItem); } } diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index b6848d0245dc..68e10a2213d6 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -195,10 +195,10 @@ export function populateTestTree( const testItem = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); testItem.tags = [RunTestTag, DebugTestTag]; - const range = new Range( - new Position(Number(child.lineno) - 1, 0), - new Position(Number(child.lineno), 0), - ); + let range: Range | undefined; + if (child.lineno) { + range = new Range(new Position(Number(child.lineno) - 1, 0), new Position(Number(child.lineno), 0)); + } testItem.canResolveChildren = false; testItem.range = range; testItem.tags = [RunTestTag, DebugTestTag]; From e772738225ade7f604b87511decd72f3a10e21f4 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Thu, 9 Jan 2025 14:18:16 -0800 Subject: [PATCH 0851/1136] remove stale PR check (#24708) The script isn't working and seems to not be worth the effort --- .github/workflows/stale-prs.yml | 51 ------------------- .../tests/pytestadapter/test_discovery.py | 40 +++++++++------ .../tests/pytestadapter/test_execution.py | 12 ++--- .../tests/unittestadapter/test_discovery.py | 6 +-- 4 files changed, 33 insertions(+), 76 deletions(-) delete mode 100644 .github/workflows/stale-prs.yml diff --git a/.github/workflows/stale-prs.yml b/.github/workflows/stale-prs.yml deleted file mode 100644 index e3a2d8600159..000000000000 --- a/.github/workflows/stale-prs.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Warn about month-old PRs - -on: - schedule: - - cron: '0 0 */2 * *' # Runs every other day at midnight - -jobs: - stale-prs: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Warn about stale PRs - uses: actions/github-script@v7 - with: - script: | - const { Octokit } = require("@octokit/rest"); - const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); - - const owner = context.repo.owner; - const repo = context.repo.repo; - const staleTime = new Date(); - staleTime.setMonth(staleTime.getMonth() - 1); - - const prs = await octokit.pulls.list({ - owner, - repo, - state: 'open' - }); - - for (const pr of prs.data) { - const comments = await octokit.issues.listComments({ - owner, - repo, - issue_number: pr.number - }); - - const lastComment = comments.data.length > 0 ? new Date(comments.data[comments.data.length - 1].created_at) : new Date(pr.created_at); - - if (lastComment < staleTime) { - await octokit.issues.createComment({ - owner, - repo, - issue_number: pr.number, - body: 'This PR has been stale for over a month. Please update or close it.' - }); - } - } - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/python_files/tests/pytestadapter/test_discovery.py b/python_files/tests/pytestadapter/test_discovery.py index a0ba9c289864..4f9fe3eb19ac 100644 --- a/python_files/tests/pytestadapter/test_discovery.py +++ b/python_files/tests/pytestadapter/test_discovery.py @@ -195,15 +195,17 @@ def test_pytest_collect(file, expected_const): if actual_list is not None: actual_item = actual_list.pop(0) assert all(item in actual_item for item in ("status", "cwd", "error")) - assert ( - actual_item.get("status") == "success" - ), f"Status is not 'success', error is: {actual_item.get('error')}" + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) assert is_same_tree( actual_item.get("tests"), expected_const, ["id_", "lineno", "name", "runID"], - ), f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_const, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_const, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) @pytest.mark.skipif( @@ -232,13 +234,13 @@ def test_symlink_root_dir(): actual_item = actual_list.pop(0) try: # Check if all requirements - assert all( - item in actual_item for item in ("status", "cwd", "error") - ), "Required keys are missing" + assert all(item in actual_item for item in ("status", "cwd", "error")), ( + "Required keys are missing" + ) assert actual_item.get("status") == "success", "Status is not 'success'" - assert actual_item.get("cwd") == os.fspath( - destination - ), f"CWD does not match: {os.fspath(destination)}" + assert actual_item.get("cwd") == os.fspath(destination), ( + f"CWD does not match: {os.fspath(destination)}" + ) assert actual_item.get("tests") == expected, "Tests do not match expected value" except AssertionError as e: # Print the actual_item in JSON format if an assertion fails @@ -271,7 +273,9 @@ def test_pytest_root_dir(): actual_item.get("tests"), expected_discovery_test_output.root_with_config_expected_output, ["id_", "lineno", "name", "runID"], - ), f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.root_with_config_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.root_with_config_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) def test_pytest_config_file(): @@ -298,7 +302,9 @@ def test_pytest_config_file(): actual_item.get("tests"), expected_discovery_test_output.root_with_config_expected_output, ["id_", "lineno", "name", "runID"], - ), f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.root_with_config_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.root_with_config_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) def test_config_sub_folder(): @@ -347,12 +353,14 @@ def test_ruff_plugin(): if actual_list is not None: actual_item = actual_list.pop(0) assert all(item in actual_item for item in ("status", "cwd", "error")) - assert ( - actual_item.get("status") == "success" - ), f"Status is not 'success', error is: {actual_item.get('error')}" + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) assert is_same_tree( actual_item.get("tests"), expected_discovery_test_output.ruff_test_expected_output, ["id_", "lineno", "name", "runID"], - ), f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.ruff_test_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.ruff_test_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) diff --git a/python_files/tests/pytestadapter/test_execution.py b/python_files/tests/pytestadapter/test_execution.py index 27fd1160441b..95a66e0e7b87 100644 --- a/python_files/tests/pytestadapter/test_execution.py +++ b/python_files/tests/pytestadapter/test_execution.py @@ -258,13 +258,13 @@ def test_symlink_run(): actual_item = actual_list.pop(0) try: # Check if all requirements - assert all( - item in actual_item for item in ("status", "cwd", "result") - ), "Required keys are missing" + assert all(item in actual_item for item in ("status", "cwd", "result")), ( + "Required keys are missing" + ) assert actual_item.get("status") == "success", "Status is not 'success'" - assert actual_item.get("cwd") == os.fspath( - destination - ), f"CWD does not match: {os.fspath(destination)}" + assert actual_item.get("cwd") == os.fspath(destination), ( + f"CWD does not match: {os.fspath(destination)}" + ) actual_result_dict = {} actual_result_dict.update(actual_item["result"]) assert actual_result_dict == expected_const diff --git a/python_files/tests/unittestadapter/test_discovery.py b/python_files/tests/unittestadapter/test_discovery.py index 972556de999b..a10b5c406680 100644 --- a/python_files/tests/unittestadapter/test_discovery.py +++ b/python_files/tests/unittestadapter/test_discovery.py @@ -314,9 +314,9 @@ def test_simple_django_collect(): if actual_list is not None: actual_item = actual_list.pop(0) assert all(item in actual_item for item in ("status", "cwd")) - assert ( - actual_item.get("status") == "success" - ), f"Status is not 'success', error is: {actual_item.get('error')}" + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) assert actual_item.get("cwd") == os.fspath(data_path) assert len(actual_item["tests"]["children"]) == 1 assert actual_item["tests"]["children"][0]["children"][0]["id_"] == os.fsdecode( From 9bc9f68410739031da688b56922235b3d13ff487 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 10 Jan 2025 13:05:09 -0800 Subject: [PATCH 0852/1136] stray debugging print left behind (#24710) mistakenly committed and needs to be removed --- python_files/unittestadapter/pvsc_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python_files/unittestadapter/pvsc_utils.py b/python_files/unittestadapter/pvsc_utils.py index 34b8553600f1..4d1cbfb5e110 100644 --- a/python_files/unittestadapter/pvsc_utils.py +++ b/python_files/unittestadapter/pvsc_utils.py @@ -350,7 +350,6 @@ def send_post_request( encoded = request.encode("utf-8") bytes_written = 0 while bytes_written < len(encoded): - print("writing more bytes!") segment = encoded[bytes_written : bytes_written + size] bytes_written += __writer.write(segment) __writer.flush() From 74a5cad0f3390d83eeb8b75b42c6531acde23917 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 10 Jan 2025 14:53:30 -0800 Subject: [PATCH 0853/1136] Restrict conda binary to be from PATH or Settings (#24709) Closes https://github.com/microsoft/vscode-python/issues/24627 --------- Co-authored-by: Eleanor Boyd --- src/client/common/utils/platform.ts | 8 ++++ .../common/environmentManagers/conda.ts | 6 +++ src/client/pythonEnvironments/nativeAPI.ts | 37 +++++++++++++++++-- .../pythonEnvironments/nativeAPI.unit.test.ts | 11 +++++- 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/client/common/utils/platform.ts b/src/client/common/utils/platform.ts index c86f5ff9364e..a1a49ba3c427 100644 --- a/src/client/common/utils/platform.ts +++ b/src/client/common/utils/platform.ts @@ -71,3 +71,11 @@ export function getUserHomeDir(): string | undefined { export function isWindows(): boolean { return getOSType() === OSType.Windows; } + +export function getPathEnvVariable(): string[] { + const value = getEnvironmentVariable('PATH') || getEnvironmentVariable('Path'); + if (value) { + return value.split(isWindows() ? ';' : ':'); + } + return []; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/src/client/pythonEnvironments/common/environmentManagers/conda.ts index bc60745dfeff..5301f82eda18 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -24,6 +24,7 @@ import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; import { splitLines } from '../../../common/stringUtils'; import { SpawnOptions } from '../../../common/process/types'; import { sleep } from '../../../common/utils/async'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; export const AnacondaCompanyName = 'Anaconda, Inc.'; export const CONDAPATH_SETTING_KEY = 'condaPath'; @@ -633,3 +634,8 @@ export async function getCondaEnvDirs(): Promise { const conda = await Conda.getConda(); return conda?.getEnvDirs(); } + +export function getCondaPathSetting(): string | undefined { + const config = getConfiguration('python'); + return config.get(CONDAPATH_SETTING_KEY, ''); +} diff --git a/src/client/pythonEnvironments/nativeAPI.ts b/src/client/pythonEnvironments/nativeAPI.ts index e069a3746ab6..a4a706fcb42b 100644 --- a/src/client/pythonEnvironments/nativeAPI.ts +++ b/src/client/pythonEnvironments/nativeAPI.ts @@ -20,14 +20,14 @@ import { NativePythonFinder, } from './base/locators/common/nativePythonFinder'; import { createDeferred, Deferred } from '../common/utils/async'; -import { Architecture, getUserHomeDir } from '../common/utils/platform'; +import { Architecture, getPathEnvVariable, getUserHomeDir } from '../common/utils/platform'; import { parseVersion } from './base/info/pythonVersion'; import { cache } from '../common/utils/decorators'; import { traceError, traceInfo, traceLog, traceWarn } from '../logging'; import { StopWatch } from '../common/utils/stopWatch'; import { FileChangeType } from '../common/platform/fileSystemWatcher'; import { categoryToKind, NativePythonEnvironmentKind } from './base/locators/common/nativePythonUtils'; -import { getCondaEnvDirs, setCondaBinary } from './common/environmentManagers/conda'; +import { getCondaEnvDirs, getCondaPathSetting, setCondaBinary } from './common/environmentManagers/conda'; import { setPyEnvBinary } from './common/environmentManagers/pyenv'; import { createPythonWatcher, @@ -166,6 +166,12 @@ function isSubDir(pathToCheck: string | undefined, parents: string[]): boolean { }); } +function foundOnPath(fsPath: string): boolean { + const paths = getPathEnvVariable().map((p) => path.normalize(p).toLowerCase()); + const normalized = path.normalize(fsPath).toLowerCase(); + return paths.some((p) => normalized.includes(p)); +} + function getName(nativeEnv: NativeEnvInfo, kind: PythonEnvKind, condaEnvDirs: string[]): string { if (nativeEnv.name) { return nativeEnv.name; @@ -387,13 +393,36 @@ class NativePythonEnvironments implements IDiscoveryAPI, Disposable { return undefined; } + private condaPathAlreadySet: string | undefined; + // eslint-disable-next-line class-methods-use-this private processEnvManager(native: NativeEnvManagerInfo) { const tool = native.tool.toLowerCase(); switch (tool) { case 'conda': - traceLog(`Conda environment manager found at: ${native.executable}`); - setCondaBinary(native.executable); + { + traceLog(`Conda environment manager found at: ${native.executable}`); + const settingPath = getCondaPathSetting(); + if (!this.condaPathAlreadySet) { + if (settingPath === '' || settingPath === undefined) { + if (foundOnPath(native.executable)) { + setCondaBinary(native.executable); + this.condaPathAlreadySet = native.executable; + traceInfo(`Using conda: ${native.executable}`); + } else { + traceInfo(`Conda not found on PATH, skipping: ${native.executable}`); + traceInfo( + 'You can set the path to conda using the setting: `python.condaPath` if you want to use a different conda binary', + ); + } + } else { + traceInfo(`Using conda from setting: ${settingPath}`); + this.condaPathAlreadySet = settingPath; + } + } else { + traceInfo(`Conda set to: ${this.condaPathAlreadySet}`); + } + } break; case 'pyenv': traceLog(`Pyenv environment manager found at: ${native.executable}`); diff --git a/src/test/pythonEnvironments/nativeAPI.unit.test.ts b/src/test/pythonEnvironments/nativeAPI.unit.test.ts index 678a8fcfe2e3..74811fa63bb6 100644 --- a/src/test/pythonEnvironments/nativeAPI.unit.test.ts +++ b/src/test/pythonEnvironments/nativeAPI.unit.test.ts @@ -13,7 +13,7 @@ import { NativeEnvManagerInfo, NativePythonFinder, } from '../../client/pythonEnvironments/base/locators/common/nativePythonFinder'; -import { Architecture, isWindows } from '../../client/common/utils/platform'; +import { Architecture, getPathEnvVariable, isWindows } from '../../client/common/utils/platform'; import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from '../../client/pythonEnvironments/base/info'; import { NativePythonEnvironmentKind } from '../../client/pythonEnvironments/base/locators/common/nativePythonUtils'; import * as condaApi from '../../client/pythonEnvironments/common/environmentManagers/conda'; @@ -25,6 +25,8 @@ suite('Native Python API', () => { let api: IDiscoveryAPI; let mockFinder: typemoq.IMock; let setCondaBinaryStub: sinon.SinonStub; + let getCondaPathSettingStub: sinon.SinonStub; + let getCondaEnvDirsStub: sinon.SinonStub; let setPyEnvBinaryStub: sinon.SinonStub; let createPythonWatcherStub: sinon.SinonStub; let mockWatcher: typemoq.IMock; @@ -136,6 +138,8 @@ suite('Native Python API', () => { setup(() => { setCondaBinaryStub = sinon.stub(condaApi, 'setCondaBinary'); + getCondaEnvDirsStub = sinon.stub(condaApi, 'getCondaEnvDirs'); + getCondaPathSettingStub = sinon.stub(condaApi, 'getCondaPathSetting'); setPyEnvBinaryStub = sinon.stub(pyenvApi, 'setPyEnvBinary'); getWorkspaceFoldersStub = sinon.stub(ws, 'getWorkspaceFolders'); getWorkspaceFoldersStub.returns([]); @@ -294,9 +298,12 @@ suite('Native Python API', () => { }); test('Setting conda binary', async () => { + getCondaPathSettingStub.returns(undefined); + getCondaEnvDirsStub.resolves(undefined); + const condaFakeDir = getPathEnvVariable()[0]; const condaMgr: NativeEnvManagerInfo = { tool: 'Conda', - executable: '/usr/bin/conda', + executable: path.join(condaFakeDir, 'conda'), }; mockFinder .setup((f) => f.refresh()) From 4d45042a5bd2967a42cb7b36451f4c9b5649b962 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 14 Jan 2025 13:46:32 -0800 Subject: [PATCH 0854/1136] Update release_plan.md (#24719) update 2025 release schedule --- .github/release_plan.md | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/.github/release_plan.md b/.github/release_plan.md index ecff43a28ee0..076cd64132dd 100644 --- a/.github/release_plan.md +++ b/.github/release_plan.md @@ -4,27 +4,22 @@ All dates should align with VS Code's [iteration](https://github.com/microsoft/v Feature freeze is Monday @ 17:00 America/Vancouver, XXX XX. At that point, commits to `main` should only be in response to bugs found during endgame testing until the release candidate is ready.
- Release Primary and Secondary Assignments for the 2024 Calendar Year - -| Month | Primary | Secondary | -|:----------|:----------|:------------| -| ~~January~~ | ~~Eleanor~~ | ~~Karthik~~ | -| ~~February~~ | ~~Kartik~~ | ~~Anthony~~ | -| ~~March~~ | ~~Karthik~~ | ~~Eleanor~~ | -| ~~April~~ | ~~Paula~~ | ~~Eleanor~~ | -| ~~May~~ | ~~Anthony~~ | ~~Karthik~~ | -| ~~June~~ | ~~Karthik~~ | ~~Eleanor~~ | -| July | Anthony | Karthik | -| August | Paula | Anthony | -| September | Anthony | Eleanor | -| October | Paula | Karthik | -| November | Eleanor | Paula | -| December | Eleanor | Anthony | - -Paula: 3 primary, 2 secondary -Eleanor: 3 primary (2 left), 3 secondary (2 left) -Anthony: 2 primary, 3 secondary (2 left) -Karthik: 2 primary (1 left), 4 secondary (3 left) + Release Primary and Secondary Assignments for the 2025 Calendar Year + +| Month and version number | Primary | Secondary | +|------------|----------|-----------| +| January v2025.0.0 | Eleanor | Karthik | +| February v2025.2.0 | Anthony | Eleanor | +| March v2025.4.0 | Karthik | Anthony | +| April v2025.6.0 | Eleanor | Karthik | +| May v2025.8.0 | Anthony | Eleanor | +| June v2025.10.0 | Karthik | Anthony | +| July v2025.12.0 | Eleanor | Karthik | +| August v2025.14.0 | Anthony | Eleanor | +| September v2025.16.0 | Karthik | Anthony | +| October v2025.18.0 | Eleanor | Karthik | +| November v2025.20.0 | Anthony | Eleanor | +| December v2025.22.0 | Karthik | Anthony |
From 8c54b8aa69e1f21a112fd55eaebd93368891125d Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 14 Jan 2025 23:55:40 +0000 Subject: [PATCH 0855/1136] Discovery cancellation (#24713) fixes https://github.com/Microsoft/vscode-python/issues/24602 --- .../testing/testController/common/types.ts | 11 +- .../pytest/pytestDiscoveryAdapter.ts | 55 ++++++-- .../pytest/pytestExecutionAdapter.ts | 18 +-- .../unittest/testDiscoveryAdapter.ts | 60 +++++++-- .../unittest/testExecutionAdapter.ts | 8 +- .../testController/workspaceTestAdapter.ts | 2 +- .../pytestDiscoveryAdapter.unit.test.ts | 81 +++++++++++- .../testDiscoveryAdapter.unit.test.ts | 119 ++++++++++++++---- 8 files changed, 280 insertions(+), 74 deletions(-) diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 692025a05f40..7139788a8177 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -156,18 +156,19 @@ export interface ITestResultResolver { } export interface ITestDiscoveryAdapter { // ** first line old method signature, second line new method signature - discoverTests(uri: Uri): Promise; + discoverTests(uri: Uri): Promise; discoverTests( uri: Uri, - executionFactory: IPythonExecutionFactory, + executionFactory?: IPythonExecutionFactory, + token?: CancellationToken, interpreter?: PythonEnvironment, - ): Promise; + ): Promise; } // interface for execution/runner adapter export interface ITestExecutionAdapter { // ** first line old method signature, second line new method signature - runTests(uri: Uri, testIds: string[], profileKind?: boolean | TestRunProfileKind): Promise; + runTests(uri: Uri, testIds: string[], profileKind?: boolean | TestRunProfileKind): Promise; runTests( uri: Uri, testIds: string[], @@ -176,7 +177,7 @@ export interface ITestExecutionAdapter { executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, interpreter?: PythonEnvironment, - ): Promise; + ): Promise; } // Same types as in python_files/unittestadapter/utils.py diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index ff73b31435a3..ef68f7d8039d 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -1,15 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as path from 'path'; -import { Uri } from 'vscode'; +import { CancellationToken, CancellationTokenSource, Uri } from 'vscode'; import * as fs from 'fs'; +import { ChildProcess } from 'child_process'; import { ExecutionFactoryCreateWithEnvironmentOptions, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; -import { Deferred } from '../../../common/utils/async'; +import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { traceError, traceInfo, traceVerbose, traceWarn } from '../../../logging'; import { DiscoveredTestPayload, ITestDiscoveryAdapter, ITestResultResolver } from '../common/types'; @@ -40,24 +41,39 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { async discoverTests( uri: Uri, executionFactory?: IPythonExecutionFactory, + token?: CancellationToken, interpreter?: PythonEnvironment, - ): Promise { - const name = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { - this.resultResolver?.resolveDiscovery(data); + ): Promise { + const cSource = new CancellationTokenSource(); + const deferredReturn = createDeferred(); + + token?.onCancellationRequested(() => { + traceInfo(`Test discovery cancelled.`); + cSource.cancel(); + deferredReturn.resolve(); }); - await this.runPytestDiscovery(uri, name, executionFactory, interpreter); + const name = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { + // if the token is cancelled, we don't want process the data + if (!token?.isCancellationRequested) { + this.resultResolver?.resolveDiscovery(data); + } + }, cSource.token); + + this.runPytestDiscovery(uri, name, cSource, executionFactory, interpreter, token).then(() => { + deferredReturn.resolve(); + }); - // this is only a placeholder to handle function overloading until rewrite is finished - const discoveryPayload: DiscoveredTestPayload = { cwd: uri.fsPath, status: 'success' }; - return discoveryPayload; + return deferredReturn.promise; } async runPytestDiscovery( uri: Uri, discoveryPipeName: string, + cSource: CancellationTokenSource, executionFactory?: IPythonExecutionFactory, interpreter?: PythonEnvironment, + token?: CancellationToken, ): Promise { const relativePathToPytest = 'python_files'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); @@ -111,6 +127,12 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { args: execArgs, env: (mutableEnv as unknown) as { [key: string]: string }, }); + token?.onCancellationRequested(() => { + traceInfo(`Test discovery cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); + proc.kill(); + deferredTillExecClose.resolve(); + cSource.cancel(); + }); proc.stdout.on('data', (data) => { const out = fixLogLinesNoTrailing(data.toString()); traceInfo(out); @@ -143,6 +165,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { throwOnStdErr: true, outputChannel: this.outputChannel, env: mutableEnv, + token, }; // Create the Python environment in which to execute the command. @@ -154,7 +177,21 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const execService = await executionFactory?.createActivatedEnvironment(creationOptions); const deferredTillExecClose: Deferred = createTestingDeferred(); + + let resultProc: ChildProcess | undefined; + + token?.onCancellationRequested(() => { + traceInfo(`Test discovery cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); + // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. + if (resultProc) { + resultProc?.kill(); + } else { + deferredTillExecClose.resolve(); + cSource.cancel(); + } + }); const result = execService?.execObservable(execArgs, spawnOptions); + resultProc = result?.proc; // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index b408280a576e..f66bff584fe2 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -38,7 +38,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, interpreter?: PythonEnvironment, - ): Promise { + ): Promise { const deferredTillServerClose: Deferred = utils.createTestingDeferred(); // create callback to handle data received on the named pipe @@ -59,12 +59,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { ); runInstance?.token.onCancellationRequested(() => { traceInfo(`Test run cancelled, resolving 'TillServerClose' deferred for ${uri.fsPath}.`); - const executionPayload: ExecutionTestPayload = { - cwd: uri.fsPath, - status: 'success', - error: '', - }; - return executionPayload; }); try { @@ -82,15 +76,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } finally { await deferredTillServerClose.promise; } - - // placeholder until after the rewrite is adopted - // TODO: remove after adoption. - const executionPayload: ExecutionTestPayload = { - cwd: uri.fsPath, - status: 'success', - error: '', - }; - return executionPayload; } private async runTestsNew( @@ -244,7 +229,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }); const result = execService?.execObservable(runArgs, spawnOptions); - resultProc = result?.proc; // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 04518e121651..73eb3f5aec2b 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -2,7 +2,9 @@ // Licensed under the MIT License. import * as path from 'path'; -import { Uri } from 'vscode'; +import { CancellationTokenSource, Uri } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { ChildProcess } from 'child_process'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { @@ -40,15 +42,31 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} - public async discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { + public async discoverTests( + uri: Uri, + executionFactory?: IPythonExecutionFactory, + token?: CancellationToken, + ): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; - const name = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { - this.resultResolver?.resolveDiscovery(data); + const cSource = new CancellationTokenSource(); + // Create a deferred to return to the caller + const deferredReturn = createDeferred(); + + token?.onCancellationRequested(() => { + traceInfo(`Test discovery cancelled.`); + cSource.cancel(); + deferredReturn.resolve(); }); + const name = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { + if (!token?.isCancellationRequested) { + this.resultResolver?.resolveDiscovery(data); + } + }, cSource.token); + // set up env with the pipe name let env: EnvironmentVariables | undefined = await this.envVarsService?.getEnvironmentVariables(uri); if (env === undefined) { @@ -62,17 +80,14 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { command, cwd, outChannel: this.outputChannel, + token, }; - try { - await this.runDiscovery(uri, options, name, cwd, executionFactory); - } finally { - // none - } - // placeholder until after the rewrite is adopted - // TODO: remove after adoption. - const discoveryPayload: DiscoveredTestPayload = { cwd, status: 'success' }; - return discoveryPayload; + this.runDiscovery(uri, options, name, cwd, cSource, executionFactory).then(() => { + deferredReturn.resolve(); + }); + + return deferredReturn.promise; } async runDiscovery( @@ -80,6 +95,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { options: TestCommandOptions, testRunPipeName: string, cwd: string, + cSource: CancellationTokenSource, executionFactory?: IPythonExecutionFactory, ): Promise { // get and edit env vars @@ -103,6 +119,12 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { args, env: (mutableEnv as unknown) as { [key: string]: string }, }); + options.token?.onCancellationRequested(() => { + traceInfo(`Test discovery cancelled, killing unittest subprocess for workspace ${uri.fsPath}`); + proc.kill(); + deferredTillExecClose.resolve(); + cSource.cancel(); + }); proc.stdout.on('data', (data) => { const out = fixLogLinesNoTrailing(data.toString()); traceInfo(out); @@ -148,7 +170,19 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + let resultProc: ChildProcess | undefined; + options.token?.onCancellationRequested(() => { + traceInfo(`Test discovery cancelled, killing unittest subprocess for workspace ${uri.fsPath}`); + // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. + if (resultProc) { + resultProc?.kill(); + } else { + deferredTillExecClose.resolve(); + cSource.cancel(); + } + }); const result = execService?.execObservable(args, spawnOptions); + resultProc = result?.proc; // Displays output to user and ensure the subprocess doesn't run into buffer overflow. // TODO: after a release, remove discovery output from the "Python Test Log" channel and send it to the "Python" channel instead. diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 6db36d96149f..e2b591379335 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -47,7 +47,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance?: TestRun, executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, - ): Promise { + ): Promise { // deferredTillServerClose awaits named pipe server close const deferredTillServerClose: Deferred = utils.createTestingDeferred(); @@ -87,12 +87,6 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { } finally { await deferredTillServerClose.promise; } - const executionPayload: ExecutionTestPayload = { - cwd: uri.fsPath, - status: 'success', - error: '', - }; - return executionPayload; } private async runTestsNew( diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index d8d6cb53d835..a73acdaba5f0 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -134,7 +134,7 @@ export class WorkspaceTestAdapter { try { // ** execution factory only defined for new rewrite way if (executionFactory !== undefined) { - await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory, interpreter); + await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory, token, interpreter); } else { await this.discoveryAdapter.discoverTests(this.workspaceUri); } diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 538b77161483..852942715270 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -2,7 +2,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as assert from 'assert'; -import { Uri } from 'vscode'; +import { Uri, CancellationTokenSource } from 'vscode'; import * as typeMoq from 'typemoq'; import * as path from 'path'; import { Observable } from 'rxjs/Observable'; @@ -13,6 +13,7 @@ import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testContr import { IPythonExecutionFactory, IPythonExecutionService, + // eslint-disable-next-line @typescript-eslint/no-unused-vars SpawnOptions, Output, } from '../../../../client/common/process/types'; @@ -31,11 +32,13 @@ suite('pytest test discovery adapter', () => { let outputChannel: typeMoq.IMock; let expectedPath: string; let uri: Uri; + // eslint-disable-next-line @typescript-eslint/no-unused-vars let expectedExtraVariables: Record; let mockProc: MockChildProcess; let deferred2: Deferred; let utilsStartDiscoveryNamedPipeStub: sinon.SinonStub; let useEnvExtensionStub: sinon.SinonStub; + let cancellationTokenSource: CancellationTokenSource; setup(() => { useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); @@ -86,9 +89,12 @@ suite('pytest test discovery adapter', () => { }, }; }); + + cancellationTokenSource = new CancellationTokenSource(); }); teardown(() => { sinon.restore(); + cancellationTokenSource.dispose(); }); test('Discovery should call exec with correct basic args', async () => { // set up exec mock @@ -333,4 +339,77 @@ suite('pytest test discovery adapter', () => { typeMoq.Times.once(), ); }); + test('Test discovery canceled before exec observable call finishes', async () => { + // set up exec mock + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + adapter = new PytestTestDiscoveryAdapter(configService, outputChannel.object); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // Trigger cancellation before exec observable call finishes + cancellationTokenSource.cancel(); + + await discoveryPromise; + + assert.ok( + true, + 'Test resolves correctly when triggering a cancellation token immediately after starting discovery.', + ); + }); + + test('Test discovery cancelled while exec observable is running and proc is closed', async () => { + // + const execService2 = typeMoq.Mock.ofType(); + execService2.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService2 + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + // Trigger cancellation while exec observable is running + cancellationTokenSource.cancel(); + return { + proc: mockProc as any, + out: new Observable>(), + dispose: () => { + /* no-body */ + }, + }; + }); + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService2.object); + }); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + adapter = new PytestTestDiscoveryAdapter(configService, outputChannel.object); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // add in await and trigger + await discoveryPromise; + assert.ok(true, 'Test resolves correctly when triggering a cancellation token in exec observable.'); + }); }); diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index a0ee65d57922..911a5f89afb4 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -4,8 +4,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as assert from 'assert'; import * as path from 'path'; -import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as fs from 'fs'; +import { CancellationTokenSource, Uri } from 'vscode'; import { Observable } from 'rxjs'; import * as sinon from 'sinon'; import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; @@ -23,38 +24,39 @@ import { import * as extapi from '../../../../client/envExt/api.internal'; suite('Unittest test discovery adapter', () => { - let stubConfigSettings: IConfigurationService; - let outputChannel: typemoq.IMock; + let configService: IConfigurationService; + let outputChannel: typeMoq.IMock; let mockProc: MockChildProcess; - let execService: typemoq.IMock; - let execFactory = typemoq.Mock.ofType(); + let execService: typeMoq.IMock; + let execFactory = typeMoq.Mock.ofType(); let deferred: Deferred; let expectedExtraVariables: Record; let expectedPath: string; let uri: Uri; let utilsStartDiscoveryNamedPipeStub: sinon.SinonStub; let useEnvExtensionStub: sinon.SinonStub; + let cancellationTokenSource: CancellationTokenSource; setup(() => { useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); useEnvExtensionStub.returns(false); expectedPath = path.join('/', 'new', 'cwd'); - stubConfigSettings = ({ + configService = ({ getSettings: () => ({ testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, }), } as unknown) as IConfigurationService; - outputChannel = typemoq.Mock.ofType(); + outputChannel = typeMoq.Mock.ofType(); // set up exec service with child process mockProc = new MockChildProcess('', ['']); const output = new Observable>(() => { /* no op */ }); - execService = typemoq.Mock.ofType(); + execService = typeMoq.Mock.ofType(); execService - .setup((x) => x.execObservable(typemoq.It.isAny(), typemoq.It.isAny())) + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => { deferred.resolve(); console.log('execObservable is returning'); @@ -66,10 +68,10 @@ suite('Unittest test discovery adapter', () => { }, }; }); - execFactory = typemoq.Mock.ofType(); + execFactory = typeMoq.Mock.ofType(); deferred = createDeferred(); execFactory - .setup((x) => x.createActivatedEnvironment(typemoq.It.isAny())) + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) .returns(() => Promise.resolve(execService.object)); execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); @@ -83,13 +85,15 @@ suite('Unittest test discovery adapter', () => { utilsStartDiscoveryNamedPipeStub = sinon.stub(util, 'startDiscoveryNamedPipe'); utilsStartDiscoveryNamedPipeStub.callsFake(() => Promise.resolve('discoveryResultPipe-mockName')); + cancellationTokenSource = new CancellationTokenSource(); }); teardown(() => { sinon.restore(); + cancellationTokenSource.dispose(); }); test('DiscoverTests should send the discovery command to the test server with the correct args', async () => { - const adapter = new UnittestTestDiscoveryAdapter(stubConfigSettings, outputChannel.object); + const adapter = new UnittestTestDiscoveryAdapter(configService, outputChannel.object); adapter.discoverTests(uri, execFactory.object); const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; @@ -100,7 +104,7 @@ suite('Unittest test discovery adapter', () => { execService.verify( (x) => x.execObservable( - typemoq.It.is>((argsActual) => { + typeMoq.It.is>((argsActual) => { try { assert.equal(argsActual.length, argsExpected.length); assert.deepEqual(argsActual, argsExpected); @@ -110,7 +114,7 @@ suite('Unittest test discovery adapter', () => { throw e; } }), - typemoq.It.is((options) => { + typeMoq.It.is((options) => { try { assert.deepEqual(options.env, expectedExtraVariables); assert.equal(options.cwd, expectedPath); @@ -122,17 +126,17 @@ suite('Unittest test discovery adapter', () => { } }), ), - typemoq.Times.once(), + typeMoq.Times.once(), ); }); test('DiscoverTests should respect settings.testings.cwd when present', async () => { const expectedNewPath = path.join('/', 'new', 'cwd'); - stubConfigSettings = ({ + configService = ({ getSettings: () => ({ testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'], cwd: expectedNewPath.toString() }, }), } as unknown) as IConfigurationService; - const adapter = new UnittestTestDiscoveryAdapter(stubConfigSettings, outputChannel.object); + const adapter = new UnittestTestDiscoveryAdapter(configService, outputChannel.object); adapter.discoverTests(uri, execFactory.object); const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; @@ -143,7 +147,7 @@ suite('Unittest test discovery adapter', () => { execService.verify( (x) => x.execObservable( - typemoq.It.is>((argsActual) => { + typeMoq.It.is>((argsActual) => { try { assert.equal(argsActual.length, argsExpected.length); assert.deepEqual(argsActual, argsExpected); @@ -153,7 +157,7 @@ suite('Unittest test discovery adapter', () => { throw e; } }), - typemoq.It.is((options) => { + typeMoq.It.is((options) => { try { assert.deepEqual(options.env, expectedExtraVariables); assert.equal(options.cwd, expectedNewPath); @@ -165,7 +169,80 @@ suite('Unittest test discovery adapter', () => { } }), ), - typemoq.Times.once(), + typeMoq.Times.once(), ); }); + test('Test discovery canceled before exec observable call finishes', async () => { + // set up exec mock + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + const adapter = new UnittestTestDiscoveryAdapter(configService, outputChannel.object); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // Trigger cancellation before exec observable call finishes + cancellationTokenSource.cancel(); + + await discoveryPromise; + + assert.ok( + true, + 'Test resolves correctly when triggering a cancellation token immediately after starting discovery.', + ); + }); + + test('Test discovery cancelled while exec observable is running and proc is closed', async () => { + // + const execService2 = typeMoq.Mock.ofType(); + execService2.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService2 + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + // Trigger cancellation while exec observable is running + cancellationTokenSource.cancel(); + return { + proc: mockProc as any, + out: new Observable>(), + dispose: () => { + /* no-body */ + }, + }; + }); + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService2.object); + }); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + const adapter = new UnittestTestDiscoveryAdapter(configService, outputChannel.object); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // add in await and trigger + await discoveryPromise; + assert.ok(true, 'Test resolves correctly when triggering a cancellation token in exec observable.'); + }); }); From 92cc4ed544235342dbfab433a4180c3ca46dfe56 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 21 Jan 2025 10:36:36 -0800 Subject: [PATCH 0856/1136] Update pylance telemetry for new experiment (#24731) Add telemetry for the computable pth experiment --- src/client/telemetry/pylance.ts | 896 ++++++++++++++++---------------- 1 file changed, 450 insertions(+), 446 deletions(-) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index 8b433673fd2f..778bfc97be12 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -1,446 +1,450 @@ -/* __GDPR__ - "language_server.enabled" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server.jinja_usage" : { - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } , - "openfileextensions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server.ready" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server.request" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "method" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "modulehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "moduleversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "resultlength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server.startup" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/analysis_complete" : { - "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "configparseerroroccurred" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "elapsedms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "externalmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "fatalerroroccurred" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "heaptotalmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "heapusedmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "isdone" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "isfirstrun" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "numfilesanalyzed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "numfilesinprogram" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "peakrssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolverid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "rssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "diagnosticsseen" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/analysis_exception" : { - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/completion_accepted" : { - "autoimport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "dictionarykey" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "memberaccess" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "keyword" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/completion_coverage" : { - "failures" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "overallfailures" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "overallsuccesses" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "overalltotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "successes" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/completion_metrics" : { - "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lastknownmembernamehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lastknownmodulehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "packagehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "unknownmembernamehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/completion_slow" : { - "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "correlationid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportadditiontimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportedittimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportimportaliascount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportimportaliastimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportindexcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportindextimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportindexused" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportitemcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportmoduleresolvetimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportmoduletimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportsymbolcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimporttotaltimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_autoimportuserindexcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_completionitems" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_completionitemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_extensiontotaltimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_selecteditemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_completiontype" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_filetype" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/exception_intellicode" : { - "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/execute_command" : { - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "name" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/goto_def_inside_string" : { - "resultlength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/import_heuristic" : { - "avgcost" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "avglevel" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "conflicts" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "nativemodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "nativepackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "reason_because_it_is_not_a_valid_directory" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "reason_could_not_parse_output" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "reason_did_not_find_file" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "reason_no_python_interpreter_search_path" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "reason_typeshed_path_not_found" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "success" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/import_metrics" : { - "absolutestubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "absolutetotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "absoluteunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "absoluteuserunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "builtinimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "builtinimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "localimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "localimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "nativemodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "nativepackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "relativestubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "relativetotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "relativeunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "stubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "thirdpartyimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "thirdpartyimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "unresolvedmodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "unresolvedpackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "unresolvedpackageslowercase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "unresolvedtotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/index_slow" : { - "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/installed_packages" : { - "packagesbitarray" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "packageslowercase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/intellicode_completion_item_selected" : { - "class" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.remotename" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "elapsedtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failurereason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "id" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "isintellicodecommit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "language" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "memoryincreasekb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "method" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "methods" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "modeltype" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "modelversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "selecteditemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/intellicode_enabled" : { - "common.remotename" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "enabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "startup" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/intellicode_model_load_failed" : { - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/intellicode_onnx_load_failed" : { - "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/rename_files" : { - "affectedfilescount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "filerenamed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/semantictokens_slow" : { - "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/server_side_request" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "method" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "modulehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "resultlength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/settings" : { - "addimportexactmatchonly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "aicodeactionsimplementabstractclasses" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "autoimportcompletions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "autosearchpaths" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "callArgumentNameInlayHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "completefunctionparens" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "disableTaggedHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "disableworkspacesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "enableextractcodeaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "enablePytestSupport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "extracommitchars" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "formatontype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "functionReturnInlayTypeHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "hasconfigfile" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "hasextrapaths" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "importformat" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "intelliCodeEnabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "includeusersymbolsinautoimport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "indexing" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "languageservermode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lspinteractivewindows" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "lspnotebooks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "movesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "nodeExecutable" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "openfilesonly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "pytestparameterinlaytypehints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "typecheckingmode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "unusablecompilerflags": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "useimportheuristic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "uselibrarycodefortypes" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "variableinlaytypehints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "watchforlibrarychanges" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "workspacecount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server/startup_metrics" : { - "analysisms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "peakrssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "presetfileopenms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokendeltams" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenfullms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenrangems" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "totalms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "userindexms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } -*/ -/* __GDPR__ - "language_server/workspaceindex_slow" : { - "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/* __GDPR__ - "language_server/workspaceindex_threshold_reached" : { - "index_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - } -*/ -/** - * Telemetry event sent when LSP server crashes - */ -/* __GDPR__ -"language_server.crash" : { - "oom" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rchiodo" }, - "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rchiodo" }, - "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } -} -*/ +/* __GDPR__ + "language_server.enabled" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server.jinja_usage" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } , + "openfileextensions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server.ready" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server.request" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "method" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "modulehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "moduleversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resultlength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server.startup" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/analysis_complete" : { + "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "configparseerroroccurred" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "elapsedms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "externalmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "fatalerroroccurred" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "heaptotalmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "heapusedmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "isdone" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "isfirstrun" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "numfilesanalyzed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "numfilesinprogram" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "peakrssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolverid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "rssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "diagnosticsseen" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "editablepthcount": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "computedpthcount": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + + } +*/ +/* __GDPR__ + "language_server/analysis_exception" : { + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/completion_accepted" : { + "autoimport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "dictionarykey" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "memberaccess" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "keyword" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/completion_coverage" : { + "failures" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "overallfailures" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "overallsuccesses" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "overalltotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "successes" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/completion_metrics" : { + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lastknownmembernamehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lastknownmodulehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "packagehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unknownmembernamehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/completion_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "correlationid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportadditiontimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportedittimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportimportaliascount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportimportaliastimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportindexcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportindextimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportindexused" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportitemcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportmoduleresolvetimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportmoduletimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportsymbolcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimporttotaltimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_autoimportuserindexcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_completionitems" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_completionitemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_extensiontotaltimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_selecteditemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_completiontype" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_filetype" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/exception_intellicode" : { + "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/execute_command" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "name" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/goto_def_inside_string" : { + "resultlength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/import_heuristic" : { + "avgcost" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "avglevel" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "conflicts" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "nativemodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "nativepackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason_because_it_is_not_a_valid_directory" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason_could_not_parse_output" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "reason_did_not_find_file" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason_no_python_interpreter_search_path" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "reason_typeshed_path_not_found" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "success" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/import_metrics" : { + "absolutestubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "absolutetotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "absoluteunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "absoluteuserunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "builtinimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "builtinimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "localimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "localimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "nativemodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "nativepackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "relativestubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "relativetotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "relativeunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "stubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "thirdpartyimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "thirdpartyimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "total" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unresolvedmodules" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unresolvedpackages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unresolvedpackageslowercase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unresolvedtotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/index_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/installed_packages" : { + "packagesbitarray" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "packageslowercase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "editablepthcount": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ +/* __GDPR__ + "language_server/intellicode_completion_item_selected" : { + "class" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "common.remotename" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "common.uikind" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "elapsedtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failurereason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "id" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "isintellicodecommit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "language" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "memoryincreasekb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "method" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "methods" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "modeltype" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "modelversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "selecteditemtelemetrybuildtimeinms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/intellicode_enabled" : { + "common.remotename" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "common.uikind" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "enabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "startup" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/intellicode_model_load_failed" : { + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/intellicode_onnx_load_failed" : { + "errorname" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorstack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/rename_files" : { + "affectedfilescount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "filerenamed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/semantictokens_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/server_side_request" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "method" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "modulehash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "resultlength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/settings" : { + "addimportexactmatchonly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "aicodeactionsimplementabstractclasses" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "autoimportcompletions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "autosearchpaths" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "callArgumentNameInlayHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "completefunctionparens" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "disableTaggedHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "disableworkspacesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "enableextractcodeaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "enablePytestSupport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "extracommitchars" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "formatontype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "functionReturnInlayTypeHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "hasconfigfile" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "hasextrapaths" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "importformat" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "intelliCodeEnabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "includeusersymbolsinautoimport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "indexing" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "languageservermode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lspinteractivewindows" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "lspnotebooks" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "movesymbol" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "nodeExecutable" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "openfilesonly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "pytestparameterinlaytypehints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "typecheckingmode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "unusablecompilerflags": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "useimportheuristic" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "uselibrarycodefortypes" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "variableinlaytypehints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "watchforlibrarychanges" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "workspacecount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ +/* __GDPR__ + "language_server/startup_metrics" : { + "analysisms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "peakrssmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "presetfileopenms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokendeltams" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenfullms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenrangems" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totalms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "userindexms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ +/* __GDPR__ + "language_server/workspaceindex_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "custom_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/* __GDPR__ + "language_server/workspaceindex_threshold_reached" : { + "index_count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ +/** + * Telemetry event sent when LSP server crashes + */ +/* __GDPR__ +"language_server.crash" : { + "oom" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rchiodo" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rchiodo" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } +} +*/ From 25411e54bfc3ff2a2ec4d9d801967a202e4d9b8d Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:47:14 -0500 Subject: [PATCH 0857/1136] Launch Native REPL using terminal link (#24734) Resolves: https://github.com/microsoft/vscode-python/issues/24270 Perhaps further improve via https://github.com/microsoft/vscode-python/issues/24270#issuecomment-2573609090 ? --- python_files/pythonrc.py | 5 ++ python_files/tests/test_shell_integration.py | 20 +++++ src/client/common/utils/localize.ts | 1 + src/client/common/vscodeApis/windowApis.ts | 5 ++ src/client/extensionActivation.ts | 2 + .../terminals/pythonStartupLinkProvider.ts | 44 +++++++++++ .../shellIntegration/pythonStartup.test.ts | 76 ++++++++++++++++++- 7 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 src/client/terminals/pythonStartupLinkProvider.ts diff --git a/python_files/pythonrc.py b/python_files/pythonrc.py index 13f374e023b8..0f552c86d375 100644 --- a/python_files/pythonrc.py +++ b/python_files/pythonrc.py @@ -77,3 +77,8 @@ def __str__(self): if sys.platform != "win32" and (not is_wsl) and use_shell_integration: sys.ps1 = PS1() + +if sys.platform == "darwin": + print("Cmd click to launch VS Code Native REPL") +else: + print("Ctrl click to launch VS Code Native REPL") diff --git a/python_files/tests/test_shell_integration.py b/python_files/tests/test_shell_integration.py index a7dfc2ff1a8f..376cb466bb50 100644 --- a/python_files/tests/test_shell_integration.py +++ b/python_files/tests/test_shell_integration.py @@ -61,3 +61,23 @@ def test_excepthook_call(): hooks.my_excepthook("mock_type", "mock_value", "mock_traceback") mock_excepthook.assert_called_once_with("mock_type", "mock_value", "mock_traceback") + + +if sys.platform == "darwin": + + def test_print_statement_darwin(monkeypatch): + importlib.reload(pythonrc) + with monkeypatch.context() as m: + m.setattr("builtins.print", Mock()) + importlib.reload(sys.modules["pythonrc"]) + print.assert_any_call("Cmd click to launch VS Code Native REPL") + + +if sys.platform == "win32": + + def test_print_statement_non_darwin(monkeypatch): + importlib.reload(pythonrc) + with monkeypatch.context() as m: + m.setattr("builtins.print", Mock()) + importlib.reload(sys.modules["pythonrc"]) + print.assert_any_call("Ctrl click to launch VS Code Native REPL") diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 1e5d28d778dc..18ab501f241b 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -92,6 +92,7 @@ export namespace AttachProcess { export namespace Repl { export const disableSmartSend = l10n.t('Disable Smart Send'); + export const launchNativeRepl = l10n.t('Launch VS Code Native REPL'); } export namespace Pylance { export const remindMeLater = l10n.t('Remind me later'); diff --git a/src/client/common/vscodeApis/windowApis.ts b/src/client/common/vscodeApis/windowApis.ts index 80825018c4a3..fc63a189f2ff 100644 --- a/src/client/common/vscodeApis/windowApis.ts +++ b/src/client/common/vscodeApis/windowApis.ts @@ -21,6 +21,7 @@ import { TerminalShellExecutionStartEvent, LogOutputChannel, OutputChannel, + TerminalLinkProvider, } from 'vscode'; import { createDeferred, Deferred } from '../utils/async'; import { Resource } from '../types'; @@ -258,3 +259,7 @@ export function createOutputChannel(name: string, languageId?: string): OutputCh export function createLogOutputChannel(name: string, options: { log: true }): LogOutputChannel { return window.createOutputChannel(name, options); } + +export function registerTerminalLinkProvider(provider: TerminalLinkProvider): Disposable { + return window.registerTerminalLinkProvider(provider); +} diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 38f2d6a56277..4a1acca62da5 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -55,6 +55,7 @@ import { registerReplCommands, registerReplExecuteOnEnter, registerStartNativeRe import { registerTriggerForTerminalREPL } from './terminals/codeExecution/terminalReplWatcher'; import { registerPythonStartup } from './terminals/pythonStartup'; import { registerPixiFeatures } from './pythonEnvironments/common/environmentManagers/pixi'; +import { registerCustomTerminalLinkProvider } from './terminals/pythonStartupLinkProvider'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -115,6 +116,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components): registerStartNativeReplCommand(ext.disposables, interpreterService); registerReplCommands(ext.disposables, interpreterService, executionHelper, commandManager); registerReplExecuteOnEnter(ext.disposables, interpreterService, commandManager); + registerCustomTerminalLinkProvider(ext.disposables); } /// ////////////////////////// diff --git a/src/client/terminals/pythonStartupLinkProvider.ts b/src/client/terminals/pythonStartupLinkProvider.ts new file mode 100644 index 000000000000..00dcbfd757aa --- /dev/null +++ b/src/client/terminals/pythonStartupLinkProvider.ts @@ -0,0 +1,44 @@ +/* eslint-disable class-methods-use-this */ +import { + CancellationToken, + Disposable, + ProviderResult, + TerminalLink, + TerminalLinkContext, + TerminalLinkProvider, +} from 'vscode'; +import { executeCommand } from '../common/vscodeApis/commandApis'; +import { registerTerminalLinkProvider } from '../common/vscodeApis/windowApis'; +import { Repl } from '../common/utils/localize'; + +interface CustomTerminalLink extends TerminalLink { + command: string; +} + +export class CustomTerminalLinkProvider implements TerminalLinkProvider { + provideTerminalLinks( + context: TerminalLinkContext, + _token: CancellationToken, + ): ProviderResult { + const links: CustomTerminalLink[] = []; + const expectedNativeLink = 'VS Code Native REPL'; + + if (context.line.includes(expectedNativeLink)) { + links.push({ + startIndex: context.line.indexOf(expectedNativeLink), + length: expectedNativeLink.length, + tooltip: Repl.launchNativeRepl, + command: 'python.startNativeREPL', + }); + } + return links; + } + + async handleTerminalLink(link: CustomTerminalLink): Promise { + await executeCommand(link.command); + } +} + +export function registerCustomTerminalLinkProvider(disposables: Disposable[]): void { + disposables.push(registerTerminalLinkProvider(new CustomTerminalLinkProvider())); +} diff --git a/src/test/terminals/shellIntegration/pythonStartup.test.ts b/src/test/terminals/shellIntegration/pythonStartup.test.ts index 5d25c2563cf9..35674e188cd9 100644 --- a/src/test/terminals/shellIntegration/pythonStartup.test.ts +++ b/src/test/terminals/shellIntegration/pythonStartup.test.ts @@ -3,10 +3,23 @@ import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import { GlobalEnvironmentVariableCollection, Uri, WorkspaceConfiguration } from 'vscode'; +import { + GlobalEnvironmentVariableCollection, + Uri, + WorkspaceConfiguration, + Disposable, + CancellationToken, + TerminalLinkContext, + Terminal, + EventEmitter, +} from 'vscode'; +import { assert } from 'chai'; import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; import { registerPythonStartup } from '../../../client/terminals/pythonStartup'; import { IExtensionContext } from '../../../client/common/types'; +import * as pythonStartupLinkProvider from '../../../client/terminals/pythonStartupLinkProvider'; +import { CustomTerminalLinkProvider } from '../../../client/terminals/pythonStartupLinkProvider'; +import { Repl } from '../../../client/common/utils/localize'; suite('Terminal - Shell Integration with PYTHONSTARTUP', () => { let getConfigurationStub: sinon.SinonStub; @@ -20,7 +33,6 @@ suite('Terminal - Shell Integration with PYTHONSTARTUP', () => { setup(() => { context = TypeMoq.Mock.ofType(); globalEnvironmentVariableCollection = TypeMoq.Mock.ofType(); - // Question: Why do we have to set up environmentVariableCollection and globalEnvironmentVariableCollection in this flip-flop way? // Reference: /vscode-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts context.setup((c) => c.environmentVariableCollection).returns(() => globalEnvironmentVariableCollection.object); @@ -122,4 +134,64 @@ suite('Terminal - Shell Integration with PYTHONSTARTUP', () => { globalEnvironmentVariableCollection.verify((c) => c.delete('PYTHONSTARTUP'), TypeMoq.Times.once()); }); + + test('Ensure registering terminal link calls registerTerminalLinkProvider', async () => { + const registerTerminalLinkProviderStub = sinon.stub( + pythonStartupLinkProvider, + 'registerCustomTerminalLinkProvider', + ); + const disposableArray: Disposable[] = []; + pythonStartupLinkProvider.registerCustomTerminalLinkProvider(disposableArray); + + sinon.assert.calledOnce(registerTerminalLinkProviderStub); + sinon.assert.calledWith(registerTerminalLinkProviderStub, disposableArray); + + registerTerminalLinkProviderStub.restore(); + }); + + test('Verify provideTerminalLinks returns links when context.line contains expectedNativeLink', () => { + const provider = new CustomTerminalLinkProvider(); + const context: TerminalLinkContext = { + line: 'Some random string with VS Code Native REPL in it', + terminal: {} as Terminal, + }; + const token: CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: new EventEmitter().event, + }; + + const links = provider.provideTerminalLinks(context, token); + + assert.isNotNull(links, 'Expected links to be not undefined'); + assert.isArray(links, 'Expected links to be an array'); + assert.isNotEmpty(links, 'Expected links to be not empty'); + + if (Array.isArray(links)) { + assert.equal(links[0].command, 'python.startNativeREPL', 'Expected command to be python.startNativeREPL'); + assert.equal( + links[0].startIndex, + context.line.indexOf('VS Code Native REPL'), + 'Expected startIndex to be 0', + ); + assert.equal(links[0].length, 'VS Code Native REPL'.length, 'Expected length to be 16'); + assert.equal(links[0].tooltip, Repl.launchNativeRepl, 'Expected tooltip to be Launch VS Code Native REPL'); + } + }); + + test('Verify provideTerminalLinks returns no links when context.line does not contain expectedNativeLink', () => { + const provider = new CustomTerminalLinkProvider(); + const context: TerminalLinkContext = { + line: 'Some random string without the expected link', + terminal: {} as Terminal, + }; + const token: CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: new EventEmitter().event, + }; + + const links = provider.provideTerminalLinks(context, token); + + assert.isArray(links, 'Expected links to be an array'); + assert.isEmpty(links, 'Expected links to be empty'); + }); }); From 43226840c47e5765f1b90ef2619bf516ae33eb68 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:30:13 -0500 Subject: [PATCH 0858/1136] Remove env var collection related debris in pythonStartup test (#24739) Resolves: https://github.com/microsoft/vscode-python/issues/24738 See explanation there. --- src/test/terminals/shellIntegration/pythonStartup.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/terminals/shellIntegration/pythonStartup.test.ts b/src/test/terminals/shellIntegration/pythonStartup.test.ts index 35674e188cd9..af90a1886bb5 100644 --- a/src/test/terminals/shellIntegration/pythonStartup.test.ts +++ b/src/test/terminals/shellIntegration/pythonStartup.test.ts @@ -33,8 +33,6 @@ suite('Terminal - Shell Integration with PYTHONSTARTUP', () => { setup(() => { context = TypeMoq.Mock.ofType(); globalEnvironmentVariableCollection = TypeMoq.Mock.ofType(); - // Question: Why do we have to set up environmentVariableCollection and globalEnvironmentVariableCollection in this flip-flop way? - // Reference: /vscode-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts context.setup((c) => c.environmentVariableCollection).returns(() => globalEnvironmentVariableCollection.object); context.setup((c) => c.storageUri).returns(() => Uri.parse('a')); From 803704e0ea775a96933e0377165a9c9ad1e32ed7 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:20:34 -0800 Subject: [PATCH 0859/1136] Bigger native repl suggestion link on terminal (#24751) Resolves: https://github.com/microsoft/vscode-python/issues/24749 --- .../terminals/pythonStartupLinkProvider.ts | 8 +- .../shellIntegration/pythonStartup.test.ts | 113 +++++++++++++----- 2 files changed, 91 insertions(+), 30 deletions(-) diff --git a/src/client/terminals/pythonStartupLinkProvider.ts b/src/client/terminals/pythonStartupLinkProvider.ts index 00dcbfd757aa..aba1270f1412 100644 --- a/src/client/terminals/pythonStartupLinkProvider.ts +++ b/src/client/terminals/pythonStartupLinkProvider.ts @@ -21,7 +21,13 @@ export class CustomTerminalLinkProvider implements TerminalLinkProvider { const links: CustomTerminalLink[] = []; - const expectedNativeLink = 'VS Code Native REPL'; + let expectedNativeLink; + + if (process.platform === 'darwin') { + expectedNativeLink = 'Cmd click to launch VS Code Native REPL'; + } else { + expectedNativeLink = 'Ctrl click to launch VS Code Native REPL'; + } if (context.line.includes(expectedNativeLink)) { links.push({ diff --git a/src/test/terminals/shellIntegration/pythonStartup.test.ts b/src/test/terminals/shellIntegration/pythonStartup.test.ts index af90a1886bb5..06364c9445aa 100644 --- a/src/test/terminals/shellIntegration/pythonStartup.test.ts +++ b/src/test/terminals/shellIntegration/pythonStartup.test.ts @@ -146,35 +146,90 @@ suite('Terminal - Shell Integration with PYTHONSTARTUP', () => { registerTerminalLinkProviderStub.restore(); }); - - test('Verify provideTerminalLinks returns links when context.line contains expectedNativeLink', () => { - const provider = new CustomTerminalLinkProvider(); - const context: TerminalLinkContext = { - line: 'Some random string with VS Code Native REPL in it', - terminal: {} as Terminal, - }; - const token: CancellationToken = { - isCancellationRequested: false, - onCancellationRequested: new EventEmitter().event, - }; - - const links = provider.provideTerminalLinks(context, token); - - assert.isNotNull(links, 'Expected links to be not undefined'); - assert.isArray(links, 'Expected links to be an array'); - assert.isNotEmpty(links, 'Expected links to be not empty'); - - if (Array.isArray(links)) { - assert.equal(links[0].command, 'python.startNativeREPL', 'Expected command to be python.startNativeREPL'); - assert.equal( - links[0].startIndex, - context.line.indexOf('VS Code Native REPL'), - 'Expected startIndex to be 0', - ); - assert.equal(links[0].length, 'VS Code Native REPL'.length, 'Expected length to be 16'); - assert.equal(links[0].tooltip, Repl.launchNativeRepl, 'Expected tooltip to be Launch VS Code Native REPL'); - } - }); + if (process.platform === 'darwin') { + test('Mac - Verify provideTerminalLinks returns links when context.line contains expectedNativeLink', () => { + const provider = new CustomTerminalLinkProvider(); + const context: TerminalLinkContext = { + line: 'Some random string with Cmd click to launch VS Code Native REPL', + terminal: {} as Terminal, + }; + const token: CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: new EventEmitter().event, + }; + + const links = provider.provideTerminalLinks(context, token); + + assert.isNotNull(links, 'Expected links to be not undefined'); + assert.isArray(links, 'Expected links to be an array'); + assert.isNotEmpty(links, 'Expected links to be not empty'); + + if (Array.isArray(links)) { + assert.equal( + links[0].command, + 'python.startNativeREPL', + 'Expected command to be python.startNativeREPL', + ); + assert.equal( + links[0].startIndex, + context.line.indexOf('Cmd click to launch VS Code Native REPL'), + 'start index should match', + ); + assert.equal( + links[0].length, + 'Cmd click to launch VS Code Native REPL'.length, + 'Match expected length', + ); + assert.equal( + links[0].tooltip, + Repl.launchNativeRepl, + 'Expected tooltip to be Launch VS Code Native REPL', + ); + } + }); + } + if (process.platform !== 'darwin') { + test('Windows/Linux - Verify provideTerminalLinks returns links when context.line contains expectedNativeLink', () => { + const provider = new CustomTerminalLinkProvider(); + const context: TerminalLinkContext = { + line: 'Some random string with Ctrl click to launch VS Code Native REPL', + terminal: {} as Terminal, + }; + const token: CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: new EventEmitter().event, + }; + + const links = provider.provideTerminalLinks(context, token); + + assert.isNotNull(links, 'Expected links to be not undefined'); + assert.isArray(links, 'Expected links to be an array'); + assert.isNotEmpty(links, 'Expected links to be not empty'); + + if (Array.isArray(links)) { + assert.equal( + links[0].command, + 'python.startNativeREPL', + 'Expected command to be python.startNativeREPL', + ); + assert.equal( + links[0].startIndex, + context.line.indexOf('Ctrl click to launch VS Code Native REPL'), + 'start index should match', + ); + assert.equal( + links[0].length, + 'Ctrl click to launch VS Code Native REPL'.length, + 'Match expected Length', + ); + assert.equal( + links[0].tooltip, + Repl.launchNativeRepl, + 'Expected tooltip to be Launch VS Code Native REPL', + ); + } + }); + } test('Verify provideTerminalLinks returns no links when context.line does not contain expectedNativeLink', () => { const provider = new CustomTerminalLinkProvider(); From 38527d65e64121b63b44e962b8f02e5189e4dc1f Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Sun, 2 Feb 2025 21:11:45 -0800 Subject: [PATCH 0860/1136] Resolve >= 3.13 failing REPL CI tests (#24775) Resolves: https://github.com/microsoft/vscode-python/issues/24773 --- .../terminals/codeExecution/helper.test.ts | 43 +++--- .../terminals/codeExecution/smartSend.test.ts | 130 +++++++++--------- 2 files changed, 90 insertions(+), 83 deletions(-) diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts index 166c4db12e7d..a43c5f8746ed 100644 --- a/src/test/terminals/codeExecution/helper.test.ts +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -33,12 +33,12 @@ import { IServiceContainer } from '../../../client/ioc/types'; import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper'; import { ICodeExecutionHelper } from '../../../client/terminals/types'; -import { PYTHON_PATH } from '../../common'; +import { PYTHON_PATH, getPythonSemVer } from '../../common'; import { ReplType } from '../../../client/repl/types'; const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'python_files', 'terminalExec'); -suite('Terminal - Code Execution Helper', () => { +suite('Terminal - Code Execution Helper', async () => { let activeResourceService: TypeMoq.IMock; let documentManager: TypeMoq.IMock; let applicationShell: TypeMoq.IMock; @@ -234,25 +234,28 @@ suite('Terminal - Code Execution Helper', () => { expect(normalizedCode).to.be.equal(normalizedExpected); } - ['', '1', '2', '3', '4', '5', '6', '7', '8'].forEach((fileNameSuffix) => { - test(`Ensure code is normalized (Sample${fileNameSuffix})`, async () => { - configurationService - .setup((c) => c.getSettings(TypeMoq.It.isAny())) - .returns({ - REPL: { - EnableREPLSmartSend: false, - REPLSmartSend: false, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - const code = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_raw.py`), 'utf8'); - const expectedCode = await fs.readFile( - path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized_selection.py`), - 'utf8', - ); - await ensureCodeIsNormalized(code, expectedCode); + const pythonTestVersion = await getPythonSemVer(); + if (pythonTestVersion && pythonTestVersion.minor < 13) { + ['', '1', '2', '3', '4', '5', '6', '7', '8'].forEach((fileNameSuffix) => { + test(`Ensure code is normalized (Sample${fileNameSuffix}) - Python < 3.13`, async () => { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: false, + REPLSmartSend: false, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const code = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_raw.py`), 'utf8'); + const expectedCode = await fs.readFile( + path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized_selection.py`), + 'utf8', + ); + await ensureCodeIsNormalized(code, expectedCode); + }); }); - }); + } test("Display message if there's no active file", async () => { documentManager.setup((doc) => doc.activeTextEditor).returns(() => undefined); diff --git a/src/test/terminals/codeExecution/smartSend.test.ts b/src/test/terminals/codeExecution/smartSend.test.ts index b81d581033aa..99ccd5d51d80 100644 --- a/src/test/terminals/codeExecution/smartSend.test.ts +++ b/src/test/terminals/codeExecution/smartSend.test.ts @@ -21,7 +21,7 @@ import { IServiceContainer } from '../../../client/ioc/types'; import { ICodeExecutionHelper } from '../../../client/terminals/types'; import { Commands, EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; -import { PYTHON_PATH } from '../../common'; +import { PYTHON_PATH, getPythonSemVer } from '../../common'; import { Architecture } from '../../../client/common/utils/platform'; import { ProcessService } from '../../../client/common/process/proc'; import { l10n } from '../../mocks/vsc'; @@ -29,7 +29,7 @@ import { ReplType } from '../../../client/repl/types'; const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'python_files', 'terminalExec'); -suite('REPL - Smart Send', () => { +suite('REPL - Smart Send', async () => { let documentManager: TypeMoq.IMock; let applicationShell: TypeMoq.IMock; @@ -168,67 +168,71 @@ suite('REPL - Smart Send', () => { commandManager.verifyAll(); }); - test('Smart send should perform smart selection and move cursor', async () => { - configurationService - .setup((c) => c.getSettings(TypeMoq.It.isAny())) - .returns({ - REPL: { - REPLSmartSend: true, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - - const activeEditor = TypeMoq.Mock.ofType(); - const firstIndexPosition = new Position(0, 0); - const selection = TypeMoq.Mock.ofType(); - const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); - - selection.setup((s) => s.anchor).returns(() => firstIndexPosition); - selection.setup((s) => s.active).returns(() => firstIndexPosition); - selection.setup((s) => s.isEmpty).returns(() => true); - activeEditor.setup((e) => e.selection).returns(() => selection.object); - - documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); - document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); - const actualProcessService = new ProcessService(); - - const { execObservable } = actualProcessService; - - processService - .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); - - const actualSmartOutput = await codeExecutionHelper.normalizeLines( - 'my_dict = {', - ReplType.terminal, - wholeFileContent, - ); - - // my_dict = { <----- smart shift+enter here - // "key1": "value1", - // "key2": "value2" - // } <---- cursor should be here afterwards, hence offset 3 - commandManager - .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny())) - .callback((_, arg2) => { - assert.deepEqual(arg2, { - to: 'down', - by: 'line', - value: 3, - }); - return Promise.resolve(); - }) - .verifiable(TypeMoq.Times.once()); - - commandManager - .setup((c) => c.executeCommand('cursorEnd')) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - - const expectedSmartOutput = 'my_dict = {\n "key1": "value1",\n "key2": "value2"\n}\n'; - expect(actualSmartOutput).to.be.equal(expectedSmartOutput); - commandManager.verifyAll(); - }); + const pythonTestVersion = await getPythonSemVer(); + + if (pythonTestVersion && pythonTestVersion.minor < 13) { + test('Smart send should perform smart selection and move cursor - Python < 3.13', async () => { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + REPLSmartSend: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => true); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + const actualSmartOutput = await codeExecutionHelper.normalizeLines( + 'my_dict = {', + ReplType.terminal, + wholeFileContent, + ); + + // my_dict = { <----- smart shift+enter here + // "key1": "value1", + // "key2": "value2" + // } <---- cursor should be here afterwards, hence offset 3 + commandManager + .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny())) + .callback((_, arg2) => { + assert.deepEqual(arg2, { + to: 'down', + by: 'line', + value: 3, + }); + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.once()); + + commandManager + .setup((c) => c.executeCommand('cursorEnd')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const expectedSmartOutput = 'my_dict = {\n "key1": "value1",\n "key2": "value2"\n}\n'; + expect(actualSmartOutput).to.be.equal(expectedSmartOutput); + commandManager.verifyAll(); + }); + } // Do not perform smart selection when there is explicit selection test('Smart send should not perform smart selection when there is explicit selection', async () => { From 6f1ea1d2979e48550ec6eea52128e4b68ca0ce17 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 3 Feb 2025 11:13:07 -0800 Subject: [PATCH 0861/1136] bump-release-2025.0 (#24778) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index eaeb530cc933..01d5d5799ec7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2024.23.0-dev", + "version": "2025.0.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2024.23.0-dev", + "version": "2025.0.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 7f7df96289d7..bac34ddf767a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2024.23.0-dev", + "version": "2025.0.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From d95649c151cbe92ede5c7c2aa249dffd66dafd2e Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Mon, 3 Feb 2025 12:03:00 -0800 Subject: [PATCH 0862/1136] Bump dev version 2025.1 (#24779) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01d5d5799ec7..4610d3957c88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.0.0-rc", + "version": "2025.1.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.0.0-rc", + "version": "2025.1.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index bac34ddf767a..ef656af5003e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.0.0-rc", + "version": "2025.1.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From e9b4b7b0f5260a6ffcfe473a291f9041cc4e9249 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 4 Feb 2025 11:01:58 -0800 Subject: [PATCH 0863/1136] update release plan to move release branching to prior thurs (#24781) --- .github/release_plan.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/release_plan.md b/.github/release_plan.md index 076cd64132dd..bc9e623bc774 100644 --- a/.github/release_plan.md +++ b/.github/release_plan.md @@ -24,8 +24,10 @@ Feature freeze is Monday @ 17:00 America/Vancouver, XXX XX. At that point, commi -# Release candidate (Monday, XXX XX) +# Release candidate (Thursday, XXX XX) +NOTE: This Thursday occurs during TESTING week. Branching should be done during this week to freeze the release with only the correct changes. Any last minute fixes go in as candidates into the release branch and will require team approval. +Other: NOTE: Third Party Notices are automatically added by our build pipelines using https://tools.opensource.microsoft.com/notice. NOTE: the number of this release is in the issue title and can be substituted in wherever you see [YYYY.minor]. From 6b784e53e70a30c0895cafd60c8ad76b2b087749 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:39:42 -0800 Subject: [PATCH 0864/1136] Use sendText to send Python code to Terminal REPL for Python >= 3.13 (#24765) Resolves: https://github.com/microsoft/vscode-python/issues/24674#issuecomment-2574108023 Use sendText to send Python code to Terminal REPL for Python >= 3.13 to prevent keyboard interrupt. Relevant file context from VS Code: https://github.com/microsoft/vscode/blob/f9c927cf7a29a59b896b6cdac2d8b5d2d43afea5/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts#L906 It seems like we are on this edge scenario where generic terminal shell integration is enabled (so executeCommand can be used), but we have temporarily disabled Python shell integration for Python >= 3.13 (and of course sending the relevant escape sequences such as the commandLine itself to VS Code). Why? * https://github.com/python/cpython/issues/126131 placing user's mouse cursor position at odd place. Why and where I think the keyboard interrupt is happening: Python extension tries to executeCommand when sending commands to terminal REPL >= Python3.13, where we are not sending shell integration escape sequences from the Python side. * I think this is why it is attaching the keyboard interrupt all the sudden, because VS Code see that Python extension is requesting executeCommand but is not sending the commandLine escape sequence to them. For every other versions < 3.13 (where we send all the shell integration escape sequences including the commandLine), this does not happen. --- src/client/common/terminal/service.ts | 11 +++- src/client/repl/replUtils.ts | 14 +++++ .../common/terminals/service.unit.test.ts | 53 ++++++++++++++++++- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index b02670836015..a051d66f015f 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -25,6 +25,7 @@ import { useEnvExtension } from '../../envExt/api.internal'; import { ensureTerminalLegacy } from '../../envExt/api.legacy'; import { sleep } from '../utils/async'; import { isWindows } from '../utils/platform'; +import { getPythonMinorVersion } from '../../repl/replUtils'; @injectable() export class TerminalService implements ITerminalService, Disposable { @@ -108,7 +109,15 @@ export class TerminalService implements ITerminalService, Disposable { const config = getConfiguration('python'); const pythonrcSetting = config.get('terminal.shellIntegration.enabled'); - if ((isPythonShell && !pythonrcSetting) || (isPythonShell && isWindows())) { + + const minorVersion = this.options?.resource + ? await getPythonMinorVersion( + this.options.resource, + this.serviceContainer.get(IInterpreterService), + ) + : undefined; + + if ((isPythonShell && !pythonrcSetting) || (isPythonShell && isWindows()) || (minorVersion ?? 0) >= 13) { // If user has explicitly disabled SI for Python, use sendText for inside Terminal REPL. terminal.sendText(commandLine); return undefined; diff --git a/src/client/repl/replUtils.ts b/src/client/repl/replUtils.ts index 0c2c4ba0d84e..8e23218c2870 100644 --- a/src/client/repl/replUtils.ts +++ b/src/client/repl/replUtils.ts @@ -118,3 +118,17 @@ export function getTabNameForUri(uri: Uri): string | undefined { return undefined; } + +/** + * Function that will return the minor version of current active Python interpreter. + */ +export async function getPythonMinorVersion( + uri: Uri | undefined, + interpreterService: IInterpreterService, +): Promise { + if (uri) { + const pythonVersion = await getActiveInterpreter(uri, interpreterService); + return pythonVersion?.version?.minor; + } + return undefined; +} diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts index 9903f6781f28..63a1cd544940 100644 --- a/src/test/common/terminals/service.unit.test.ts +++ b/src/test/common/terminals/service.unit.test.ts @@ -11,6 +11,7 @@ import { TerminalShellExecution, TerminalShellExecutionEndEvent, TerminalShellIntegration, + Uri, Terminal as VSCodeTerminal, WorkspaceConfiguration, } from 'vscode'; @@ -18,7 +19,12 @@ import { ITerminalManager, IWorkspaceService } from '../../../client/common/appl import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import { IPlatformService } from '../../../client/common/platform/types'; import { TerminalService } from '../../../client/common/terminal/service'; -import { ITerminalActivator, ITerminalHelper, TerminalShellType } from '../../../client/common/terminal/types'; +import { + ITerminalActivator, + ITerminalHelper, + TerminalCreationOptions, + TerminalShellType, +} from '../../../client/common/terminal/types'; import { IDisposableRegistry } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { ITerminalAutoActivation } from '../../../client/terminals/types'; @@ -26,6 +32,8 @@ import { createPythonInterpreter } from '../../utils/interpreters'; import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; import * as platform from '../../../client/common/utils/platform'; import * as extapi from '../../../client/envExt/api.internal'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; suite('Terminal Service', () => { let service: TerminalService; @@ -46,6 +54,8 @@ suite('Terminal Service', () => { let editorConfig: TypeMoq.IMock; let isWindowsStub: sinon.SinonStub; let useEnvExtensionStub: sinon.SinonStub; + let interpreterService: TypeMoq.IMock; + let options: TypeMoq.IMock; setup(() => { useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); @@ -92,6 +102,13 @@ suite('Terminal Service', () => { disposables = []; mockServiceContainer = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + + options = TypeMoq.Mock.ofType(); + options.setup((o) => o.resource).returns(() => Uri.parse('a')); mockServiceContainer.setup((c) => c.get(ITerminalManager)).returns(() => terminalManager.object); mockServiceContainer.setup((c) => c.get(ITerminalHelper)).returns(() => terminalHelper.object); @@ -100,6 +117,7 @@ suite('Terminal Service', () => { mockServiceContainer.setup((c) => c.get(IWorkspaceService)).returns(() => workspaceService.object); mockServiceContainer.setup((c) => c.get(ITerminalActivator)).returns(() => terminalActivator.object); mockServiceContainer.setup((c) => c.get(ITerminalAutoActivation)).returns(() => terminalAutoActivator.object); + mockServiceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); isWindowsStub = sinon.stub(platform, 'isWindows'); pythonConfig = TypeMoq.Mock.ofType(); @@ -117,6 +135,7 @@ suite('Terminal Service', () => { } disposables.filter((item) => !!item).forEach((item) => item.dispose()); sinon.restore(); + interpreterService.reset(); }); test('Ensure terminal is disposed', async () => { @@ -239,7 +258,7 @@ suite('Terminal Service', () => { terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); - test('Ensure sendText is NOT called when Python shell integration and terminal shell integration are both enabled - Mac, Linux', async () => { + test('Ensure sendText is NOT called when Python shell integration and terminal shell integration are both enabled - Mac, Linux && Python < 3.13', async () => { isWindowsStub.returns(false); pythonConfig .setup((p) => p.get('terminal.shellIntegration.enabled')) @@ -261,6 +280,36 @@ suite('Terminal Service', () => { terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.never()); }); + test('Ensure sendText is called when Python shell integration and terminal shell integration are both enabled - Mac, Linux && Python >= 3.13', async () => { + interpreterService.reset(); + + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ path: 'yo', version: { major: 3, minor: 13, patch: 0 } } as PythonEnvironment), + ); + + isWindowsStub.returns(false); + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + + service = new TerminalService(mockServiceContainer.object, options.object); + const textToSend = 'Some Text'; + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.ensureTerminal(); + await service.executeCommand(textToSend, true); + + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.once()); + }); + test('Ensure sendText IS called even when Python shell integration and terminal shell integration are both enabled - Window', async () => { isWindowsStub.returns(true); pythonConfig From b4e1ddb37bae6ffb49bd49d13cc3ffb98c147acd Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 5 Feb 2025 10:04:37 +1100 Subject: [PATCH 0865/1136] Jupyter API to get Env associated with Notebooks (#24771) See https://github.com/microsoft/vscode-jupyter/issues/15987 Should also fix https://github.com/microsoft/vscode-jupyter/issues/16112 Should also avoid Pylance having to monitor notebook changes and then trying to figure out the Environment for a Notebook. previous discussion here https://github.com/microsoft/vscode-python/pull/24358 --- pythonExtensionApi/src/main.ts | 4 +- src/client/api.ts | 13 ++- src/client/api/types.ts | 4 +- src/client/environmentApi.ts | 43 +++++++-- src/client/jupyter/jupyterIntegration.ts | 110 ++++++++++++++++++++++- src/test/api.functional.test.ts | 10 +++ src/test/environmentApi.unit.test.ts | 16 +++- 7 files changed, 182 insertions(+), 18 deletions(-) diff --git a/pythonExtensionApi/src/main.ts b/pythonExtensionApi/src/main.ts index 154ffbbd857a..2173245cbb28 100644 --- a/pythonExtensionApi/src/main.ts +++ b/pythonExtensionApi/src/main.ts @@ -227,9 +227,9 @@ export type EnvironmentsChangeEvent = { export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & { /** - * Workspace folder the environment changed for. + * Resource the environment changed for. */ - readonly resource: WorkspaceFolder | undefined; + readonly resource: Resource | undefined; }; /** diff --git a/src/client/api.ts b/src/client/api.ts index 899326647808..15fb4d688a89 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -15,7 +15,11 @@ import { IConfigurationService, Resource } from './common/types'; import { getDebugpyLauncherArgs } from './debugger/extension/adapter/remoteLaunchers'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer, IServiceManager } from './ioc/types'; -import { JupyterExtensionIntegration } from './jupyter/jupyterIntegration'; +import { + JupyterExtensionIntegration, + JupyterExtensionPythonEnvironments, + JupyterPythonEnvironmentApi, +} from './jupyter/jupyterIntegration'; import { traceError } from './logging'; import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { buildEnvironmentApi } from './environmentApi'; @@ -33,11 +37,16 @@ export function buildApi( const configurationService = serviceContainer.get(IConfigurationService); const interpreterService = serviceContainer.get(IInterpreterService); serviceManager.addSingleton(JupyterExtensionIntegration, JupyterExtensionIntegration); + serviceManager.addSingleton( + JupyterExtensionPythonEnvironments, + JupyterExtensionPythonEnvironments, + ); serviceManager.addSingleton( TensorboardExtensionIntegration, TensorboardExtensionIntegration, ); const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); + const jupyterPythonEnvApi = serviceContainer.get(JupyterExtensionPythonEnvironments); const tensorboardIntegration = serviceContainer.get( TensorboardExtensionIntegration, ); @@ -146,7 +155,7 @@ export function buildApi( stop: (client: BaseLanguageClient): Promise => client.stop(), getTelemetryReporter: () => getTelemetryReporter(), }, - environments: buildEnvironmentApi(discoveryApi, serviceContainer), + environments: buildEnvironmentApi(discoveryApi, serviceContainer, jupyterPythonEnvApi), }; // In test environment return the DI Container. diff --git a/src/client/api/types.ts b/src/client/api/types.ts index 4e67334121fb..95556aacbd90 100644 --- a/src/client/api/types.ts +++ b/src/client/api/types.ts @@ -227,9 +227,9 @@ export type EnvironmentsChangeEvent = { export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & { /** - * Workspace folder the environment changed for. + * Resource the environment changed for. */ - readonly resource: WorkspaceFolder | undefined; + readonly resource: Resource | undefined; }; /** diff --git a/src/client/environmentApi.ts b/src/client/environmentApi.ts index 6c4b5cf94d92..558938d7d0b7 100644 --- a/src/client/environmentApi.ts +++ b/src/client/environmentApi.ts @@ -33,6 +33,8 @@ import { } from './api/types'; import { buildEnvironmentCreationApi } from './pythonEnvironments/creation/createEnvApi'; import { EnvironmentKnownCache } from './environmentKnownCache'; +import type { JupyterPythonEnvironmentApi } from './jupyter/jupyterIntegration'; +import { noop } from './common/utils/misc'; type ActiveEnvironmentChangeEvent = { resource: WorkspaceFolder | undefined; @@ -115,6 +117,7 @@ function filterUsingVSCodeContext(e: PythonEnvInfo) { export function buildEnvironmentApi( discoveryApi: IDiscoveryAPI, serviceContainer: IServiceContainer, + jupyterPythonEnvsApi: JupyterPythonEnvironmentApi, ): PythonExtension['environments'] { const interpreterPathService = serviceContainer.get(IInterpreterPathService); const configService = serviceContainer.get(IConfigurationService); @@ -146,6 +149,28 @@ export function buildEnvironmentApi( }) .ignoreErrors(); } + + function getActiveEnvironmentPath(resource?: Resource) { + resource = resource && 'uri' in resource ? resource.uri : resource; + const jupyterEnv = + resource && jupyterPythonEnvsApi.getPythonEnvironment + ? jupyterPythonEnvsApi.getPythonEnvironment(resource) + : undefined; + if (jupyterEnv) { + traceVerbose('Python Environment returned from Jupyter', resource?.fsPath, jupyterEnv.id); + return { + id: jupyterEnv.id, + path: jupyterEnv.path, + }; + } + const path = configService.getSettings(resource).pythonPath; + const id = path === 'python' ? 'DEFAULT_PYTHON' : getEnvID(path); + return { + id, + path, + }; + } + disposables.push( discoveryApi.onProgress((e) => { if (e.stage === ProgressReportStage.discoveryFinished) { @@ -206,6 +231,16 @@ export function buildEnvironmentApi( }), onEnvironmentsChanged, onEnvironmentVariablesChanged, + jupyterPythonEnvsApi.onDidChangePythonEnvironment + ? jupyterPythonEnvsApi.onDidChangePythonEnvironment((e) => { + const jupyterEnv = getActiveEnvironmentPath(e); + onDidActiveInterpreterChangedEvent.fire({ + id: jupyterEnv.id, + path: jupyterEnv.path, + resource: e, + }); + }, undefined) + : { dispose: noop }, ); if (!knownCache!) { knownCache = initKnownCache(); @@ -223,13 +258,7 @@ export function buildEnvironmentApi( }, getActiveEnvironmentPath(resource?: Resource) { sendApiTelemetry('getActiveEnvironmentPath'); - resource = resource && 'uri' in resource ? resource.uri : resource; - const path = configService.getSettings(resource).pythonPath; - const id = path === 'python' ? 'DEFAULT_PYTHON' : getEnvID(path); - return { - id, - path, - }; + return getActiveEnvironmentPath(resource); }, updateActiveEnvironmentPath(env: Environment | EnvironmentPath | string, resource?: Resource): Promise { sendApiTelemetry('updateActiveEnvironmentPath'); diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts index 69583b744da9..1136502c1ef2 100644 --- a/src/client/jupyter/jupyterIntegration.ts +++ b/src/client/jupyter/jupyterIntegration.ts @@ -1,12 +1,12 @@ /* eslint-disable comma-dangle */ -/* eslint-disable implicit-arrow-linebreak */ +/* eslint-disable implicit-arrow-linebreak, max-classes-per-file */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { inject, injectable, named } from 'inversify'; import { dirname } from 'path'; -import { Extension, Memento, Uri } from 'vscode'; +import { EventEmitter, Extension, Memento, Uri, workspace, Event } from 'vscode'; import type { SemVer } from 'semver'; import { IContextKeyManager, IWorkspaceService } from '../common/application/types'; import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants'; @@ -23,6 +23,7 @@ import { PylanceApi } from '../activation/node/pylanceApi'; import { ExtensionContextKey } from '../common/application/contextKeys'; import { getDebugpyPath } from '../debugger/pythonDebugger'; import type { Environment } from '../api/types'; +import { DisposableBase } from '../common/utils/resourceLifecycle'; type PythonApiForJupyterExtension = { /** @@ -170,3 +171,108 @@ export class JupyterExtensionIntegration { } } } + +export interface JupyterPythonEnvironmentApi { + /** + * This event is triggered when the environment associated with a Jupyter Notebook or Interactive Window changes. + * The Uri in the event is the Uri of the Notebook/IW. + */ + onDidChangePythonEnvironment?: Event; + /** + * Returns the EnvironmentPath to the Python environment associated with a Jupyter Notebook or Interactive Window. + * If the Uri is not associated with a Jupyter Notebook or Interactive Window, then this method returns undefined. + * @param uri + */ + getPythonEnvironment?( + uri: Uri, + ): + | undefined + | { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; + }; +} + +@injectable() +export class JupyterExtensionPythonEnvironments extends DisposableBase implements JupyterPythonEnvironmentApi { + private jupyterExtension?: JupyterPythonEnvironmentApi; + + private readonly _onDidChangePythonEnvironment = this._register(new EventEmitter()); + + public readonly onDidChangePythonEnvironment = this._onDidChangePythonEnvironment.event; + + constructor(@inject(IExtensions) private readonly extensions: IExtensions) { + super(); + } + + public getPythonEnvironment( + uri: Uri, + ): + | undefined + | { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; + } { + if (!isJupyterResource(uri)) { + return undefined; + } + const api = this.getJupyterApi(); + if (api?.getPythonEnvironment) { + return api.getPythonEnvironment(uri); + } + return undefined; + } + + private getJupyterApi() { + if (!this.jupyterExtension) { + const ext = this.extensions.getExtension(JUPYTER_EXTENSION_ID); + if (!ext) { + return undefined; + } + if (!ext.isActive) { + ext.activate().then(() => { + this.hookupOnDidChangePythonEnvironment(ext.exports); + }); + return undefined; + } + this.hookupOnDidChangePythonEnvironment(ext.exports); + } + return this.jupyterExtension; + } + + private hookupOnDidChangePythonEnvironment(api: JupyterPythonEnvironmentApi) { + this.jupyterExtension = api; + if (api.onDidChangePythonEnvironment) { + this._register( + api.onDidChangePythonEnvironment( + this._onDidChangePythonEnvironment.fire, + this._onDidChangePythonEnvironment, + ), + ); + } + } +} + +function isJupyterResource(resource: Uri): boolean { + // Jupyter extension only deals with Notebooks and Interactive Windows. + return ( + resource.fsPath.endsWith('.ipynb') || + workspace.notebookDocuments.some((item) => item.uri.toString() === resource.toString()) + ); +} diff --git a/src/test/api.functional.test.ts b/src/test/api.functional.test.ts index eea0fb920b15..1149dcb7da9d 100644 --- a/src/test/api.functional.test.ts +++ b/src/test/api.functional.test.ts @@ -19,6 +19,8 @@ import { ServiceManager } from '../client/ioc/serviceManager'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; import { IDiscoveryAPI } from '../client/pythonEnvironments/base/locator'; import * as pythonDebugger from '../client/debugger/pythonDebugger'; +import { JupyterExtensionPythonEnvironments, JupyterPythonEnvironmentApi } from '../client/jupyter/jupyterIntegration'; +import { EventEmitter, Uri } from 'vscode'; suite('Extension API', () => { const debuggerPath = path.join(EXTENSION_ROOT_DIR, 'python_files', 'lib', 'python', 'debugpy'); @@ -49,6 +51,14 @@ suite('Extension API', () => { instance(environmentVariablesProvider), ); when(serviceContainer.get(IInterpreterService)).thenReturn(instance(interpreterService)); + const onDidChangePythonEnvironment = new EventEmitter(); + const jupyterApi: JupyterPythonEnvironmentApi = { + onDidChangePythonEnvironment: onDidChangePythonEnvironment.event, + getPythonEnvironment: (_uri: Uri) => undefined, + }; + when(serviceContainer.get(JupyterExtensionPythonEnvironments)).thenReturn( + jupyterApi, + ); when(serviceContainer.get(IDisposableRegistry)).thenReturn([]); getDebugpyPathStub = sinon.stub(pythonDebugger, 'getDebugpyPath'); getDebugpyPathStub.resolves(debuggerPath); diff --git a/src/test/environmentApi.unit.test.ts b/src/test/environmentApi.unit.test.ts index 012e1a0bfc69..2e5d13161f7b 100644 --- a/src/test/environmentApi.unit.test.ts +++ b/src/test/environmentApi.unit.test.ts @@ -38,6 +38,7 @@ import { EnvironmentsChangeEvent, PythonExtension, } from '../client/api/types'; +import { JupyterPythonEnvironmentApi } from '../client/jupyter/jupyterIntegration'; suite('Python Environment API', () => { const workspacePath = 'path/to/workspace'; @@ -80,7 +81,6 @@ suite('Python Environment API', () => { onDidChangeRefreshState = new EventEmitter(); onDidChangeEnvironments = new EventEmitter(); onDidChangeEnvironmentVariables = new EventEmitter(); - serviceContainer.setup((s) => s.get(IExtensions)).returns(() => extensions.object); serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); serviceContainer.setup((s) => s.get(IConfigurationService)).returns(() => configService.object); @@ -94,8 +94,13 @@ suite('Python Environment API', () => { discoverAPI.setup((d) => d.onProgress).returns(() => onDidChangeRefreshState.event); discoverAPI.setup((d) => d.onChanged).returns(() => onDidChangeEnvironments.event); discoverAPI.setup((d) => d.getEnvs()).returns(() => []); + const onDidChangePythonEnvironment = new EventEmitter(); + const jupyterApi: JupyterPythonEnvironmentApi = { + onDidChangePythonEnvironment: onDidChangePythonEnvironment.event, + getPythonEnvironment: (_uri: Uri) => undefined, + }; - environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object); + environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object, jupyterApi); }); teardown(() => { @@ -323,7 +328,12 @@ suite('Python Environment API', () => { }, ]; discoverAPI.setup((d) => d.getEnvs()).returns(() => envs); - environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object); + const onDidChangePythonEnvironment = new EventEmitter(); + const jupyterApi: JupyterPythonEnvironmentApi = { + onDidChangePythonEnvironment: onDidChangePythonEnvironment.event, + getPythonEnvironment: (_uri: Uri) => undefined, + }; + environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object, jupyterApi); const actual = environmentApi.known; const actualEnvs = actual?.map((a) => (a as EnvironmentReference).internal); assert.deepEqual( From d5b19e7be9ce1e5d9d4b3f8c78d2f6b5df76c29a Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:51:17 -0800 Subject: [PATCH 0866/1136] add extra newline to execute when returning dictionary (#24784) Resolves: https://github.com/microsoft/vscode-python/issues/22469 Need one more extra line to "execute" on behalf of user when returning dictionary. --- python_files/normalizeSelection.py | 9 ++++ .../tests/test_normalize_selection.py | 47 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/python_files/normalizeSelection.py b/python_files/normalizeSelection.py index 3d5137fe4aeb..9d82a4dc9440 100644 --- a/python_files/normalizeSelection.py +++ b/python_files/normalizeSelection.py @@ -120,8 +120,12 @@ def normalize_lines(selection): # Insert a newline between each top-level statement, and append a newline to the selection. source = "\n".join(statements) + "\n" + # If selection ends with trailing dictionary or list, remove last unnecessary newline. if selection[-2] == "}" or selection[-2] == "]": source = source[:-1] + # If the selection contains trailing return dictionary, insert newline to trigger execute. + if check_end_with_return_dict(selection): + source = source + "\n" except Exception: # If there's a problem when parsing statements, # append a blank line to end the block and send it as-is. @@ -134,6 +138,11 @@ def normalize_lines(selection): min_key = None +def check_end_with_return_dict(code): + stripped_code = code.strip() + return stripped_code.endswith("}") and "return {" in stripped_code.strip() + + def check_exact_exist(top_level_nodes, start_line, end_line): return [ node diff --git a/python_files/tests/test_normalize_selection.py b/python_files/tests/test_normalize_selection.py index e16eb118db12..779bb9720bfa 100644 --- a/python_files/tests/test_normalize_selection.py +++ b/python_files/tests/test_normalize_selection.py @@ -268,3 +268,50 @@ def test_list_comp(self): result = normalizeSelection.normalize_lines(src) assert result == expected + + def test_return_dict(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def get_dog(name, breed): + return {'name': name, 'breed': breed} + """ + ) + + expected = textwrap.dedent( + """\ + def get_dog(name, breed): + return {'name': name, 'breed': breed} + + """ + ) + + result = normalizeSelection.normalize_lines(src) + + assert result == expected + + def test_return_dict2(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def get_dog(name, breed): + return {'name': name, 'breed': breed} + + dog = get_dog('Ahri', 'Pomeranian') + print(dog) + """ + ) + + expected = textwrap.dedent( + """\ + def get_dog(name, breed): + return {'name': name, 'breed': breed} + + dog = get_dog('Ahri', 'Pomeranian') + print(dog) + """ + ) + + result = normalizeSelection.normalize_lines(src) + + assert result == expected From bf38fc54bdc76a7b6cb45e7a417d0d7b5aa84538 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:20:54 -0800 Subject: [PATCH 0867/1136] Add "Native" in front of "Python REPL" under Run Python menu (#24785) Aligning more with command palette option we have: `Python: Start Native Python REPL` and taking feedback from https://github.com/microsoft/vscode-python/discussions/24443#discussioncomment-12063285 --- package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.nls.json b/package.nls.json index d744ef430fe4..c4eaa0280f02 100644 --- a/package.nls.json +++ b/package.nls.json @@ -15,7 +15,7 @@ "python.command.python.configureTests.title": "Configure Tests", "python.command.testing.rerunFailedTests.title": "Rerun Failed Tests", "python.command.python.execSelectionInTerminal.title": "Run Selection/Line in Python Terminal", - "python.command.python.execInREPL.title": "Run Selection/Line in Python REPL", + "python.command.python.execInREPL.title": "Run Selection/Line in Native Python REPL", "python.command.python.execSelectionInDjangoShell.title": "Run Selection/Line in Django Shell", "python.command.python.reportIssue.title": "Report Issue...", "python.command.python.clearCacheAndReload.title": "Clear Cache and Reload Window", From 95787858335e744dd152a5901ab70978350821f6 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 5 Feb 2025 17:33:40 -0800 Subject: [PATCH 0868/1136] Fix event duplication when using Python Environments (#24786) Partial fix for https://github.com/microsoft/vscode-python/issues/24783 --- src/client/envExt/api.legacy.ts | 23 ++++++++++++++++---- src/client/interpreter/interpreterService.ts | 7 ++---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/client/envExt/api.legacy.ts b/src/client/envExt/api.legacy.ts index 1d9d94ccc98f..7546a429c76a 100644 --- a/src/client/envExt/api.legacy.ts +++ b/src/client/envExt/api.legacy.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Terminal, Uri } from 'vscode'; +import { Terminal, Uri, WorkspaceFolder } from 'vscode'; import { getEnvExtApi, getEnvironment } from './api.internal'; import { EnvironmentType, PythonEnvironment as PythonEnvironmentLegacy } from '../pythonEnvironments/info'; import { PythonEnvironment, PythonTerminalOptions } from './types'; @@ -10,7 +10,7 @@ import { parseVersion } from '../pythonEnvironments/base/info/pythonVersion'; import { PythonEnvType } from '../pythonEnvironments/base/info'; import { traceError, traceInfo } from '../logging'; import { reportActiveInterpreterChanged } from '../environmentApi'; -import { getWorkspaceFolder } from '../common/vscodeApis/workspaceApis'; +import { getWorkspaceFolder, getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; function toEnvironmentType(pythonEnv: PythonEnvironment): EnvironmentType { if (pythonEnv.envId.managerId.toLowerCase().endsWith('system')) { @@ -106,16 +106,25 @@ export async function getActiveInterpreterLegacy(resource?: Uri): Promise 0 && resource !== undefined); + if (shouldReport && newEnv && oldEnv?.envId.id !== pythonEnv?.envId.id) { reportActiveInterpreterChanged({ resource: getWorkspaceFolder(resource), path: newEnv.path, }); + previousEnvMap.set(uri?.fsPath || '', pythonEnv); } return pythonEnv ? toLegacyType(pythonEnv) : undefined; } -export async function ensureEnvironmentContainsPythonLegacy(pythonPath: string): Promise { +export async function ensureEnvironmentContainsPythonLegacy( + pythonPath: string, + workspaceFolder: WorkspaceFolder | undefined, + callback: () => void, +): Promise { const api = await getEnvExtApi(); const pythonEnv = await api.resolveEnvironment(Uri.file(pythonPath)); if (!pythonEnv) { @@ -132,6 +141,12 @@ export async function ensureEnvironmentContainsPythonLegacy(pythonPath: string): traceInfo(`EnvExt: Python not found in ${envType} environment ${pythonPath}`); traceInfo(`EnvExt: Installing Python in ${envType} environment ${pythonPath}`); await api.installPackages(pythonEnv, ['python']); + previousEnvMap.set(workspaceFolder?.uri.fsPath || '', pythonEnv); + reportActiveInterpreterChanged({ + path: pythonPath, + resource: workspaceFolder, + }); + callback(); } } diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 628a25d6b3b1..3a1aaed312ff 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -290,11 +290,8 @@ export class InterpreterService implements Disposable, IInterpreterService { @cache(-1, true) private async ensureEnvironmentContainsPython(pythonPath: string, workspaceFolder: WorkspaceFolder | undefined) { if (useEnvExtension()) { - await ensureEnvironmentContainsPythonLegacy(pythonPath); - this.didChangeInterpreterEmitter.fire(workspaceFolder?.uri); - reportActiveInterpreterChanged({ - path: pythonPath, - resource: workspaceFolder, + await ensureEnvironmentContainsPythonLegacy(pythonPath, workspaceFolder, () => { + this.didChangeInterpreterEmitter.fire(workspaceFolder?.uri); }); return; } From e8ed713a50b0b8d4f2f5fc92dd5b7cd392326169 Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Thu, 6 Feb 2025 13:45:18 -0800 Subject: [PATCH 0869/1136] Update Pylance GDPR tags (#24794) --- src/client/telemetry/pylance.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index 778bfc97be12..42d177488790 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -47,8 +47,6 @@ */ /* __GDPR__ "language_server/analysis_complete" : { - "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "configparseerroroccurred" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "elapsedms" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "externalmb" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, @@ -152,8 +150,6 @@ */ /* __GDPR__ "language_server/exception_intellicode" : { - "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } } @@ -199,8 +195,6 @@ "absoluteuserunresolved" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "builtinimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "builtinimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "localimportstubs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "localimporttotal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -255,8 +249,6 @@ /* __GDPR__ "language_server/intellicode_completion_item_selected" : { "class" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "common.remotename" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "count" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "elapsedtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "failurereason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, @@ -277,8 +269,6 @@ */ /* __GDPR__ "language_server/intellicode_enabled" : { - "common.remotename" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, - "common.uikind" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "enabled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "installsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, From 32c2cf9c4f2c4fe75fef8682612e704d96b13fca Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 7 Feb 2025 12:00:40 -0800 Subject: [PATCH 0870/1136] add check for tmp dir access error on testIds file (#24798) fixes #24406 --- .../testing/testController/common/utils.ts | 2 + .../testing/testController/utils.unit.test.ts | 226 +++--------------- 2 files changed, 39 insertions(+), 189 deletions(-) diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 68e10a2213d6..e3b37bf74e40 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -53,6 +53,8 @@ export async function writeTestIdsFile(testIds: string[]): Promise { try { traceLog('Attempting to use temp directory for test ids file, file name:', tempName); tempFileName = path.join(os.tmpdir(), tempName); + // attempt access to written file to check permissions + await fs.promises.access(os.tmpdir()); } catch (error) { // Handle the error when accessing the temp directory traceError('Error accessing temp directory:', error, ' Attempt to use extension root dir instead'); diff --git a/src/test/testing/testController/utils.unit.test.ts b/src/test/testing/testController/utils.unit.test.ts index b871d18348e2..ff1a0c707678 100644 --- a/src/test/testing/testController/utils.unit.test.ts +++ b/src/test/testing/testController/utils.unit.test.ts @@ -1,202 +1,50 @@ -// // Copyright (c) Microsoft Corporation. All rights reserved. -// // Licensed under the MIT License. +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { writeTestIdsFile } from '../../../client/testing/testController/common/utils'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; -// import * as assert from 'assert'; -// import { -// JSONRPC_CONTENT_LENGTH_HEADER, -// JSONRPC_CONTENT_TYPE_HEADER, -// JSONRPC_UUID_HEADER, -// ExtractJsonRPCData, -// parseJsonRPCHeadersAndData, -// splitTestNameWithRegex, -// argKeyExists, -// addValueIfKeyNotExist, -// } from '../../../client/testing/testController/common/utils'; +suite('writeTestIdsFile tests', () => { + let sandbox: sinon.SinonSandbox; -// suite('Test Controller Utils: JSON RPC', () => { -// test('Empty raw data string', async () => { -// const rawDataString = ''; + setup(() => { + sandbox = sinon.createSandbox(); + }); -// const output = parseJsonRPCHeadersAndData(rawDataString); -// assert.deepStrictEqual(output.headers.size, 0); -// assert.deepStrictEqual(output.remainingRawData, ''); -// }); + teardown(() => { + sandbox.restore(); + }); -// test('Valid data empty JSON', async () => { -// const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 2\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n{}`; + test('should write test IDs to a temporary file', async () => { + const testIds = ['test1', 'test2', 'test3']; + const writeFileStub = sandbox.stub(fs.promises, 'writeFile').resolves(); -// const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); -// assert.deepStrictEqual(rpcHeaders.headers.size, 3); -// assert.deepStrictEqual(rpcHeaders.remainingRawData, '{}'); -// const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); -// assert.deepStrictEqual(rpcContent.extractedJSON, '{}'); -// }); + const result = await writeTestIdsFile(testIds); -// test('Valid data NO JSON', async () => { -// const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: 0\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n`; + const tmpDir = os.tmpdir(); -// const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); -// assert.deepStrictEqual(rpcHeaders.headers.size, 3); -// assert.deepStrictEqual(rpcHeaders.remainingRawData, ''); -// const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); -// assert.deepStrictEqual(rpcContent.extractedJSON, ''); -// }); + assert.ok(result.startsWith(tmpDir)); -// test('Valid data with full JSON', async () => { -// // this is just some random JSON -// const json = -// '{"jsonrpc": "2.0", "method": "initialize", "params": {"processId": 1234, "rootPath": "/home/user/project", "rootUri": "file:///home/user/project", "capabilities": {}}, "id": 0}'; -// const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`; + assert.ok(writeFileStub.calledOnceWith(sinon.match.string, testIds.join('\n'))); + }); -// const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString); -// assert.deepStrictEqual(rpcHeaders.headers.size, 3); -// assert.deepStrictEqual(rpcHeaders.remainingRawData, json); -// const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); -// assert.deepStrictEqual(rpcContent.extractedJSON, json); -// }); + test('should handle error when accessing temp directory', async () => { + const testIds = ['test1', 'test2', 'test3']; + const error = new Error('Access error'); + const accessStub = sandbox.stub(fs.promises, 'access').rejects(error); + const writeFileStub = sandbox.stub(fs.promises, 'writeFile').resolves(); + const mkdirStub = sandbox.stub(fs.promises, 'mkdir').resolves(); -// test('Valid data with multiple JSON', async () => { -// const json = -// '{"jsonrpc": "2.0", "method": "initialize", "params": {"processId": 1234, "rootPath": "/home/user/project", "rootUri": "file:///home/user/project", "capabilities": {}}, "id": 0}'; -// const rawDataString = `${JSONRPC_CONTENT_LENGTH_HEADER}: ${json.length}\n${JSONRPC_CONTENT_TYPE_HEADER}: application/json\n${JSONRPC_UUID_HEADER}: 1234\n\n${json}`; -// const rawDataString2 = rawDataString + rawDataString; + const result = await writeTestIdsFile(testIds); -// const rpcHeaders = parseJsonRPCHeadersAndData(rawDataString2); -// assert.deepStrictEqual(rpcHeaders.headers.size, 3); -// const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); -// assert.deepStrictEqual(rpcContent.extractedJSON, json); -// assert.deepStrictEqual(rpcContent.remainingRawData, rawDataString); -// }); + const tempFileFolder = path.join(EXTENSION_ROOT_DIR, '.temp'); -// test('Valid constant', async () => { -// const data = `{"cwd": "/Users/eleanorboyd/testingFiles/inc_dec_example", "status": "success", "result": {"test_dup_class.test_a.TestSomething.test_a": {"test": "test_dup_class.test_a.TestSomething.test_a", "outcome": "success", "message": "None", "traceback": null, "subtest": null}}}`; -// const secondPayload = `Content-Length: 270 -// Content-Type: application/json -// Request-uuid: 496c86b1-608f-4886-9436-ec00538e144c + assert.ok(result.startsWith(tempFileFolder)); -// ${data}`; -// const payload = `Content-Length: 270 -// Content-Type: application/json -// Request-uuid: 496c86b1-608f-4886-9436-ec00538e144c - -// ${data}${secondPayload}`; - -// const rpcHeaders = parseJsonRPCHeadersAndData(payload); -// assert.deepStrictEqual(rpcHeaders.headers.size, 3); -// const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); -// assert.deepStrictEqual(rpcContent.extractedJSON, data); -// assert.deepStrictEqual(rpcContent.remainingRawData, secondPayload); -// }); -// test('Valid content length as only header with carriage return', async () => { -// const payload = `Content-Length: 7 -// `; - -// const rpcHeaders = parseJsonRPCHeadersAndData(payload); -// assert.deepStrictEqual(rpcHeaders.headers.size, 1); -// const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); -// assert.deepStrictEqual(rpcContent.extractedJSON, ''); -// assert.deepStrictEqual(rpcContent.remainingRawData, ''); -// }); -// test('Valid content length header with no value', async () => { -// const payload = `Content-Length:`; - -// const rpcHeaders = parseJsonRPCHeadersAndData(payload); -// const rpcContent = ExtractJsonRPCData(rpcHeaders.headers.get('Content-Length'), rpcHeaders.remainingRawData); -// assert.deepStrictEqual(rpcContent.extractedJSON, ''); -// assert.deepStrictEqual(rpcContent.remainingRawData, ''); -// }); - -// suite('Test Controller Utils: Other', () => { -// interface TestCase { -// name: string; -// input: string; -// expectedParent: string; -// expectedSubtest: string; -// } - -// const testCases: Array = [ -// { -// name: 'Single parameter, named', -// input: 'test_package.ClassName.test_method (param=value)', -// expectedParent: 'test_package.ClassName.test_method', -// expectedSubtest: '(param=value)', -// }, -// { -// name: 'Single parameter, unnamed', -// input: 'test_package.ClassName.test_method [value]', -// expectedParent: 'test_package.ClassName.test_method', -// expectedSubtest: '[value]', -// }, -// { -// name: 'Multiple parameters, named', -// input: 'test_package.ClassName.test_method (param1=value1, param2=value2)', -// expectedParent: 'test_package.ClassName.test_method', -// expectedSubtest: '(param1=value1, param2=value2)', -// }, -// { -// name: 'Multiple parameters, unnamed', -// input: 'test_package.ClassName.test_method [value1, value2]', -// expectedParent: 'test_package.ClassName.test_method', -// expectedSubtest: '[value1, value2]', -// }, -// { -// name: 'Names with special characters', -// input: 'test_package.ClassName.test_method (param1=value/1, param2=value+2)', -// expectedParent: 'test_package.ClassName.test_method', -// expectedSubtest: '(param1=value/1, param2=value+2)', -// }, -// { -// name: 'Names with spaces', -// input: 'test_package.ClassName.test_method ["a b c d"]', -// expectedParent: 'test_package.ClassName.test_method', -// expectedSubtest: '["a b c d"]', -// }, -// ]; - -// testCases.forEach((testCase) => { -// test(`splitTestNameWithRegex: ${testCase.name}`, () => { -// const splitResult = splitTestNameWithRegex(testCase.input); -// assert.deepStrictEqual(splitResult, [testCase.expectedParent, testCase.expectedSubtest]); -// }); -// }); -// }); -// suite('Test Controller Utils: Args Mapping', () => { -// suite('addValueIfKeyNotExist', () => { -// test('should add key-value pair if key does not exist', () => { -// const args = ['key1=value1', 'key2=value2']; -// const result = addValueIfKeyNotExist(args, 'key3', 'value3'); -// assert.deepEqual(result, ['key1=value1', 'key2=value2', 'key3=value3']); -// }); - -// test('should not add key-value pair if key already exists', () => { -// const args = ['key1=value1', 'key2=value2']; -// const result = addValueIfKeyNotExist(args, 'key1', 'value3'); -// assert.deepEqual(result, ['key1=value1', 'key2=value2']); -// }); -// test('should not add key-value pair if key exists as a solo element', () => { -// const args = ['key1=value1', 'key2']; -// const result = addValueIfKeyNotExist(args, 'key2', 'yellow'); -// assert.deepEqual(result, ['key1=value1', 'key2']); -// }); -// test('add just key if value is null', () => { -// const args = ['key1=value1', 'key2']; -// const result = addValueIfKeyNotExist(args, 'key3', null); -// assert.deepEqual(result, ['key1=value1', 'key2', 'key3']); -// }); -// }); - -// suite('argKeyExists', () => { -// test('should return true if key exists', () => { -// const args = ['key1=value1', 'key2=value2']; -// const result = argKeyExists(args, 'key1'); -// assert.deepEqual(result, true); -// }); - -// test('should return false if key does not exist', () => { -// const args = ['key1=value1', 'key2=value2']; -// const result = argKeyExists(args, 'key3'); -// assert.deepEqual(result, false); -// }); -// }); -// }); -// }); + assert.ok(accessStub.called); + assert.ok(mkdirStub.called); + assert.ok(writeFileStub.calledOnceWith(sinon.match.string, testIds.join('\n'))); + }); +}); From 0983657bce40111fe719ad2c3713f6fb67334897 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 7 Feb 2025 13:07:37 -0800 Subject: [PATCH 0871/1136] improve logging for python testing (#24799) will help provide clarity for https://github.com/microsoft/vscode-python/issues/24585 --- .../testController/pytest/pytestDiscoveryAdapter.ts | 7 ++++++- .../testController/pytest/pytestExecutionAdapter.ts | 10 +++++++++- .../testController/unittest/testDiscoveryAdapter.ts | 4 +++- .../testController/unittest/testExecutionAdapter.ts | 12 ++++++++++-- .../pytest/pytestDiscoveryAdapter.unit.test.ts | 1 + .../pytest/pytestExecutionAdapter.unit.test.ts | 2 ++ .../unittest/testDiscoveryAdapter.unit.test.ts | 1 + .../unittest/testExecutionAdapter.unit.test.ts | 2 ++ 8 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index ef68f7d8039d..71d71997c57e 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -111,7 +111,9 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); mutableEnv.PYTHONPATH = pythonPathCommand; mutableEnv.TEST_RUN_PIPE = discoveryPipeName; - traceInfo(`All environment variables set for pytest discovery: ${JSON.stringify(mutableEnv)}`); + traceInfo( + `All environment variables set for pytest discovery, PYTHONPATH: ${JSON.stringify(mutableEnv.PYTHONPATH)}`, + ); // delete UUID following entire discovery finishing. const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); @@ -176,6 +178,9 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + const execInfo = await execService?.getExecutablePath(); + traceVerbose(`Executable path for pytest discovery: ${execInfo}.`); + const deferredTillExecClose: Deferred = createTestingDeferred(); let resultProc: ChildProcess | undefined; diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index f66bff584fe2..3a824f79ac63 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -116,6 +116,10 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }; // need to check what will happen in the exec service is NOT defined and is null const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + + const execInfo = await execService?.getExecutablePath(); + traceVerbose(`Executable path for pytest execution: ${execInfo}.`); + try { // Remove positional test folders and files, we will add as needed per node let testArgs = removePositionalFoldersAndFiles(pytestArgs); @@ -133,7 +137,11 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // create a file with the test ids and set the environment variable to the file name const testIdsFileName = await utils.writeTestIdsFile(testIds); mutableEnv.RUN_TEST_IDS_PIPE = testIdsFileName; - traceInfo(`All environment variables set for pytest execution: ${JSON.stringify(mutableEnv)}`); + traceInfo( + `All environment variables set for pytest execution, PYTHONPATH: ${JSON.stringify( + mutableEnv.PYTHONPATH, + )}`, + ); const spawnOptions: SpawnOptions = { cwd, diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 73eb3f5aec2b..7e478b25735a 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -28,7 +28,7 @@ import { fixLogLinesNoTrailing, startDiscoveryNamedPipe, } from '../common/utils'; -import { traceError, traceInfo, traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { getEnvironment, runInBackground, useEnvExtension } from '../../../envExt/api.internal'; /** @@ -169,6 +169,8 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { resource: options.workspaceFolder, }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + const execInfo = await execService?.getExecutablePath(); + traceVerbose(`Executable path for unittest discovery: ${execInfo}.`); let resultProc: ChildProcess | undefined; options.token?.onCancellationRequested(() => { diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index e2b591379335..e46e8c436583 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -14,7 +14,7 @@ import { TestCommandOptions, TestExecutionCommand, } from '../common/types'; -import { traceError, traceInfo, traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { MESSAGE_ON_TESTING_OUTPUT_MOVE, fixLogLinesNoTrailing } from '../common/utils'; import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../../common/variables/types'; import { @@ -130,7 +130,11 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { // create named pipe server to send test ids const testIdsFileName = await utils.writeTestIdsFile(testIds); mutableEnv.RUN_TEST_IDS_PIPE = testIdsFileName; - traceInfo(`All environment variables set for pytest execution: ${JSON.stringify(mutableEnv)}`); + traceInfo( + `All environment variables set for unittest execution, PYTHONPATH: ${JSON.stringify( + mutableEnv.PYTHONPATH, + )}`, + ); const spawnOptions: SpawnOptions = { token: options.token, @@ -145,6 +149,10 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { resource: options.workspaceFolder, }; const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + + const execInfo = await execService?.getExecutablePath(); + traceVerbose(`Executable path for unittest execution: ${execInfo}.`); + const args = [options.command.script].concat(options.command.args); if (options.outChannel) { diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 852942715270..157134cdf276 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -71,6 +71,7 @@ suite('pytest test discovery adapter', () => { mockProc = new MockChildProcess('', ['']); execService = typeMoq.Mock.ofType(); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); outputChannel = typeMoq.Mock.ofType(); const output = new Observable>(() => { diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 18cabcc96772..413c0af9406d 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -89,6 +89,8 @@ suite('pytest test execution adapter', () => { utilsStartRunResultNamedPipeStub = sinon.stub(util, 'startRunResultNamedPipe'); utilsStartRunResultNamedPipeStub.callsFake(() => Promise.resolve('runResultPipe-mockName')); + + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); }); teardown(() => { sinon.restore(); diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index 911a5f89afb4..0a2cfad866d5 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -68,6 +68,7 @@ suite('Unittest test discovery adapter', () => { }, }; }); + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); execFactory = typeMoq.Mock.ofType(); deferred = createDeferred(); execFactory diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index 78dcb0229e45..688d6d398101 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -88,6 +88,8 @@ suite('Unittest test execution adapter', () => { utilsStartRunResultNamedPipeStub = sinon.stub(util, 'startRunResultNamedPipe'); utilsStartRunResultNamedPipeStub.callsFake(() => Promise.resolve('runResultPipe-mockName')); + + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); }); teardown(() => { sinon.restore(); From b4aa112990c7e25f1eb06fb5e2b85418f2f4c4fa Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Fri, 7 Feb 2025 14:30:26 -0800 Subject: [PATCH 0872/1136] handle un-analyzable files in coverage run (#24800) fixes https://github.com/microsoft/vscode-python/issues/24703 --- python_files/vscode_pytest/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 0ba5fd62221a..00f356e20dcd 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -462,6 +462,11 @@ def pytest_sessionfinish(session, exitstatus): except NoSource: # as per issue 24308 this best way to handle this edge case continue + except Exception as e: + print( + f"Plugin error[vscode-pytest]: Skipping analysis of file: {file} due to error: {e}" + ) + continue lines_executable = {int(line_no) for line_no in analysis[1]} lines_missed = {int(line_no) for line_no in analysis[3]} lines_covered = lines_executable - lines_missed From 79e8a134daea16a9219987eb02f62cd358e88d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Correia?= <46303606+jpcorreia99@users.noreply.github.com> Date: Fri, 14 Feb 2025 17:50:33 +0000 Subject: [PATCH 0873/1136] Always use environment path when running conda environment commands (#24807) Attempt at fixing https://github.com/microsoft/vscode-python/issues/24585 There are many edge scenarious where refering to the name of the environment rather than the path can cause breaks in the extension. Some examples 1 -**If we have two anonymous environments with the same name in different folders** /path1/my-env /path2/my-env (where my active vscode python interpreter is) by using conda -n my-env it'll always use the first env. 2 - **Some times people avoid actually activating their conda envs when using conda-pack** https://github.com/conda/conda-pack This is because the activation scripts are known to be flaky and not very reliable 3 - **The environment may have been created by a conda-compliant replacement** Therefore conda itself is not aware of it by name but can work with it properly using the path. This is the case of [hawk](https://community.palantir.com/t/introducing-hawk-for-python-package-management-in-code-repositories/500) or frankly anyone building their own conda package manager on top of [rattler](https://github.com/conda/rattler). Some of these points are also hinted at https://github.com/microsoft/vscode-python/issues/24627#issuecomment-2584819147 , and supported by a conda maintainer in https://github.com/microsoft/vscode-python/issues/24585#issuecomment-2603071038 This PR has a minimal attempt at changing that by always forcing -p usage --- .../common/environmentManagers/conda.ts | 7 ++----- .../common/process/pythonEnvironment.unit.test.ts | 12 ++++++------ .../common/environmentManagers/conda.unit.test.ts | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/src/client/pythonEnvironments/common/environmentManagers/conda.ts index 5301f82eda18..2fd3f3207fc5 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -564,11 +564,8 @@ export class Conda { return undefined; } const args = []; - if (env.name) { - args.push('-n', env.name); - } else { - args.push('-p', env.prefix); - } + args.push('-p', env.prefix); + const python = [ forShellExecution ? this.shellCommand : this.command, 'run', diff --git a/src/test/common/process/pythonEnvironment.unit.test.ts b/src/test/common/process/pythonEnvironment.unit.test.ts index f4b11bb97cd5..a2cca66d08be 100644 --- a/src/test/common/process/pythonEnvironment.unit.test.ts +++ b/src/test/common/process/pythonEnvironment.unit.test.ts @@ -284,7 +284,7 @@ suite('CondaEnvironment', () => { teardown(() => sinon.restore()); - test('getExecutionInfo with a named environment should return execution info using the environment name', async () => { + test('getExecutionInfo with a named environment should return execution info using the environment path', async () => { const condaInfo = { name: 'foo', path: 'bar' }; const env = await createCondaEnv(condaInfo, processService.object, fileSystem.object); @@ -292,8 +292,8 @@ suite('CondaEnvironment', () => { expect(result).to.deep.equal({ command: condaFile, - args: ['run', '-n', condaInfo.name, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT, ...args], - python: [condaFile, 'run', '-n', condaInfo.name, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], + args: ['run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT, ...args], + python: [condaFile, 'run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], pythonExecutable: pythonPath, }); }); @@ -312,12 +312,12 @@ suite('CondaEnvironment', () => { }); }); - test('getExecutionObservableInfo with a named environment should return execution info using conda full path with the name', async () => { + test('getExecutionObservableInfo with a named environment should return execution info using conda full path with the path', async () => { const condaInfo = { name: 'foo', path: 'bar' }; const expected = { command: condaFile, - args: ['run', '-n', condaInfo.name, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT, ...args], - python: [condaFile, 'run', '-n', condaInfo.name, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], + args: ['run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT, ...args], + python: [condaFile, 'run', '-p', condaInfo.path, '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], pythonExecutable: pythonPath, }; const env = await createCondaEnv(condaInfo, processService.object, fileSystem.object); diff --git a/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts index e621c25aeb62..9480dffe6a59 100644 --- a/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts @@ -536,7 +536,7 @@ suite('Conda and its environments are located correctly', () => { expect(args).to.not.equal(undefined); assert.deepStrictEqual( args, - ['conda', 'run', '-n', 'envName', '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], + ['conda', 'run', '-p', 'envPrefix', '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], 'Incorrect args for case 1', ); From 08e228df23b3efbfa404a472a249d482bcab9ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9amus=20=C3=93=20Ceanainn?= Date: Fri, 14 Feb 2025 18:01:17 +0000 Subject: [PATCH 0874/1136] Introduce `autoTestDiscoverOnSavePattern` configuration option (#24728) Closes https://github.com/microsoft/vscode-python/issues/24817 ## What this change does Introduce `autoTestDiscoverOnSavePattern` configuration option to control `autoTestDiscoverOnSaveEnabled` behavior to only attempt test discovery refresh when files matching the specified glob pattern are saved. ## Why this change In a Python project we have with over 40K tests, developers definitely notice issues when pytest discovery is running whenever any file in the workspace is saved, despite all tests matching a very consistent pattern (`./tests/**/test_*.py`). ## Other alternatives I considered I did consider trying to match only the specific patterns used by unittest/pytest here. Given that would require parsing underlying configuration files / raw args in the test configuration for the workspace for both unittest and pytest (plus any other test runners supported in future) - I don't think that's going to be easy to maintain. Plus the addition / deletion of `__init__.py` files play a significant part in test discovery despite not being covered by the test configuration pattern - so this solution would be incomplete. Another alternative would be to accept a parent directory and only include python files from that directory + subdirectories (using workspace directory as default value). This avoids introducing a glob configuration value, but feels very limiting. --------- Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- package.json | 6 ++++++ package.nls.json | 1 + resources/report_issue_user_settings.json | 3 ++- src/client/common/configSettings.ts | 2 ++ src/client/testing/configuration/types.ts | 1 + src/client/testing/testController/controller.ts | 4 +++- 6 files changed, 15 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ef656af5003e..f773c32c08da 100644 --- a/package.json +++ b/package.json @@ -657,6 +657,12 @@ "scope": "resource", "type": "boolean" }, + "python.testing.autoTestDiscoverOnSavePattern": { + "default": "**/*.py", + "description": "%python.testing.autoTestDiscoverOnSavePattern.description%", + "scope": "resource", + "type": "string" + }, "python.testing.cwd": { "default": null, "description": "%python.testing.cwd.description%", diff --git a/package.nls.json b/package.nls.json index c4eaa0280f02..8bff60a4b07d 100644 --- a/package.nls.json +++ b/package.nls.json @@ -74,6 +74,7 @@ "python.terminal.focusAfterLaunch.description": "When launching a python terminal, whether to focus the cursor on the terminal.", "python.terminal.launchArgs.description": "Python launch arguments to use when executing a file in the terminal.", "python.testing.autoTestDiscoverOnSaveEnabled.description": "Enable auto run test discovery when saving a test file.", + "python.testing.autoTestDiscoverOnSavePattern.description": "Glob pattern used to determine which files are used by autoTestDiscoverOnSaveEnabled.", "python.testing.cwd.description": "Optional working directory for tests.", "python.testing.debugPort.description": "Port number used for debugging of tests.", "python.testing.promptToConfigure.description": "Prompt to configure a test framework if potential tests directories are discovered.", diff --git a/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json index ef85267c0e65..7e034651c46d 100644 --- a/resources/report_issue_user_settings.json +++ b/resources/report_issue_user_settings.json @@ -79,7 +79,8 @@ "pytestPath": "placeholder", "unittestArgs": "placeholder", "unittestEnabled": true, - "autoTestDiscoverOnSaveEnabled": true + "autoTestDiscoverOnSaveEnabled": true, + "autoTestDiscoverOnSavePattern": "placeholder" }, "terminal": { "activateEnvironment": true, diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 58c41587c4f8..7ae3467b2cfd 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -320,6 +320,7 @@ export class PythonSettings implements IPythonSettings { unittestEnabled: false, pytestPath: 'pytest', autoTestDiscoverOnSaveEnabled: true, + autoTestDiscoverOnSavePattern: '**/*.py', } as ITestingSettings; } } @@ -336,6 +337,7 @@ export class PythonSettings implements IPythonSettings { unittestArgs: [], unittestEnabled: false, autoTestDiscoverOnSaveEnabled: true, + autoTestDiscoverOnSavePattern: '**/*.py', }; this.testing.pytestPath = getAbsolutePath(systemVariables.resolveAny(this.testing.pytestPath), workspaceRoot); if (this.testing.cwd) { diff --git a/src/client/testing/configuration/types.ts b/src/client/testing/configuration/types.ts index 5da99398283b..3b759bcb39e8 100644 --- a/src/client/testing/configuration/types.ts +++ b/src/client/testing/configuration/types.ts @@ -11,6 +11,7 @@ export interface ITestingSettings { unittestArgs: string[]; cwd?: string; readonly autoTestDiscoverOnSaveEnabled: boolean; + readonly autoTestDiscoverOnSavePattern: string; } export type TestSettingsPropertyNames = { diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 6142140b3e2e..98a7f909a8e2 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -3,6 +3,7 @@ import { inject, injectable, named } from 'inversify'; import { uniq } from 'lodash'; +import * as minimatch from 'minimatch'; import { CancellationToken, TestController, @@ -552,7 +553,8 @@ export class PythonTestController implements ITestController, IExtensionSingleAc private watchForTestContentChangeOnSave(): void { this.disposables.push( onDidSaveTextDocument(async (doc: TextDocument) => { - if (doc.fileName.endsWith('.py')) { + const settings = this.configSettings.getSettings(doc.uri); + if (minimatch.default(doc.uri.fsPath, settings.testing.autoTestDiscoverOnSavePattern)) { traceVerbose(`Testing: Trigger refresh after saving ${doc.uri.fsPath}`); this.sendTriggerTelemetry('watching'); this.refreshData.trigger(doc.uri, false); From 2bcd5577774ccc1e825eee0ed59a59d4d6bb83e7 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:12:38 -0800 Subject: [PATCH 0875/1136] Ensure Python Terminal Shell Integration setting is effective without reloading (#24826) Resolves: https://github.com/microsoft/vscode-python/issues/24373 --- src/client/terminals/pythonStartup.ts | 15 +++++++++++++-- .../shellIntegration/pythonStartup.test.ts | 13 +++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/client/terminals/pythonStartup.ts b/src/client/terminals/pythonStartup.ts index 542a2e6a6355..28878713a8db 100644 --- a/src/client/terminals/pythonStartup.ts +++ b/src/client/terminals/pythonStartup.ts @@ -3,10 +3,10 @@ import { ExtensionContext, Uri } from 'vscode'; import * as path from 'path'; -import { copy, createDirectory, getConfiguration } from '../common/vscodeApis/workspaceApis'; +import { copy, createDirectory, getConfiguration, onDidChangeConfiguration } from '../common/vscodeApis/workspaceApis'; import { EXTENSION_ROOT_DIR } from '../constants'; -export async function registerPythonStartup(context: ExtensionContext): Promise { +async function applyPythonStartupSetting(context: ExtensionContext): Promise { const config = getConfiguration('python'); const pythonrcSetting = config.get('terminal.shellIntegration.enabled'); @@ -25,3 +25,14 @@ export async function registerPythonStartup(context: ExtensionContext): Promise< context.environmentVariableCollection.delete('PYTHONSTARTUP'); } } + +export async function registerPythonStartup(context: ExtensionContext): Promise { + await applyPythonStartupSetting(context); + context.subscriptions.push( + onDidChangeConfiguration(async (e) => { + if (e.affectsConfiguration('python.terminal.shellIntegration.enabled')) { + await applyPythonStartupSetting(context); + } + }), + ); +} diff --git a/src/test/terminals/shellIntegration/pythonStartup.test.ts b/src/test/terminals/shellIntegration/pythonStartup.test.ts index 06364c9445aa..45535d0ceecc 100644 --- a/src/test/terminals/shellIntegration/pythonStartup.test.ts +++ b/src/test/terminals/shellIntegration/pythonStartup.test.ts @@ -12,6 +12,7 @@ import { TerminalLinkContext, Terminal, EventEmitter, + workspace, } from 'vscode'; import { assert } from 'chai'; import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; @@ -35,6 +36,7 @@ suite('Terminal - Shell Integration with PYTHONSTARTUP', () => { globalEnvironmentVariableCollection = TypeMoq.Mock.ofType(); context.setup((c) => c.environmentVariableCollection).returns(() => globalEnvironmentVariableCollection.object); context.setup((c) => c.storageUri).returns(() => Uri.parse('a')); + context.setup((c) => c.subscriptions).returns(() => []); globalEnvironmentVariableCollection .setup((c) => c.replace(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) @@ -146,6 +148,17 @@ suite('Terminal - Shell Integration with PYTHONSTARTUP', () => { registerTerminalLinkProviderStub.restore(); }); + + test('Verify onDidChangeConfiguration is called when configuration changes', async () => { + const onDidChangeConfigurationSpy = sinon.spy(workspace, 'onDidChangeConfiguration'); + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + assert.isTrue(onDidChangeConfigurationSpy.calledOnce); + onDidChangeConfigurationSpy.restore(); + }); + if (process.platform === 'darwin') { test('Mac - Verify provideTerminalLinks returns links when context.line contains expectedNativeLink', () => { const provider = new CustomTerminalLinkProvider(); From 7697cf34e3414bb04137f69b5f60eaf1ce337a42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Feb 2025 11:08:32 -0800 Subject: [PATCH 0876/1136] Bump elliptic from 6.6.0 to 6.6.1 (#24819) Bumps [elliptic](https://github.com/indutny/elliptic) from 6.6.0 to 6.6.1.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=elliptic&package-manager=npm_and_yarn&previous-version=6.6.0&new-version=6.6.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4610d3957c88..ac14997948c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5162,9 +5162,9 @@ "dev": true }, "node_modules/elliptic": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz", - "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", "dev": true, "license": "MIT", "dependencies": { @@ -18647,9 +18647,9 @@ "dev": true }, "elliptic": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz", - "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", "dev": true, "requires": { "bn.js": "^4.11.9", From 42962ce91b0167b755e7735e2d8346c5b60bf803 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Feb 2025 11:08:50 -0800 Subject: [PATCH 0877/1136] Bump serialize-javascript and mocha (#24820) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [serialize-javascript](https://github.com/yahoo/serialize-javascript) to 6.0.2 and updates ancestor dependency [mocha](https://github.com/mochajs/mocha). These dependencies need to be updated together. Updates `serialize-javascript` from 6.0.0 to 6.0.2
Release notes

Sourced from serialize-javascript's releases.

v6.0.2

  • fix: serialize URL string contents to prevent XSS (#173) f27d65d
  • Bump @​babel/traverse from 7.10.1 to 7.23.7 (#171) 02499c0
  • docs: update readme with URL support (#146) 0d88527
  • chore: update node version and lock file e2a3a91
  • fix typo (#164) 5a1fa64

https://github.com/yahoo/serialize-javascript/compare/v6.0.1...v6.0.2

v6.0.1

What's Changed

New Contributors

Full Changelog: https://github.com/yahoo/serialize-javascript/compare/v6.0.0...v6.0.1

Commits

Updates `mocha` from 9.2.2 to 11.1.0
Release notes

Sourced from mocha's releases.

v11.1.0

11.1.0 (2025-01-02)

🌟 Features

v11.0.2

11.0.2 (2024-12-09)

🩹 Fixes

  • catch exceptions setting Error.stackTraceLimit (#5254) (259f8f8)
  • error handling for unexpected numeric arguments passed to cli (#5263) (210d658)

📚 Documentation

  • correct outdated status: accepting prs link (#5268) (f729cd0)
  • replace "New in" with "Since" in version annotations (#5262) (6f10d12)

v11.0.1

11.0.1 (2024-12-02)

🌟 Features

📚 Documentation

  • fix examples for linkPartialObjects methods (#5255) (34e0e52)

v11.0.0 Prerelease

11.0.0 (2024-11-11)

⚠ BREAKING CHANGES

  • adapt new engine range for Mocha 11 (#5216)

🌟 Features

🩹 Fixes

... (truncated)

Changelog

Sourced from mocha's changelog.

11.1.0 (2025-01-02)

🌟 Features

11.0.2 (2024-12-09)

🩹 Fixes

  • catch exceptions setting Error.stackTraceLimit (#5254) (259f8f8)
  • error handling for unexpected numeric arguments passed to cli (#5263) (210d658)

📚 Documentation

  • correct outdated status: accepting prs link (#5268) (f729cd0)
  • replace "New in" with "Since" in version annotations (#5262) (6f10d12)

11.0.1 (2024-12-02)

🌟 Features

📚 Documentation

  • fix examples for linkPartialObjects methods (#5255) (34e0e52)

11.0.0 (2024-11-11)

⚠ BREAKING CHANGES

  • adapt new engine range for Mocha 11 (#5216)

🌟 Features

🩹 Fixes

📚 Documentation

... (truncated)

Commits
Maintainer changes

This version was pushed to npm by voxpelli, a new releaser for mocha since your current version.


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 1011 ++++++++++++++++++++++++++++++++++++--------- package.json | 2 +- 2 files changed, 809 insertions(+), 204 deletions(-) diff --git a/package-lock.json b/package-lock.json index ac14997948c5..78a149e61569 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,7 +86,7 @@ "get-port": "^5.1.1", "gulp": "^5.0.0", "gulp-typescript": "^5.0.0", - "mocha": "^9.2.2", + "mocha": "^11.1.0", "mocha-junit-reporter": "^2.0.2", "mocha-multi-reporters": "^1.1.7", "node-has-native-dependencies": "^1.0.2", @@ -1078,6 +1078,109 @@ "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", @@ -1495,6 +1598,17 @@ "node": ">=14" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.21", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", @@ -2122,12 +2236,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, "node_modules/@vscode/extension-telemetry": { "version": "0.8.4", "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz", @@ -5146,6 +5254,13 @@ "node": ">=0.10.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -7228,15 +7343,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true, - "engines": { - "node": ">=4.x" - } - }, "node_modules/gulp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.0.tgz", @@ -8814,6 +8920,22 @@ "node": ">= 4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -9656,6 +9778,16 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -9676,46 +9808,39 @@ "optional": true }, "node_modules/mocha": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", - "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", - "dev": true, - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.3", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "4.2.1", - "ms": "2.1.3", - "nanoid": "3.3.1", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "workerpool": "6.2.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", + "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", - "mocha": "bin/mocha" + "mocha": "bin/mocha.js" }, "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/mocha-junit-reporter": { @@ -9768,10 +9893,11 @@ } }, "node_modules/mocha/node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -9782,13 +9908,54 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mocha/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/mocha/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -9799,17 +9966,12 @@ } } }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/mocha/node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -9842,6 +10004,60 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mocha/node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mocha/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -9879,12 +10095,13 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", - "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { "node": ">=10" @@ -9896,18 +10113,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/mocha/node_modules/nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/mocha/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9947,6 +10152,52 @@ "node": ">=8" } }, + "node_modules/mocha/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -9967,26 +10218,38 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10" + "node": ">=12" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" } }, "node_modules/module-details-from-path": { @@ -10854,6 +11117,13 @@ "node": ">=8" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -11005,6 +11275,30 @@ "node": ">=0.10.0" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -11919,10 +12213,11 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -12460,6 +12755,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", @@ -12540,6 +12851,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-dirs": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", @@ -12828,15 +13153,6 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -14333,10 +14649,11 @@ } }, "node_modules/workerpool": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", - "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", - "dev": true + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "7.0.0", @@ -14344,17 +14661,72 @@ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -15430,6 +15802,71 @@ "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", @@ -15769,6 +16206,13 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.15.2.tgz", "integrity": "sha512-CjbOKwk2s+3xPIMcd5UNYQzsf+v94RczbdNix9/kQh38WiQkM90sUOi3if8eyHFgiBjBjhwXrA7W3ydiSQP9mw==" }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, "@polka/url": { "version": "1.0.0-next.21", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", @@ -16270,12 +16714,6 @@ } } }, - "@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, "@vscode/extension-telemetry": { "version": "0.8.4", "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz", @@ -18631,6 +19069,12 @@ } } }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -20241,12 +20685,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, "gulp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.0.tgz", @@ -21387,6 +21825,16 @@ "is-object": "^1.0.1" } }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, "jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -22089,6 +22537,12 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -22106,41 +22560,37 @@ "optional": true }, "mocha": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", - "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", - "dev": true, - "requires": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.3", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "4.2.1", - "ms": "2.1.3", - "nanoid": "3.3.1", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "workerpool": "6.2.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", + "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" }, "dependencies": { "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true }, "argparse": { @@ -22149,27 +22599,50 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } + "ms": "^2.1.3" } }, "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true }, "escape-string-regexp": { @@ -22188,6 +22661,41 @@ "path-exists": "^4.0.0" } }, + "foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + } + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "dependencies": { + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -22213,12 +22721,12 @@ } }, "minimatch": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", - "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" } }, "ms": { @@ -22227,12 +22735,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", - "dev": true - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -22257,6 +22759,33 @@ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, "supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -22273,19 +22802,25 @@ "dev": true }, "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "requires": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true } } }, @@ -23001,6 +23536,12 @@ "release-zalgo": "^1.0.0" } }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -23131,6 +23672,24 @@ "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", "dev": true }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + } + } + }, "path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -23824,9 +24383,9 @@ } }, "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -24231,6 +24790,17 @@ "strip-ansi": "^6.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "string.prototype.matchall": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", @@ -24290,6 +24860,15 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-dirs": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", @@ -24512,17 +25091,6 @@ "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", "terser": "^5.26.0" - }, - "dependencies": { - "serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - } } }, "test-exclude": { @@ -25663,9 +26231,9 @@ } }, "workerpool": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", - "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "wrap-ansi": { @@ -25705,6 +26273,43 @@ } } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index f773c32c08da..c94be0a12475 100644 --- a/package.json +++ b/package.json @@ -1589,7 +1589,7 @@ "get-port": "^5.1.1", "gulp": "^5.0.0", "gulp-typescript": "^5.0.0", - "mocha": "^9.2.2", + "mocha": "^11.1.0", "mocha-junit-reporter": "^2.0.2", "mocha-multi-reporters": "^1.1.7", "node-has-native-dependencies": "^1.0.2", From b84fce23d9e705f1a4ad45fced63696f4ce9a9b4 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:46:44 -0800 Subject: [PATCH 0878/1136] Prevent python extension from overriding gitbash pwd (#24832) Gitbash repro steps: - Opt into Terminal Env Var experiment - Make sure shell integration setting is on (generic terminal one) - Be on Windows gitbash (with activated environment) - You will see messed up prompt cwd --- src/client/terminals/envCollectionActivation/service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts index a527abe90454..43b8ceeb8e06 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -472,6 +472,7 @@ function shouldSkip(env: string) { 'PYTHONUTF8', // We have deactivate service which takes care of setting it. '_OLD_VIRTUAL_PATH', + 'PWD', ].includes(env); } From 2698d5afe7338505e1694ca55cd4382fc1f1f499 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:54:59 -0800 Subject: [PATCH 0879/1136] remove TERMINAL_DEACTIVATE_PROMPT telemetry event (#24843) event is no longer needed --- src/client/telemetry/constants.ts | 1 - src/client/telemetry/index.ts | 18 +----------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 53420c275e8a..9684627603a0 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -31,7 +31,6 @@ export enum EventName { PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT', PYTHON_NOT_INSTALLED_PROMPT = 'PYTHON_NOT_INSTALLED_PROMPT', CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT', - TERMINAL_DEACTIVATE_PROMPT = 'TERMINAL_DEACTIVATE_PROMPT', REQUIRE_JUPYTER_PROMPT = 'REQUIRE_JUPYTER_PROMPT', ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH', ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 37ae9328c546..58586e7027c0 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -2021,23 +2021,7 @@ export interface IEventNamePropertyMapping { */ selection: 'Allow' | 'Close' | undefined; }; - /** - * Telemetry event sent with details when user clicks the prompt with the following message: - * - * 'We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we suggest the "terminal.integrated.inheritEnv" setting to be changed to false. Would you like to update this setting?' - */ - /* __GDPR__ - "conda_inherit_env_prompt" : { - "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } - } - */ - [EventName.TERMINAL_DEACTIVATE_PROMPT]: { - /** - * `Yes` When 'Allow' option is selected - * `Close` When 'Close' option is selected - */ - selection: 'Edit script' | "Don't show again" | undefined; - }; + /** * Telemetry event sent with details when user attempts to run in interactive window when Jupyter is not installed. */ From cd992fc892b879e2ff423a132c6a2716aec9e913 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:55:26 -0800 Subject: [PATCH 0880/1136] remove stale debugging telemetry (#24842) --- .../application/debugSessionTelemetry.ts | 80 ----- src/client/common/serviceRegistry.ts | 5 - .../debugger/extension/adapter/factory.ts | 7 - .../configuration/resolvers/attach.ts | 1 - .../extension/configuration/resolvers/base.ts | 33 -- .../configuration/resolvers/launch.ts | 7 +- .../debugger/extension/debugCommands.ts | 1 - .../hooks/childProcessAttachService.ts | 3 - src/client/telemetry/constants.ts | 10 - src/client/telemetry/index.ts | 322 ------------------ src/client/telemetry/types.ts | 1 - src/test/common/moduleInstaller.test.ts | 5 - .../extension/adapter/factory.unit.test.ts | 5 - 13 files changed, 1 insertion(+), 479 deletions(-) delete mode 100644 src/client/common/application/debugSessionTelemetry.ts diff --git a/src/client/common/application/debugSessionTelemetry.ts b/src/client/common/application/debugSessionTelemetry.ts deleted file mode 100644 index 42b8b2651092..000000000000 --- a/src/client/common/application/debugSessionTelemetry.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { inject, injectable } from 'inversify'; -import { DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, ProviderResult } from 'vscode'; -import { DebugProtocol } from 'vscode-debugprotocol'; - -import { IExtensionSingleActivationService } from '../../activation/types'; -import { AttachRequestArguments, ConsoleType, LaunchRequestArguments, TriggerType } from '../../debugger/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { IDisposableRegistry } from '../types'; -import { StopWatch } from '../utils/stopWatch'; -import { IDebugService } from './types'; - -function isResponse(a: any): a is DebugProtocol.Response { - return a.type === 'response'; -} -class TelemetryTracker implements DebugAdapterTracker { - private timer = new StopWatch(); - private readonly trigger: TriggerType = 'launch'; - private readonly console: ConsoleType | undefined; - - constructor(session: DebugSession) { - this.trigger = session.configuration.request as TriggerType; - const debugConfiguration = session.configuration as Partial; - this.console = debugConfiguration.console; - } - - public onWillStartSession() { - this.sendTelemetry(EventName.DEBUG_SESSION_START); - } - - public onDidSendMessage(message: any): void { - if (isResponse(message)) { - if (message.command === 'configurationDone') { - // "configurationDone" response is sent immediately after user code starts running. - this.sendTelemetry(EventName.DEBUG_SESSION_USER_CODE_RUNNING); - } - } - } - - public onWillStopSession(): void { - this.sendTelemetry(EventName.DEBUG_SESSION_STOP); - } - - public onError?(_error: Error): void { - this.sendTelemetry(EventName.DEBUG_SESSION_ERROR); - } - - private sendTelemetry(eventName: EventName): void { - if (eventName === EventName.DEBUG_SESSION_START) { - this.timer.reset(); - } - const telemetryProps = { - trigger: this.trigger, - console: this.console, - }; - sendTelemetryEvent(eventName, this.timer.elapsedTime, telemetryProps); - } -} - -@injectable() -export class DebugSessionTelemetry implements DebugAdapterTrackerFactory, IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; - constructor( - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(IDebugService) debugService: IDebugService, - ) { - disposableRegistry.push(debugService.registerDebugAdapterTrackerFactory('python', this)); - } - - public async activate(): Promise { - // We actually register in the constructor. Not necessary to do it here - } - - public createDebugAdapterTracker(session: DebugSession): ProviderResult { - return new TelemetryTracker(session); - } -} diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 5b9eb544f93b..abd2b220e400 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -28,7 +28,6 @@ import { CommandManager } from './application/commandManager'; import { ReloadVSCodeCommandHandler } from './application/commands/reloadCommand'; import { ReportIssueCommandHandler } from './application/commands/reportIssueCommand'; import { DebugService } from './application/debugService'; -import { DebugSessionTelemetry } from './application/debugSessionTelemetry'; import { DocumentManager } from './application/documentManager'; import { Extensions } from './application/extensions'; import { LanguageService } from './application/languageService'; @@ -189,8 +188,4 @@ export function registerTypes(serviceManager: IServiceManager): void { IExtensionSingleActivationService, ReportIssueCommandHandler, ); - serviceManager.addSingleton( - IExtensionSingleActivationService, - DebugSessionTelemetry, - ); } diff --git a/src/client/debugger/extension/adapter/factory.ts b/src/client/debugger/extension/adapter/factory.ts index 546414699971..edef16368dc0 100644 --- a/src/client/debugger/extension/adapter/factory.ts +++ b/src/client/debugger/extension/adapter/factory.ts @@ -17,8 +17,6 @@ import { EXTENSION_ROOT_DIR } from '../../../constants'; import { IInterpreterService } from '../../../interpreter/contracts'; import { traceError, traceLog, traceVerbose } from '../../../logging'; import { PythonEnvironment } from '../../../pythonEnvironments/info'; -import { sendTelemetryEvent } from '../../../telemetry'; -import { EventName } from '../../../telemetry/constants'; import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; import { IDebugAdapterDescriptorFactory } from '../types'; import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; @@ -76,10 +74,6 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac const command = await this.getDebugAdapterPython(configuration, session.workspaceFolder); if (command.length !== 0) { - if (configuration.request === 'attach' && configuration.processId !== undefined) { - sendTelemetryEvent(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS); - } - const executable = command.shift() ?? 'python'; // "logToFile" is not handled directly by the adapter - instead, we need to pass @@ -100,7 +94,6 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac const args = command.concat([debuggerAdapterPathToUse, ...logArgs]); traceLog(`DAP Server launched with command: ${executable} ${args.join(' ')}`); - sendTelemetryEvent(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH, undefined, { usingWheels: true }); return new DebugAdapterExecutable(executable, args); } diff --git a/src/client/debugger/extension/configuration/resolvers/attach.ts b/src/client/debugger/extension/configuration/resolvers/attach.ts index 6e5ca501463e..1c232f261d03 100644 --- a/src/client/debugger/extension/configuration/resolvers/attach.ts +++ b/src/client/debugger/extension/configuration/resolvers/attach.ts @@ -87,7 +87,6 @@ export class AttachConfigurationResolver extends BaseConfigurationResolver ): boolean { return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK'); } - - protected static sendTelemetry( - trigger: 'launch' | 'attach' | 'test', - debugConfiguration: Partial, - ): void { - const name = debugConfiguration.name || ''; - const moduleName = debugConfiguration.module || ''; - const telemetryProps: DebuggerTelemetry = { - trigger, - console: debugConfiguration.console, - hasEnvVars: typeof debugConfiguration.env === 'object' && Object.keys(debugConfiguration.env).length > 0, - django: !!debugConfiguration.django, - fastapi: BaseConfigurationResolver.isDebuggingFastAPI(debugConfiguration), - flask: BaseConfigurationResolver.isDebuggingFlask(debugConfiguration), - hasArgs: Array.isArray(debugConfiguration.args) && debugConfiguration.args.length > 0, - isLocalhost: BaseConfigurationResolver.isLocalHost(debugConfiguration.host), - isModule: moduleName.length > 0, - isSudo: !!debugConfiguration.sudo, - jinja: !!debugConfiguration.jinja, - pyramid: !!debugConfiguration.pyramid, - stopOnEntry: !!debugConfiguration.stopOnEntry, - showReturnValue: !!debugConfiguration.showReturnValue, - subProcess: !!debugConfiguration.subProcess, - watson: name.toLowerCase().indexOf('watson') >= 0, - pyspark: name.toLowerCase().indexOf('pyspark') >= 0, - gevent: name.toLowerCase().indexOf('gevent') >= 0, - scrapy: moduleName.toLowerCase() === 'scrapy', - }; - sendTelemetryEvent(EventName.DEBUGGER, undefined, telemetryProps); - } } diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts index d9bedaa5e4d5..3ca38fb0f710 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -13,7 +13,7 @@ import { EnvironmentVariables } from '../../../../common/variables/types'; import { IEnvironmentActivationService } from '../../../../interpreter/activation/types'; import { IInterpreterService } from '../../../../interpreter/contracts'; import { DebuggerTypeName } from '../../../constants'; -import { DebugOptions, DebugPurpose, LaunchRequestArguments } from '../../../types'; +import { DebugOptions, LaunchRequestArguments } from '../../../types'; import { BaseConfigurationResolver } from './base'; import { getProgram, IDebugEnvironmentVariablesService } from './helper'; import { @@ -194,11 +194,6 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver 0 ? pathMappings : undefined; } - const trigger = - debugConfiguration.purpose?.includes(DebugPurpose.DebugTest) || debugConfiguration.request === 'test' - ? 'test' - : 'launch'; - LaunchConfigurationResolver.sendTelemetry(trigger, debugConfiguration); } protected async validateLaunchConfiguration( diff --git a/src/client/debugger/extension/debugCommands.ts b/src/client/debugger/extension/debugCommands.ts index b3322e8e7dd1..629f8616a6d6 100644 --- a/src/client/debugger/extension/debugCommands.ts +++ b/src/client/debugger/extension/debugCommands.ts @@ -33,7 +33,6 @@ export class DebugCommands implements IExtensionSingleActivationService { public activate(): Promise { this.disposables.push( this.commandManager.registerCommand(Commands.Debug_In_Terminal, async (file?: Uri) => { - sendTelemetryEvent(EventName.DEBUG_IN_TERMINAL_BUTTON); const interpreter = await this.interpreterService.getActiveInterpreter(file); if (!interpreter) { this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); diff --git a/src/client/debugger/extension/hooks/childProcessAttachService.ts b/src/client/debugger/extension/hooks/childProcessAttachService.ts index 24eaf1b52769..39556f94c87c 100644 --- a/src/client/debugger/extension/hooks/childProcessAttachService.ts +++ b/src/client/debugger/extension/hooks/childProcessAttachService.ts @@ -7,8 +7,6 @@ import { inject, injectable } from 'inversify'; import { IDebugService } from '../../../common/application/types'; import { DebugConfiguration, DebugSession, l10n, WorkspaceFolder, DebugSessionOptions } from 'vscode'; import { noop } from '../../../common/utils/misc'; -import { captureTelemetry } from '../../../telemetry'; -import { EventName } from '../../../telemetry/constants'; import { AttachRequestArguments } from '../../types'; import { IChildProcessAttachService } from './types'; import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; @@ -22,7 +20,6 @@ import { getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; export class ChildProcessAttachService implements IChildProcessAttachService { constructor(@inject(IDebugService) private readonly debugService: IDebugService) {} - @captureTelemetry(EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS) public async attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise { const debugConfig: AttachRequestArguments & DebugConfiguration = data; const folder = this.getRelatedWorkspaceFolder(debugConfig); diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 9684627603a0..ecc44177338a 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -38,16 +38,6 @@ export enum EventName { EXECUTION_CODE = 'EXECUTION_CODE', EXECUTION_DJANGO = 'EXECUTION_DJANGO', - DEBUG_IN_TERMINAL_BUTTON = 'DEBUG.IN_TERMINAL', - DEBUG_ADAPTER_USING_WHEELS_PATH = 'DEBUG_ADAPTER.USING_WHEELS_PATH', - DEBUG_SESSION_ERROR = 'DEBUG_SESSION.ERROR', - DEBUG_SESSION_START = 'DEBUG_SESSION.START', - DEBUG_SESSION_STOP = 'DEBUG_SESSION.STOP', - DEBUG_SESSION_USER_CODE_RUNNING = 'DEBUG_SESSION.USER_CODE_RUNNING', - DEBUGGER = 'DEBUGGER', - DEBUGGER_ATTACH_TO_CHILD_PROCESS = 'DEBUGGER.ATTACH_TO_CHILD_PROCESS', - DEBUGGER_ATTACH_TO_LOCAL_PROCESS = 'DEBUGGER.ATTACH_TO_LOCAL_PROCESS', - // Python testing specific telemetry UNITTEST_CONFIGURING = 'UNITTEST.CONFIGURING', UNITTEST_CONFIGURE = 'UNITTEST.CONFIGURE', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 58586e7027c0..f71f963d0156 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -9,7 +9,6 @@ import { AppinsightsKey, isTestExecution, isUnitTestExecution, PVSC_EXTENSION_ID import type { TerminalShellType } from '../common/terminal/types'; import { isPromise } from '../common/utils/async'; import { StopWatch } from '../common/utils/stopWatch'; -import { ConsoleType, TriggerType } from '../debugger/types'; import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info'; import { TensorBoardPromptSelection } from '../tensorBoard/constants'; import { EventName } from './constants'; @@ -300,327 +299,6 @@ type FailedEventType = { failed: true }; // Map all events to their properties export interface IEventNamePropertyMapping { - /** - * Telemetry event sent when debug in terminal button was used to debug current file. - */ - /* __GDPR__ - "debug_in_terminal_button" : { "owner": "paulacamargo25" } - */ - [EventName.DEBUG_IN_TERMINAL_BUTTON]: never | undefined; - /** - * Telemetry event captured when debug adapter executable is created - */ - /* __GDPR__ - "debug_adapter.using_wheels_path" : { - "usingwheels" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" } - } - */ - - [EventName.DEBUG_ADAPTER_USING_WHEELS_PATH]: { - /** - * Carries boolean - * - `true` if path used for the adapter is the debugger with wheels. - * - `false` if path used for the adapter is the source only version of the debugger. - */ - usingWheels: boolean; - }; - /** - * Telemetry captured before starting debug session. - */ - /* __GDPR__ - "debug_session.start" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" }, - "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } - } - */ - [EventName.DEBUG_SESSION_START]: { - /** - * Trigger for starting the debugger. - * - `launch`: Launch/start new code and debug it. - * - `attach`: Attach to an exiting python process (remote debugging). - * - `test`: Debugging python tests. - * - * @type {TriggerType} - */ - trigger: TriggerType; - /** - * Type of console used. - * -`internalConsole`: Use VS Code debug console (no shells/terminals). - * - `integratedTerminal`: Use VS Code terminal. - * - `externalTerminal`: Use an External terminal. - * - * @type {ConsoleType} - */ - console?: ConsoleType; - }; - /** - * Telemetry captured when debug session runs into an error. - */ - /* __GDPR__ - "debug_session.error" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" }, - "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } - } - */ - [EventName.DEBUG_SESSION_ERROR]: { - /** - * Trigger for starting the debugger. - * - `launch`: Launch/start new code and debug it. - * - `attach`: Attach to an exiting python process (remote debugging). - * - `test`: Debugging python tests. - * - * @type {TriggerType} - */ - trigger: TriggerType; - /** - * Type of console used. - * -`internalConsole`: Use VS Code debug console (no shells/terminals). - * - `integratedTerminal`: Use VS Code terminal. - * - `externalTerminal`: Use an External terminal. - * - * @type {ConsoleType} - */ - console?: ConsoleType; - }; - /** - * Telemetry captured after stopping debug session. - */ - /* __GDPR__ - "debug_session.stop" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" }, - "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } - } - */ - [EventName.DEBUG_SESSION_STOP]: { - /** - * Trigger for starting the debugger. - * - `launch`: Launch/start new code and debug it. - * - `attach`: Attach to an exiting python process (remote debugging). - * - `test`: Debugging python tests. - * - * @type {TriggerType} - */ - trigger: TriggerType; - /** - * Type of console used. - * -`internalConsole`: Use VS Code debug console (no shells/terminals). - * - `integratedTerminal`: Use VS Code terminal. - * - `externalTerminal`: Use an External terminal. - * - * @type {ConsoleType} - */ - console?: ConsoleType; - }; - /** - * Telemetry captured when user code starts running after loading the debugger. - */ - /* __GDPR__ - "debug_session.user_code_running" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" }, - "console" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "paulacamargo25" } - } - */ - [EventName.DEBUG_SESSION_USER_CODE_RUNNING]: { - /** - * Trigger for starting the debugger. - * - `launch`: Launch/start new code and debug it. - * - `attach`: Attach to an exiting python process (remote debugging). - * - `test`: Debugging python tests. - * - * @type {TriggerType} - */ - trigger: TriggerType; - /** - * Type of console used. - * -`internalConsole`: Use VS Code debug console (no shells/terminals). - * - `integratedTerminal`: Use VS Code terminal. - * - `externalTerminal`: Use an External terminal. - * - * @type {ConsoleType} - */ - console?: ConsoleType; - }; - /** - * Telemetry captured when starting the debugger. - */ - /* __GDPR__ - "debugger" : { - "trigger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "console" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "hasenvvars": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "hasargs": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "django": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "fastapi": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "flask": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "jinja": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "islocalhost": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "ismodule": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "issudo": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "stoponentry": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "showreturnvalue": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "pyramid": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "subprocess": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "watson": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "pyspark": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "gevent": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" }, - "scrapy": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "paulacamargo25" } - } - */ - [EventName.DEBUGGER]: { - /** - * Trigger for starting the debugger. - * - `launch`: Launch/start new code and debug it. - * - `attach`: Attach to an exiting python process (remote debugging). - * - `test`: Debugging python tests. - * - * @type {TriggerType} - */ - trigger: TriggerType; - /** - * Type of console used. - * -`internalConsole`: Use VS Code debug console (no shells/terminals). - * - `integratedTerminal`: Use VS Code terminal. - * - `externalTerminal`: Use an External terminal. - * - * @type {ConsoleType} - */ - console?: ConsoleType; - /** - * Whether user has defined environment variables. - * Could have been defined in launch.json or the env file (defined in `settings.json`). - * Default `env file` is `.env` in the workspace folder. - * - * @type {boolean} - */ - hasEnvVars: boolean; - /** - * Whether there are any CLI arguments that need to be passed into the program being debugged. - * - * @type {boolean} - */ - hasArgs: boolean; - /** - * Whether the user is debugging `django`. - * - * @type {boolean} - */ - django: boolean; - /** - * Whether the user is debugging `fastapi`. - * - * @type {boolean} - */ - fastapi: boolean; - /** - * Whether the user is debugging `flask`. - * - * @type {boolean} - */ - flask: boolean; - /** - * Whether the user is debugging `jinja` templates. - * - * @type {boolean} - */ - jinja: boolean; - /** - * Whether user is attaching to a local python program (attach scenario). - * - * @type {boolean} - */ - isLocalhost: boolean; - /** - * Whether debugging a module. - * - * @type {boolean} - */ - isModule: boolean; - /** - * Whether debugging with `sudo`. - * - * @type {boolean} - */ - isSudo: boolean; - /** - * Whether required to stop upon entry. - * - * @type {boolean} - */ - stopOnEntry: boolean; - /** - * Whether required to display return types in debugger. - * - * @type {boolean} - */ - showReturnValue: boolean; - /** - * Whether debugging `pyramid`. - * - * @type {boolean} - */ - pyramid: boolean; - /** - * Whether debugging a subprocess. - * - * @type {boolean} - */ - subProcess: boolean; - /** - * Whether debugging `watson`. - * - * @type {boolean} - */ - watson: boolean; - /** - * Whether degbugging `pyspark`. - * - * @type {boolean} - */ - pyspark: boolean; - /** - * Whether using `gevent` when debugging. - * - * @type {boolean} - */ - gevent: boolean; - /** - * Whether debugging `scrapy`. - * - * @type {boolean} - */ - scrapy: boolean; - }; - /** - * Telemetry event sent when attaching to child process - */ - /* __GDPR__ - "debugger.attach_to_child_process" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "paulacamargo25" } - } - */ - [EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS]: never | undefined; - /** - * Telemetry event sent when attaching to a local process. - */ - /* __GDPR__ - "debugger.attach_to_local_process" : { "owner": "paulacamargo25" } - */ - [EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS]: never | undefined; - /** - * Telemetry event sent with details of actions when invoking a diagnostic command - */ - /* __GDPR__ - "diagnostics.action" : { - "commandname" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "ignorecode" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "url" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, - "action" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } - } - */ [EventName.DIAGNOSTICS_ACTION]: { /** * Diagnostics command executed. diff --git a/src/client/telemetry/types.ts b/src/client/telemetry/types.ts index 865dca278bf0..42e51b261129 100644 --- a/src/client/telemetry/types.ts +++ b/src/client/telemetry/types.ts @@ -9,7 +9,6 @@ import { EventName } from './constants'; export type EditorLoadTelemetry = IEventNamePropertyMapping[EventName.EDITOR_LOAD]; export type PythonInterpreterTelemetry = IEventNamePropertyMapping[EventName.PYTHON_INTERPRETER]; -export type DebuggerTelemetry = IEventNamePropertyMapping[EventName.DEBUGGER]; export type TestTool = 'pytest' | 'unittest'; export type TestRunTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_RUN]; export type TestDiscoveryTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_DONE]; diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index 0b900b97c4c0..0cdb6f270c54 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -13,7 +13,6 @@ import { CommandManager } from '../../client/common/application/commandManager'; import { ReloadVSCodeCommandHandler } from '../../client/common/application/commands/reloadCommand'; import { ReportIssueCommandHandler } from '../../client/common/application/commands/reportIssueCommand'; import { DebugService } from '../../client/common/application/debugService'; -import { DebugSessionTelemetry } from '../../client/common/application/debugSessionTelemetry'; import { DocumentManager } from '../../client/common/application/documentManager'; import { Extensions } from '../../client/common/application/extensions'; import { @@ -259,10 +258,6 @@ suite('Module Installer', () => { IExtensionSingleActivationService, ReportIssueCommandHandler, ); - ioc.serviceManager.addSingleton( - IExtensionSingleActivationService, - DebugSessionTelemetry, - ); } test('Ensure pip is supported and conda is not', async () => { ioc.serviceManager.addSingletonInstance( diff --git a/src/test/debugger/extension/adapter/factory.unit.test.ts b/src/test/debugger/extension/adapter/factory.unit.test.ts index fde87d930078..50984327e40d 100644 --- a/src/test/debugger/extension/adapter/factory.unit.test.ts +++ b/src/test/debugger/extension/adapter/factory.unit.test.ts @@ -23,7 +23,6 @@ import { IInterpreterService } from '../../../../client/interpreter/contracts'; import { InterpreterService } from '../../../../client/interpreter/interpreterService'; import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; import { clearTelemetryReporter } from '../../../../client/telemetry'; -import { EventName } from '../../../../client/telemetry/constants'; import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; import { ICommandManager } from '../../../../client/common/application/types'; @@ -269,16 +268,12 @@ suite('Debugging - Adapter Factory', () => { test('Send attach to local process telemetry if attaching to a local process', async () => { const session = createSession({ request: 'attach', processId: 1234 }); await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.ok(Reporter.eventNames.includes(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS)); }); test("Don't send any telemetry if not attaching to a local process", async () => { const session = createSession({}); await factory.createDebugAdapterDescriptor(session, nodeExecutable); - - assert.ok(Reporter.eventNames.includes(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH)); }); test('Use "debugAdapterPath" when specified', async () => { From 60d04730b3c23e235a3f2e547f0f821a91ed8218 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 24 Feb 2025 12:53:42 -0800 Subject: [PATCH 0881/1136] fix: identify script/module launch vs repl launch from terminal (#24844) closes https://github.com/microsoft/vscode-python/issues/24526 --- src/client/telemetry/index.ts | 7 ++++++- .../codeExecution/terminalReplWatcher.ts | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index f71f963d0156..6c97bd083d96 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1972,8 +1972,13 @@ export interface IEventNamePropertyMapping { [EventName.REPL]: { /** * Whether the user launched the Terminal REPL or Native REPL + * + * Terminal - Terminal REPL user ran `Python: Start Terminal REPL` command. + * Native - Native REPL user ran `Python: Start Native Python REPL` command. + * manualTerminal - User started REPL in terminal using `python`, `python3` or `py` etc without arguments in terminal. + * runningScript - User ran a script in terminal like `python myscript.py`. */ - replType: 'Terminal' | 'Native' | 'manualTerminal'; + replType: 'Terminal' | 'Native' | 'manualTerminal' | `runningScript`; }; /** * Telemetry event sent if and when user configure tests command. This command can be trigerred from multiple places in the extension. (Command palette, prompt etc.) diff --git a/src/client/terminals/codeExecution/terminalReplWatcher.ts b/src/client/terminals/codeExecution/terminalReplWatcher.ts index bab70cb2f654..951961ab6901 100644 --- a/src/client/terminals/codeExecution/terminalReplWatcher.ts +++ b/src/client/terminals/codeExecution/terminalReplWatcher.ts @@ -3,16 +3,24 @@ import { onDidStartTerminalShellExecution } from '../../common/vscodeApis/window import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -function checkREPLCommand(command: string): boolean { +function checkREPLCommand(command: string): undefined | 'manualTerminal' | `runningScript` { const lower = command.toLowerCase().trimStart(); - return lower.startsWith('python') || lower.startsWith('py '); + if (lower.startsWith('python') || lower.startsWith('py ')) { + const parts = lower.split(' '); + if (parts.length === 1) { + return 'manualTerminal'; + } + return 'runningScript'; + } + return undefined; } export function registerTriggerForTerminalREPL(disposables: Disposable[]): void { disposables.push( onDidStartTerminalShellExecution(async (e: TerminalShellExecutionStartEvent) => { - if (e.execution.commandLine.isTrusted && checkREPLCommand(e.execution.commandLine.value)) { - sendTelemetryEvent(EventName.REPL, undefined, { replType: 'manualTerminal' }); + const replType = checkREPLCommand(e.execution.commandLine.value); + if (e.execution.commandLine.isTrusted && replType) { + sendTelemetryEvent(EventName.REPL, undefined, { replType }); } }), ); From 5cdbc60550a71556b77f3afb4f6b8ef1bcd41c70 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 24 Feb 2025 13:01:04 -0800 Subject: [PATCH 0882/1136] fix: ensure interpreter change event is raised when using environments extension (#24838) Fixes https://github.com/microsoft/pylance-release/issues/6968 --- src/client/envExt/api.internal.ts | 55 +++++++++++++++++--- src/client/environmentApi.ts | 15 +++++- src/client/extensionActivation.ts | 2 + src/test/common/persistentState.unit.test.ts | 9 ++++ 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/src/client/envExt/api.internal.ts b/src/client/envExt/api.internal.ts index a47193d3cd95..7d511eac49ea 100644 --- a/src/client/envExt/api.internal.ts +++ b/src/client/envExt/api.internal.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Terminal, Uri } from 'vscode'; +import { EventEmitter, Terminal, Uri, Disposable, ConfigurationTarget } from 'vscode'; import { getExtension } from '../common/vscodeApis/extensionsApi'; import { GetEnvironmentScope, @@ -10,8 +10,10 @@ import { PythonEnvironmentApi, PythonProcess, RefreshEnvironmentsScope, + DidChangeEnvironmentEventArgs, } from './types'; import { executeCommand } from '../common/vscodeApis/commandApis'; +import { IInterpreterPathService } from '../common/types'; export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; @@ -24,6 +26,17 @@ export function useEnvExtension(): boolean { return _useExt; } +const onDidChangeEnvironmentEnvExtEmitter: EventEmitter = new EventEmitter< + DidChangeEnvironmentEventArgs +>(); +export function onDidChangeEnvironmentEnvExt( + listener: (e: DidChangeEnvironmentEventArgs) => unknown, + thisArgs?: unknown, + disposables?: Disposable[], +): Disposable { + return onDidChangeEnvironmentEnvExtEmitter.event(listener, thisArgs, disposables); +} + let _extApi: PythonEnvironmentApi | undefined; export async function getEnvExtApi(): Promise { if (_extApi) { @@ -33,14 +46,15 @@ export async function getEnvExtApi(): Promise { if (!extension) { throw new Error('Python Environments extension not found.'); } - if (extension?.isActive) { - _extApi = extension.exports as PythonEnvironmentApi; - return _extApi; + if (!extension?.isActive) { + await extension.activate(); } - await extension.activate(); - _extApi = extension.exports as PythonEnvironmentApi; + _extApi.onDidChangeEnvironment((e) => { + onDidChangeEnvironmentEnvExtEmitter.fire(e); + }); + return _extApi; } @@ -106,3 +120,32 @@ export async function clearCache(): Promise { await executeCommand('python-envs.clearCache'); } } + +export function registerEnvExtFeatures( + disposables: Disposable[], + interpreterPathService: IInterpreterPathService, +): void { + if (useEnvExtension()) { + disposables.push( + onDidChangeEnvironmentEnvExt(async (e: DidChangeEnvironmentEventArgs) => { + const previousPath = interpreterPathService.get(e.uri); + + if (previousPath !== e.new?.environmentPath.fsPath) { + if (e.uri) { + await interpreterPathService.update( + e.uri, + ConfigurationTarget.WorkspaceFolder, + e.new?.environmentPath.fsPath, + ); + } else { + await interpreterPathService.update( + undefined, + ConfigurationTarget.Global, + e.new?.environmentPath.fsPath, + ); + } + } + }), + ); + } +} diff --git a/src/client/environmentApi.ts b/src/client/environmentApi.ts index 558938d7d0b7..ecd8eef21845 100644 --- a/src/client/environmentApi.ts +++ b/src/client/environmentApi.ts @@ -11,7 +11,7 @@ import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironment import { getEnvPath } from './pythonEnvironments/base/info/env'; import { IDiscoveryAPI, ProgressReportStage } from './pythonEnvironments/base/locator'; import { IPythonExecutionFactory } from './common/process/types'; -import { traceError, traceVerbose } from './logging'; +import { traceError, traceInfo, traceVerbose } from './logging'; import { isParentPath, normCasePath } from './common/platform/fs-paths'; import { sendTelemetryEvent } from './telemetry'; import { EventName } from './telemetry/constants'; @@ -42,7 +42,13 @@ type ActiveEnvironmentChangeEvent = { }; const onDidActiveInterpreterChangedEvent = new EventEmitter(); +const previousEnvMap = new Map(); export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangeEvent): void { + const oldPath = previousEnvMap.get(e.resource?.uri.fsPath ?? ''); + if (oldPath === e.path) { + return; + } + previousEnvMap.set(e.resource?.uri.fsPath ?? '', e.path); onDidActiveInterpreterChangedEvent.fire({ id: getEnvID(e.path), path: e.path, resource: e.resource }); reportActiveInterpreterChangedDeprecated({ path: e.path, resource: e.resource?.uri }); } @@ -172,6 +178,13 @@ export function buildEnvironmentApi( } disposables.push( + onDidActiveInterpreterChangedEvent.event((e) => { + let scope = 'global'; + if (e.resource) { + scope = e.resource instanceof Uri ? e.resource.fsPath : e.resource.uri.fsPath; + } + traceInfo(`Active interpreter [${scope}]: `, e.path); + }), discoveryApi.onProgress((e) => { if (e.stage === ProgressReportStage.discoveryFinished) { knownCache = initKnownCache(); diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 4a1acca62da5..362fcf8468ad 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -56,6 +56,7 @@ import { registerTriggerForTerminalREPL } from './terminals/codeExecution/termin import { registerPythonStartup } from './terminals/pythonStartup'; import { registerPixiFeatures } from './pythonEnvironments/common/environmentManagers/pixi'; import { registerCustomTerminalLinkProvider } from './terminals/pythonStartupLinkProvider'; +import { registerEnvExtFeatures } from './envExt/api.internal'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -101,6 +102,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components): const interpreterService: IInterpreterService = ext.legacyIOC.serviceContainer.get( IInterpreterService, ); + registerEnvExtFeatures(ext.disposables, interpreterPathService); const pathUtils = ext.legacyIOC.serviceContainer.get(IPathUtils); registerPixiFeatures(ext.disposables); registerAllCreateEnvironmentFeatures( diff --git a/src/test/common/persistentState.unit.test.ts b/src/test/common/persistentState.unit.test.ts index 9af28e2f5860..a77ee571559e 100644 --- a/src/test/common/persistentState.unit.test.ts +++ b/src/test/common/persistentState.unit.test.ts @@ -5,6 +5,7 @@ import { assert, expect } from 'chai'; import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; import { Memento } from 'vscode'; import { ICommandManager } from '../../client/common/application/types'; import { Commands } from '../../client/common/constants'; @@ -17,17 +18,25 @@ import { import { IDisposable } from '../../client/common/types'; import { sleep } from '../core'; import { MockMemento } from '../mocks/mementos'; +import * as apiInt from '../../client/envExt/api.internal'; suite('Persistent State', () => { let cmdManager: TypeMoq.IMock; let persistentStateFactory: PersistentStateFactory; let workspaceMemento: Memento; let globalMemento: Memento; + let useEnvExtensionStub: sinon.SinonStub; setup(() => { cmdManager = TypeMoq.Mock.ofType(); workspaceMemento = new MockMemento(); globalMemento = new MockMemento(); persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento, cmdManager.object); + + useEnvExtensionStub = sinon.stub(apiInt, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + }); + teardown(() => { + sinon.restore(); }); test('Global states created are restored on invoking clean storage command', async () => { From 7fd5cb361f41b9977330bacc8efc61dc6ff77588 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 27 Feb 2025 18:31:59 -0800 Subject: [PATCH 0883/1136] Bump release 2025.2.0 (#24853) --- build/azure-pipeline.stable.yml | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index ef8f501b6e5a..a5276bbb09d2 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -102,7 +102,7 @@ extends: project: 'Monaco' definition: 593 buildVersionToDownload: 'latestFromBranch' - branchName: 'refs/heads/release/2024.22' + branchName: 'refs/heads/release/2025.2' targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' artifactName: 'bin-$(vsceTarget)' itemPattern: | diff --git a/package-lock.json b/package-lock.json index 78a149e61569..f806482d959f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.1.0-dev", + "version": "2025.2.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.1.0-dev", + "version": "2025.2.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index c94be0a12475..08cf36ed6e84 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.1.0-dev", + "version": "2025.2.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From 054e682028450c1e3872cf176f96013239e8b353 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Fri, 28 Feb 2025 09:35:12 -0800 Subject: [PATCH 0884/1136] Bump to 2025.3.0-dev (#24854) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f806482d959f..10344748740a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.2.0-rc", + "version": "2025.3.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.2.0-rc", + "version": "2025.3.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 08cf36ed6e84..6732fac75a53 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.2.0-rc", + "version": "2025.3.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From a9c38bfd9c3026dddf180acdc15d82979728bc5f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 5 Mar 2025 19:24:54 +0000 Subject: [PATCH 0885/1136] remove airbnb rules (#24867) fixes https://github.com/microsoft/vscode-python/issues/24101 --- .eslintrc | 5 ++-- package-lock.json | 74 ----------------------------------------------- package.json | 1 - 3 files changed, 3 insertions(+), 77 deletions(-) diff --git a/.eslintrc b/.eslintrc index 6ddb988b21a6..3070b37db800 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,7 +10,6 @@ "no-only-tests" ], "extends": [ - "airbnb", "plugin:@typescript-eslint/recommended", "plugin:import/errors", "plugin:import/warnings", @@ -101,6 +100,8 @@ ], "operator-assignment": "off", "strict": "off", - "no-only-tests/no-only-tests": ["error", { "block": ["test", "suite"], "focus": ["only"] }] + "no-only-tests/no-only-tests": ["error", { "block": ["test", "suite"], "focus": ["only"] }], + "prefer-const": "off", + "import/no-named-as-default-member": "off" } } diff --git a/package-lock.json b/package-lock.json index 10344748740a..8484251cf299 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,7 +74,6 @@ "del": "^6.0.0", "download": "^8.0.0", "eslint": "^7.2.0", - "eslint-config-airbnb": "^18.2.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.3.1", @@ -4362,12 +4361,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "node_modules/confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "dev": true - }, "node_modules/console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -5611,45 +5604,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-config-airbnb": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.2.1.tgz", - "integrity": "sha512-glZNDEZ36VdlZWoxn/bUR1r/sdFKPd1mHPbqUtkctgNG4yT2DLLtJ3D+yCV+jzZCc2V1nBVkmdknOJBZ5Hc0fg==", - "dev": true, - "dependencies": { - "eslint-config-airbnb-base": "^14.2.1", - "object.assign": "^4.1.2", - "object.entries": "^1.1.2" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint": "^5.16.0 || ^6.8.0 || ^7.2.0", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-react": "^7.21.5", - "eslint-plugin-react-hooks": "^4 || ^3 || ^2.3.0 || ^1.7.0" - } - }, - "node_modules/eslint-config-airbnb-base": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", - "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", - "dev": true, - "dependencies": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.2" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint": "^5.16.0 || ^6.8.0 || ^7.2.0", - "eslint-plugin-import": "^2.22.1" - } - }, "node_modules/eslint-config-prettier": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", @@ -18354,12 +18308,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "dev": true - }, "console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -19527,28 +19475,6 @@ } } }, - "eslint-config-airbnb": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.2.1.tgz", - "integrity": "sha512-glZNDEZ36VdlZWoxn/bUR1r/sdFKPd1mHPbqUtkctgNG4yT2DLLtJ3D+yCV+jzZCc2V1nBVkmdknOJBZ5Hc0fg==", - "dev": true, - "requires": { - "eslint-config-airbnb-base": "^14.2.1", - "object.assign": "^4.1.2", - "object.entries": "^1.1.2" - } - }, - "eslint-config-airbnb-base": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", - "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", - "dev": true, - "requires": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.2" - } - }, "eslint-config-prettier": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", diff --git a/package.json b/package.json index 6732fac75a53..a82ae3b280a7 100644 --- a/package.json +++ b/package.json @@ -1577,7 +1577,6 @@ "del": "^6.0.0", "download": "^8.0.0", "eslint": "^7.2.0", - "eslint-config-airbnb": "^18.2.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.3.1", From 5f31ebbcca1af369b17531ae069788f1eb8e42b8 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 5 Mar 2025 21:02:18 +0000 Subject: [PATCH 0886/1136] update eslint version (#24868) --- .eslintrc | 2 +- package-lock.json | 1892 ++++++++--------- package.json | 6 +- src/client/common/pipes/namedPipes.ts | 2 +- src/client/interpreter/activation/service.ts | 2 +- .../common/environmentManagers/activestate.ts | 2 +- .../environmentManagers/condaService.ts | 4 +- .../common/environmentManagers/pixi.ts | 3 +- 8 files changed, 956 insertions(+), 957 deletions(-) diff --git a/.eslintrc b/.eslintrc index 3070b37db800..3535797eb92c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -53,7 +53,7 @@ "@typescript-eslint/no-var-requires": "off", // Other rules - "class-methods-use-this": ["error", {"exceptMethods": ["dispose"]}], + "class-methods-use-this": ["error", { "exceptMethods": ["dispose"] }], "func-names": "off", "import/extensions": "off", "import/namespace": "off", diff --git a/package-lock.json b/package-lock.json index 8484251cf299..10866a3d9ca1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "minimatch": "^5.0.1", "named-js-regexp": "^1.3.3", "node-stream-zip": "^1.6.0", - "reflect-metadata": "^0.1.12", + "reflect-metadata": "^0.2.2", "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", "semver": "^7.5.2", @@ -73,9 +73,9 @@ "cross-spawn": "^6.0.5", "del": "^6.0.0", "download": "^8.0.0", - "eslint": "^7.2.0", + "eslint": "^8.57.1", "eslint-config-prettier": "^8.3.0", - "eslint-plugin-import": "^2.29.1", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-no-only-tests": "^3.3.0", "eslint-plugin-react": "^7.20.3", @@ -455,15 +455,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "node_modules/@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, "node_modules/@babel/compat-data": { "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", @@ -896,18 +887,6 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@eslint-community/regexpp": { "version": "4.11.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", @@ -918,32 +897,44 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/@eslint/eslintrc/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -955,10 +946,11 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -969,13 +961,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "engines": { - "node": ">= 4" + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { @@ -983,6 +979,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -990,11 +987,19 @@ "node": "*" } }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -1002,6 +1007,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@gulpjs/messages": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", @@ -1024,26 +1039,29 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" }, "engines": { "node": ">=10.10.0" } }, "node_modules/@humanwhocodes/config-array/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1059,6 +1077,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1066,11 +1085,34 @@ "node": "*" } }, + "node_modules/@humanwhocodes/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@iarna/toml": { "version": "2.2.5", @@ -1614,11 +1656,19 @@ "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", "dev": true }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@sindresorhus/is": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -2223,17 +2273,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } + "license": "ISC" }, "node_modules/@vscode/extension-telemetry": { "version": "0.8.4", @@ -2680,9 +2725,10 @@ "dev": true }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -2712,6 +2758,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -2931,8 +2978,9 @@ "node_modules/archive-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", - "integrity": "sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=", + "integrity": "sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==", "dev": true, + "license": "MIT", "dependencies": { "file-type": "^4.2.0" }, @@ -2943,8 +2991,9 @@ "node_modules/archive-type/node_modules/file-type": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", - "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=", + "integrity": "sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -3190,15 +3239,6 @@ "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", "dev": true }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", @@ -3441,6 +3481,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -3674,6 +3715,7 @@ "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", "dev": true, + "license": "MIT", "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" @@ -3683,7 +3725,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/buffer-crc32": { "version": "0.2.13", @@ -3703,8 +3746,9 @@ "node_modules/buffer-fill": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", - "dev": true + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true, + "license": "MIT" }, "node_modules/buffer-from": { "version": "1.1.1", @@ -3735,6 +3779,7 @@ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", "integrity": "sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==", "dev": true, + "license": "MIT", "dependencies": { "clone-response": "1.0.2", "get-stream": "3.0.0", @@ -3745,11 +3790,22 @@ "responselike": "1.0.2" } }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/cacheable-request/node_modules/lowercase-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", - "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", + "integrity": "sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3788,6 +3844,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -4253,8 +4319,9 @@ "node_modules/clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", "dev": true, + "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" } @@ -4374,17 +4441,39 @@ "dev": true }, "node_modules/content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, + "license": "MIT", "dependencies": { - "safe-buffer": "5.1.2" + "safe-buffer": "5.2.1" }, "engines": { "node": ">= 0.6" } }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/continuation-local-storage": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", @@ -4732,6 +4821,7 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10" } @@ -4741,6 +4831,7 @@ "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", "dev": true, + "license": "MIT", "dependencies": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", @@ -4758,8 +4849,9 @@ "node_modules/decompress-response": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", "dev": true, + "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" }, @@ -4772,6 +4864,7 @@ "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", "dev": true, + "license": "MIT", "dependencies": { "file-type": "^5.2.0", "is-stream": "^1.1.0", @@ -4784,8 +4877,9 @@ "node_modules/decompress-tar/node_modules/file-type": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4795,6 +4889,7 @@ "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", "dev": true, + "license": "MIT", "dependencies": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", @@ -4811,6 +4906,7 @@ "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4820,6 +4916,7 @@ "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", "dev": true, + "license": "MIT", "dependencies": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", @@ -4832,8 +4929,9 @@ "node_modules/decompress-targz/node_modules/file-type": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4841,8 +4939,9 @@ "node_modules/decompress-unzip": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", "dev": true, + "license": "MIT", "dependencies": { "file-type": "^3.8.0", "get-stream": "^2.2.0", @@ -4856,8 +4955,9 @@ "node_modules/decompress-unzip/node_modules/file-type": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4865,8 +4965,9 @@ "node_modules/decompress-unzip/node_modules/get-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", - "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", "dev": true, + "license": "MIT", "dependencies": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" @@ -4878,8 +4979,9 @@ "node_modules/decompress-unzip/node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4889,6 +4991,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^3.0.0" }, @@ -4899,8 +5002,9 @@ "node_modules/decompress/node_modules/make-dir/node_modules/pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4908,8 +5012,9 @@ "node_modules/decompress/node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5140,6 +5245,7 @@ "resolved": "https://registry.npmjs.org/download/-/download-8.0.0.tgz", "integrity": "sha512-ASRY5QhDk7FK+XrQtQyvhpDKanLluEEQtWl/J7Lxuf/b+i8RYh997QeXvL85xitrmRKVlx9c7eTrcRdq2GS4eA==", "dev": true, + "license": "MIT", "dependencies": { "archive-type": "^4.0.0", "content-disposition": "^0.5.2", @@ -5157,23 +5263,12 @@ "node": ">=10" } }, - "node_modules/download/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/download/node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^4.0.1", "semver": "^5.6.0" @@ -5182,21 +5277,12 @@ "node": ">=6" } }, - "node_modules/download/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/download/node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -5208,10 +5294,11 @@ "dev": true }, "node_modules/duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/duplexify": { "version": "3.7.1", @@ -5336,27 +5423,6 @@ "node": ">=10.13.0" } }, - "node_modules/enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "dependencies": { - "ansi-colors": "^4.1.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/enquirer/node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", @@ -5548,57 +5614,57 @@ } }, "node_modules/eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", + "debug": "^4.3.2", "doctrine": "^3.0.0", - "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -5637,10 +5703,11 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -5658,39 +5725,43 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, + "license": "MIT", "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/debug": { @@ -5886,28 +5957,17 @@ "node": ">=8.0.0" } }, - "node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">=6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/ansi-styles": { @@ -5926,6 +5986,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", @@ -6016,32 +6083,69 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/eslint/node_modules/globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -6061,13 +6165,33 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, "engines": { - "node": ">= 4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint/node_modules/minimatch": { @@ -6082,6 +6206,38 @@ "node": "*" } }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6091,15 +6247,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/eslint/node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6138,6 +6285,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -6146,36 +6294,29 @@ } }, "node_modules/espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/espree/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "engines": { - "node": ">=0.4.0" + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -6188,6 +6329,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -6385,6 +6527,7 @@ "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": "^1.28.0" }, @@ -6397,6 +6540,7 @@ "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", "dev": true, + "license": "MIT", "dependencies": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" @@ -6529,6 +6673,7 @@ "resolved": "https://registry.npmjs.org/file-type/-/file-type-11.1.0.tgz", "integrity": "sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -6536,8 +6681,9 @@ "node_modules/filename-reserved-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", - "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -6547,6 +6693,7 @@ "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-3.0.0.tgz", "integrity": "sha512-5EFZ//MsvJgXjBAFJ+Bh2YaCTRF/VP1YOmGrgt+KJ4SFRLjI87EIdwLLuT6wQX0I4F9W41xutobzczjsOKlI/g==", "dev": true, + "license": "MIT", "dependencies": { "filename-reserved-regex": "^2.0.0", "strip-outer": "^1.0.0", @@ -6607,15 +6754,6 @@ "node": ">=8" } }, - "node_modules/find-up/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/findup-sync": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", @@ -6807,8 +6945,9 @@ "node_modules/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" @@ -6925,12 +7064,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, "node_modules/functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -7008,12 +7141,27 @@ } }, "node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, "engines": { - "node": ">=4" + "node": ">=6" + } + }, + "node_modules/get-stream/node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, "node_modules/get-symbol-description": { @@ -7248,6 +7396,7 @@ "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", "dev": true, + "license": "MIT", "dependencies": { "@sindresorhus/is": "^0.7.0", "cacheable-request": "^2.1.1", @@ -7271,11 +7420,22 @@ "node": ">=4" } }, + "node_modules/got/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/got/node_modules/pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -7285,12 +7445,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "node_modules/graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -7743,6 +7897,7 @@ "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -7764,6 +7919,7 @@ "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", "dev": true, + "license": "MIT", "dependencies": { "has-symbol-support-x": "^1.4.1" }, @@ -7884,7 +8040,8 @@ "version": "3.8.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/http-proxy-agent": { "version": "4.0.1", @@ -8007,10 +8164,11 @@ "dev": true }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -8027,6 +8185,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -8125,8 +8284,9 @@ "node_modules/into-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", - "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "integrity": "sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==", "dev": true, + "license": "MIT", "dependencies": { "from2": "^2.1.1", "p-is-promise": "^1.1.0" @@ -8244,9 +8404,10 @@ } }, "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -8366,8 +8527,9 @@ "node_modules/is-natural-number": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", - "dev": true + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true, + "license": "MIT" }, "node_modules/is-negated-glob": { "version": "1.0.0", @@ -8415,10 +8577,14 @@ } }, "node_modules/is-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", - "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", - "dev": true + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/is-path-cwd": { "version": "2.2.0", @@ -8441,8 +8607,9 @@ "node_modules/is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8488,10 +8655,11 @@ } }, "node_modules/is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8514,8 +8682,9 @@ "node_modules/is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8866,6 +9035,7 @@ "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", "dev": true, + "license": "MIT", "dependencies": { "has-to-string-tag-x": "^1.2.0", "is-object": "^1.0.1" @@ -8975,8 +9145,9 @@ "node_modules/json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", - "dev": true + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -9125,6 +9296,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.0" } @@ -9384,12 +9556,6 @@ "lodash._reinterpolate": "^3.0.0" } }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", - "dev": true - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -9502,6 +9668,7 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -9691,6 +9858,7 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -10097,15 +10265,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mocha/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/mocha/node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -10246,10 +10405,23 @@ "integrity": "sha512-XO0DPujDP9IWpkt690iWLreKztb/VB811DGl5N3z7BfhkMJuiVZXOi6YN/fEB9qkvtMVTgSZDW8pzdVt8vj/FA==" }, "node_modules/nanoid": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", - "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==", - "dev": true + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } }, "node_modules/napi-build-utils": { "version": "1.0.2", @@ -10582,6 +10754,7 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", "dev": true, + "license": "MIT", "dependencies": { "prepend-http": "^2.0.0", "query-string": "^5.0.1", @@ -10591,18 +10764,6 @@ "node": ">=4" } }, - "node_modules/normalize-url/node_modules/sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", - "dev": true, - "dependencies": { - "is-plain-obj": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/now-and-later": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", @@ -10959,6 +11120,7 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -10968,6 +11130,7 @@ "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", "dev": true, + "license": "MIT", "dependencies": { "p-timeout": "^2.0.1" }, @@ -10978,8 +11141,9 @@ "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -10987,8 +11151,9 @@ "node_modules/p-is-promise": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", - "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", + "integrity": "sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -11040,6 +11205,7 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", "dev": true, + "license": "MIT", "dependencies": { "p-finally": "^1.0.0" }, @@ -11089,6 +11255,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -11096,15 +11263,6 @@ "node": ">=6" } }, - "node_modules/parent-module/node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/parse-asn1": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", @@ -11186,6 +11344,16 @@ "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", "dev": true }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -11322,6 +11490,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -11329,8 +11498,9 @@ "node_modules/pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -11338,8 +11508,9 @@ "node_modules/pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, + "license": "MIT", "dependencies": { "pinkie": "^2.0.0" }, @@ -11443,8 +11614,9 @@ "node_modules/prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -11535,10 +11707,11 @@ } }, "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -11563,6 +11736,7 @@ "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", "dev": true, + "license": "MIT", "dependencies": { "decode-uri-component": "^0.2.0", "object-assign": "^4.1.0", @@ -11729,9 +11903,10 @@ } }, "node_modules/reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" }, "node_modules/regenerator-runtime": { "version": "0.13.9", @@ -11757,18 +11932,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -11841,15 +12004,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-in-the-middle": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", @@ -11944,8 +12098,9 @@ "node_modules/responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", "dev": true, + "license": "MIT", "dependencies": { "lowercase-keys": "^1.0.0" } @@ -12116,30 +12271,19 @@ } }, "node_modules/seek-bzip": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", - "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", "dev": true, + "license": "MIT", "dependencies": { - "commander": "~2.8.1" + "commander": "^2.8.1" }, "bin": { "seek-bunzip": "bin/seek-bunzip", "seek-table": "bin/seek-bzip-table" } }, - "node_modules/seek-bzip/node_modules/commander": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", - "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", - "dev": true, - "dependencies": { - "graceful-readlink": ">= 1.0.0" - }, - "engines": { - "node": ">= 0.6.x" - } - }, "node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -12272,12 +12416,13 @@ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, "node_modules/shortid": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", - "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", + "version": "2.2.17", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.17.tgz", + "integrity": "sha512-GpbM3gLF1UUXZvQw6MCyulHkWbRseNO4cyBEZresZRorwl1+SLu1ZdqgVtuwqz8mB6RpwPkm541mYSqrKyJSaA==", "dev": true, + "license": "MIT", "dependencies": { - "nanoid": "^2.1.0" + "nanoid": "^3.3.8" } }, "node_modules/side-channel": { @@ -12451,61 +12596,38 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "is-plain-obj": "^1.0.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=4" } }, - "node_modules/slice-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "sort-keys": "^1.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=0.10.0" } }, - "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/sort-keys": { + "node_modules/sort-keys-length/node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", "dev": true, + "license": "MIT", "dependencies": { "is-plain-obj": "^1.0.0" }, @@ -12513,18 +12635,6 @@ "node": ">=0.10.0" } }, - "node_modules/sort-keys-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", - "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", - "dev": true, - "dependencies": { - "sort-keys": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -12660,8 +12770,9 @@ "node_modules/strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -12824,6 +12935,7 @@ "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", "dev": true, + "license": "MIT", "dependencies": { "is-natural-number": "^4.0.1" } @@ -12854,6 +12966,7 @@ "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.2" }, @@ -12908,44 +13021,6 @@ "semver": "bin/semver.js" } }, - "node_modules/table": { - "version": "6.7.5", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.5.tgz", - "integrity": "sha512-LFNeryOqiQHqCVKzhkymKwt6ozeRhlm8IL1mE8rNUurkir4heF6PzMyRgaTa4tlyPTGGgXuvVOF/OLWiH09Lqw==", - "dev": true, - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/table/node_modules/ajv": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", - "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -13028,6 +13103,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", "dev": true, + "license": "MIT", "dependencies": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", @@ -13151,8 +13227,9 @@ "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" }, "node_modules/through2": { "version": "2.0.5", @@ -13177,8 +13254,9 @@ "node_modules/timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "integrity": "sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -13229,7 +13307,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/to-fast-properties": { "version": "2.0.0", @@ -13276,8 +13355,9 @@ "node_modules/trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.2" }, @@ -13763,10 +13843,11 @@ } }, "node_modules/unbzip2-stream": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", - "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, + "license": "MIT", "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -13902,8 +13983,9 @@ "node_modules/url-parse-lax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", "dev": true, + "license": "MIT", "dependencies": { "prepend-http": "^2.0.0" }, @@ -13914,8 +13996,9 @@ "node_modules/url-to-options": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", - "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", + "integrity": "sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -13955,12 +14038,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz", @@ -15291,15 +15368,6 @@ } } }, - "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, "@babel/compat-data": { "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", @@ -15625,14 +15693,6 @@ "dev": true, "requires": { "eslint-visitor-keys": "^3.3.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true - } } }, "@eslint-community/regexpp": { @@ -15642,45 +15702,54 @@ "dev": true }, "@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "requires": { "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" } }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } }, "minimatch": { "version": "3.1.2", @@ -15691,6 +15760,12 @@ "brace-expansion": "^1.1.7" } }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -15699,6 +15774,12 @@ } } }, + "@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true + }, "@gulpjs/messages": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", @@ -15715,23 +15796,23 @@ } }, "@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" }, "dependencies": { "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "minimatch": { @@ -15742,13 +15823,25 @@ "requires": { "brace-expansion": "^1.1.7" } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true } } }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, "@iarna/toml": { @@ -16173,6 +16266,12 @@ "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", "dev": true }, + "@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, "@sindresorhus/is": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", @@ -16658,16 +16757,14 @@ "requires": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true - } } }, + "@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true + }, "@vscode/extension-telemetry": { "version": "0.8.4", "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.8.4.tgz", @@ -17021,9 +17118,9 @@ "dev": true }, "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==" + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==" }, "acorn-import-assertions": { "version": "1.9.0", @@ -17192,7 +17289,7 @@ "archive-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", - "integrity": "sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=", + "integrity": "sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==", "dev": true, "requires": { "file-type": "^4.2.0" @@ -17201,7 +17298,7 @@ "file-type": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", - "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=", + "integrity": "sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==", "dev": true } } @@ -17392,12 +17489,6 @@ "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", "dev": true }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, "async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", @@ -17796,7 +17887,7 @@ "buffer-fill": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", "dev": true }, "buffer-from": { @@ -17838,10 +17929,16 @@ "responselike": "1.0.2" }, "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true + }, "lowercase-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", - "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", + "integrity": "sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==", "dev": true } } @@ -17871,6 +17968,12 @@ "set-function-length": "^1.2.1" } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -18210,7 +18313,7 @@ "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", "dev": true, "requires": { "mimic-response": "^1.0.0" @@ -18321,12 +18424,20 @@ "dev": true }, "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "continuation-local-storage": { @@ -18636,7 +18747,7 @@ "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true } } @@ -18644,7 +18755,7 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true } } @@ -18652,7 +18763,7 @@ "decompress-response": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", "dev": true, "requires": { "mimic-response": "^1.0.0" @@ -18672,7 +18783,7 @@ "file-type": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", "dev": true } } @@ -18712,7 +18823,7 @@ "file-type": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", "dev": true } } @@ -18720,7 +18831,7 @@ "decompress-unzip": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", "dev": true, "requires": { "file-type": "^3.8.0", @@ -18732,13 +18843,13 @@ "file-type": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", "dev": true }, "get-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", - "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", "dev": true, "requires": { "object-assign": "^4.0.1", @@ -18748,7 +18859,7 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true } } @@ -18938,15 +19049,6 @@ "pify": "^4.0.1" }, "dependencies": { - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -18957,16 +19059,6 @@ "semver": "^5.6.0" } }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -18982,9 +19074,9 @@ "dev": true }, "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", "dev": true }, "duplexify": { @@ -19092,29 +19184,12 @@ }, "enhanced-resolve": { "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - }, - "dependencies": { - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - } + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" } }, "entities": { @@ -19269,51 +19344,49 @@ "dev": true }, "eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", - "dev": true, - "requires": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", + "debug": "^4.3.2", "doctrine": "^3.0.0", - "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "dependencies": { "ansi-styles": { @@ -19326,6 +19399,12 @@ "color-convert": "^2.0.1" } }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", @@ -19386,25 +19465,45 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "requires": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" } }, "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -19416,11 +19515,23 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } }, "minimatch": { "version": "3.1.2", @@ -19431,18 +19542,30 @@ "brace-expansion": "^1.1.7" } }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -19505,9 +19628,9 @@ } }, "eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, "requires": { "debug": "^3.2.7" @@ -19525,27 +19648,29 @@ } }, "eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "requires": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "dependencies": { @@ -19700,44 +19825,27 @@ "estraverse": "^4.1.1" } }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, "espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" - }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - } + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" } }, "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -20018,7 +20126,7 @@ "filename-reserved-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", - "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", "dev": true }, "filenamify": { @@ -20066,14 +20174,6 @@ "requires": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" - }, - "dependencies": { - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } } }, "findup-sync": { @@ -20228,7 +20328,7 @@ "from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", "dev": true, "requires": { "inherits": "^2.0.1", @@ -20321,12 +20421,6 @@ "functions-have-names": "^1.2.3" } }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, "functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -20377,10 +20471,25 @@ "dev": true }, "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + }, + "dependencies": { + "pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } }, "get-symbol-description": { "version": "1.0.2", @@ -20586,10 +20695,16 @@ "url-to-options": "^1.0.1" }, "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true } } @@ -20599,12 +20714,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true - }, "graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -21138,9 +21247,9 @@ "dev": true }, "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "requires": { "parent-module": "^1.0.0", @@ -21228,7 +21337,7 @@ "into-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", - "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "integrity": "sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==", "dev": true, "requires": { "from2": "^2.1.1", @@ -21311,9 +21420,9 @@ "dev": true }, "is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "requires": { "hasown": "^2.0.2" } @@ -21385,7 +21494,7 @@ "is-natural-number": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", "dev": true }, "is-negated-glob": { @@ -21416,9 +21525,9 @@ } }, "is-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", - "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", "dev": true }, "is-path-cwd": { @@ -21436,7 +21545,7 @@ "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true }, "is-plain-object": { @@ -21468,9 +21577,9 @@ } }, "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", "dev": true }, "is-shared-array-buffer": { @@ -21485,7 +21594,7 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true }, "is-string": { @@ -21822,7 +21931,7 @@ "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", "dev": true }, "json-parse-even-better-errors": { @@ -22188,12 +22297,6 @@ "lodash._reinterpolate": "^3.0.0" } }, - "lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", - "dev": true - }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -22679,12 +22782,6 @@ "p-limit": "^3.0.2" } }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -22818,9 +22915,9 @@ "integrity": "sha512-XO0DPujDP9IWpkt690iWLreKztb/VB811DGl5N3z7BfhkMJuiVZXOi6YN/fEB9qkvtMVTgSZDW8pzdVt8vj/FA==" }, "nanoid": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", - "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true }, "napi-build-utils": { @@ -23103,17 +23200,6 @@ "prepend-http": "^2.0.0", "query-string": "^5.0.1", "sort-keys": "^2.0.0" - }, - "dependencies": { - "sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", - "dev": true, - "requires": { - "is-plain-obj": "^1.0.0" - } - } } }, "now-and-later": { @@ -23399,13 +23485,13 @@ "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "dev": true }, "p-is-promise": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", - "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", + "integrity": "sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==", "dev": true }, "p-limit": { @@ -23481,14 +23567,6 @@ "dev": true, "requires": { "callsites": "^3.0.0" - }, - "dependencies": { - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - } } }, "parse-asn1": { @@ -23567,6 +23645,12 @@ "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", "dev": true }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -23674,13 +23758,13 @@ "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "dev": true }, "pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, "requires": { "pinkie": "^2.0.0" @@ -23762,7 +23846,7 @@ "prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", "dev": true }, "prettier": { @@ -23839,9 +23923,9 @@ } }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, "qs": { @@ -23989,9 +24073,9 @@ } }, "reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, "regenerator-runtime": { "version": "0.13.9", @@ -24011,12 +24095,6 @@ "set-function-name": "^2.0.1" } }, - "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", - "dev": true - }, "release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -24071,12 +24149,6 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true - }, "require-in-the-middle": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", @@ -24144,7 +24216,7 @@ "responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", "dev": true, "requires": { "lowercase-keys": "^1.0.0" @@ -24272,23 +24344,12 @@ } }, "seek-bzip": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", - "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", "dev": true, "requires": { - "commander": "~2.8.1" - }, - "dependencies": { - "commander": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", - "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", - "dev": true, - "requires": { - "graceful-readlink": ">= 1.0.0" - } - } + "commander": "^2.8.1" } }, "semver": { @@ -24395,12 +24456,12 @@ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, "shortid": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", - "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", + "version": "2.2.17", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.17.tgz", + "integrity": "sha512-GpbM3gLF1UUXZvQw6MCyulHkWbRseNO4cyBEZresZRorwl1+SLu1ZdqgVtuwqz8mB6RpwPkm541mYSqrKyJSaA==", "dev": true, "requires": { - "nanoid": "^2.1.0" + "nanoid": "^3.3.8" } }, "side-channel": { @@ -24513,47 +24574,10 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } - } - }, "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", "dev": true, "requires": { "is-plain-obj": "^1.0.0" @@ -24562,10 +24586,21 @@ "sort-keys-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", - "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", "dev": true, "requires": { "sort-keys": "^1.0.0" + }, + "dependencies": { + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + } } }, "source-map": { @@ -24685,7 +24720,7 @@ "strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", "dev": true }, "string_decoder": { @@ -24862,39 +24897,6 @@ } } }, - "table": { - "version": "6.7.5", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.5.tgz", - "integrity": "sha512-LFNeryOqiQHqCVKzhkymKwt6ozeRhlm8IL1mE8rNUurkir4heF6PzMyRgaTa4tlyPTGGgXuvVOF/OLWiH09Lqw==", - "dev": true, - "requires": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ajv": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", - "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - } - } - }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -25059,7 +25061,7 @@ "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, "through2": { @@ -25085,7 +25087,7 @@ "timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "integrity": "sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==", "dev": true }, "timers-browserify": { @@ -25160,7 +25162,7 @@ "trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", "dev": true, "requires": { "escape-string-regexp": "^1.0.2" @@ -25518,9 +25520,9 @@ } }, "unbzip2-stream": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", - "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, "requires": { "buffer": "^5.2.1", @@ -25635,7 +25637,7 @@ "url-parse-lax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", "dev": true, "requires": { "prepend-http": "^2.0.0" @@ -25644,7 +25646,7 @@ "url-to-options": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", - "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", + "integrity": "sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==", "dev": true }, "util": { @@ -25675,12 +25677,6 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, "v8-compile-cache-lib": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz", diff --git a/package.json b/package.json index a82ae3b280a7..5c7f91de335a 100644 --- a/package.json +++ b/package.json @@ -1524,7 +1524,7 @@ "minimatch": "^5.0.1", "named-js-regexp": "^1.3.3", "node-stream-zip": "^1.6.0", - "reflect-metadata": "^0.1.12", + "reflect-metadata": "^0.2.2", "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", "semver": "^7.5.2", @@ -1576,9 +1576,9 @@ "cross-spawn": "^6.0.5", "del": "^6.0.0", "download": "^8.0.0", - "eslint": "^7.2.0", + "eslint": "^8.57.1", "eslint-config-prettier": "^8.3.0", - "eslint-plugin-import": "^2.29.1", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-no-only-tests": "^3.3.0", "eslint-plugin-react": "^7.20.3", diff --git a/src/client/common/pipes/namedPipes.ts b/src/client/common/pipes/namedPipes.ts index 8cccd4cdcfed..9bffe78f2b9f 100644 --- a/src/client/common/pipes/namedPipes.ts +++ b/src/client/common/pipes/namedPipes.ts @@ -92,7 +92,7 @@ class CombinedReader implements rpc.MessageReader { private _onPartialMessage = new rpc.Emitter(); - // eslint-disable-next-line @typescript-eslint/no-empty-function + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function private _callback: rpc.DataCallback = () => {}; private _disposables: rpc.Disposable[] = []; diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index 6b49444b3b3d..f47575cad60b 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -386,9 +386,9 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi return undefined; } + // eslint-disable-next-line class-methods-use-this @traceDecoratorError('Failed to parse Environment variables') @traceDecoratorVerbose('parseEnvironmentOutput', TraceOptions.None) - // eslint-disable-next-line class-methods-use-this private parseEnvironmentOutput(output: string, parse: (out: string) => NodeJS.ProcessEnv | undefined) { if (output.indexOf(ENVIRONMENT_PREFIX) === -1) { return parse(output); diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts index 75b34f41176c..5f22a96e4f83 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -75,8 +75,8 @@ export class ActiveState { private static readonly defaultStateCommand: string = 'state'; - @cache(30_000, true, 10_000) // eslint-disable-next-line class-methods-use-this + @cache(30_000, true, 10_000) private async getProjectsCached(): Promise { try { const stateCommand = diff --git a/src/client/pythonEnvironments/common/environmentManagers/condaService.ts b/src/client/pythonEnvironments/common/environmentManagers/condaService.ts index 049e19380d4e..0739993dad37 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/condaService.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/condaService.ts @@ -55,6 +55,7 @@ export class CondaService implements ICondaService { /** * Return the path to the "conda file". */ + // eslint-disable-next-line class-methods-use-this public async getCondaFile(forShellExecution?: boolean): Promise { return Conda.getConda().then((conda) => { @@ -142,8 +143,9 @@ export class CondaService implements ICondaService { * Return the info reported by the conda install. * The result is cached for 30s. */ - @cache(60_000) + // eslint-disable-next-line class-methods-use-this + @cache(60_000) public async getCondaInfo(): Promise { const conda = await Conda.getConda(); return conda?.getInfo(); diff --git a/src/client/pythonEnvironments/common/environmentManagers/pixi.ts b/src/client/pythonEnvironments/common/environmentManagers/pixi.ts index 9ad98d1714fb..6443e64f9ae8 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/pixi.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/pixi.ts @@ -154,8 +154,9 @@ export class Pixi { * * @param envDir The root directory (or prefix) of a conda environment */ - @cache(5_000, true, 10_000) + // eslint-disable-next-line class-methods-use-this + @cache(5_000, true, 10_000) async getPixiEnvironmentMetadata(envDir: string): Promise { const pixiPath = path.join(envDir, 'conda-meta/pixi'); try { From 0c4a30d7ff364bcca83401ddb5d48a4c67a82cfd Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 7 Mar 2025 19:56:16 +0000 Subject: [PATCH 0887/1136] switch to use eslint.config.mjs (#24882) - add eslint.config.mjs (copied over eslintignore into the file as well) - fixed package.json script due to deprecation of `--ext` - few small linting issues arose as a result which I just added eslint ignores by hand --- build/ci/scripts/spec_with_pid.js | 1 + eslint.config.mjs | 390 ++++++++++++++++++++++++++ package.json | 4 +- src/client/common/extensions.ts | 1 + src/client/common/terminal/service.ts | 1 + src/test/common.ts | 1 + 6 files changed, 396 insertions(+), 2 deletions(-) create mode 100644 eslint.config.mjs diff --git a/build/ci/scripts/spec_with_pid.js b/build/ci/scripts/spec_with_pid.js index 9815feaac76a..a8453353aa79 100644 --- a/build/ci/scripts/spec_with_pid.js +++ b/build/ci/scripts/spec_with_pid.js @@ -98,5 +98,6 @@ Spec.description = 'hierarchical & verbose [default]'; * Expose `Spec`. */ +// eslint-disable-next-line no-global-assign exports = Spec; module.exports = exports; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000000..979b26459d06 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,390 @@ +/** + * ESLint Configuration for VS Code Python Extension + * This file configures linting rules for the TypeScript/JavaScript codebase. + * It uses the new flat config format introduced in ESLint 8.21.0 + */ + +// Import essential ESLint plugins and configurations +import tseslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import noOnlyTests from 'eslint-plugin-no-only-tests'; +import prettier from 'eslint-config-prettier'; +import importPlugin from 'eslint-plugin-import'; +import js from '@eslint/js'; + +export default [ + { + ignores: ['**/node_modules/**', '**/out/**'], + }, + // Base configuration for all files + { + ignores: [ + '**/node_modules/**', + '**/out/**', + 'src/test/analysisEngineTest.ts', + 'src/test/ciConstants.ts', + 'src/test/common.ts', + 'src/test/constants.ts', + 'src/test/core.ts', + 'src/test/extension-version.functional.test.ts', + 'src/test/fixtures.ts', + 'src/test/index.ts', + 'src/test/initialize.ts', + 'src/test/mockClasses.ts', + 'src/test/performanceTest.ts', + 'src/test/proc.ts', + 'src/test/smokeTest.ts', + 'src/test/standardTest.ts', + 'src/test/startupTelemetry.unit.test.ts', + 'src/test/testBootstrap.ts', + 'src/test/testLogger.ts', + 'src/test/testRunner.ts', + 'src/test/textUtils.ts', + 'src/test/unittests.ts', + 'src/test/vscode-mock.ts', + 'src/test/interpreters/mocks.ts', + 'src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts', + 'src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts', + 'src/test/interpreters/activation/service.unit.test.ts', + 'src/test/interpreters/helpers.unit.test.ts', + 'src/test/interpreters/display.unit.test.ts', + 'src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts', + 'src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts', + 'src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts', + 'src/test/activation/activeResource.unit.test.ts', + 'src/test/activation/extensionSurvey.unit.test.ts', + 'src/test/utils/fs.ts', + 'src/test/api.functional.test.ts', + 'src/test/testing/common/debugLauncher.unit.test.ts', + 'src/test/testing/common/services/configSettingService.unit.test.ts', + 'src/test/common/exitCIAfterTestReporter.ts', + 'src/test/common/terminals/activator/index.unit.test.ts', + 'src/test/common/terminals/activator/base.unit.test.ts', + 'src/test/common/terminals/shellDetector.unit.test.ts', + 'src/test/common/terminals/service.unit.test.ts', + 'src/test/common/terminals/helper.unit.test.ts', + 'src/test/common/terminals/activation.unit.test.ts', + 'src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts', + 'src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts', + 'src/test/common/socketStream.test.ts', + 'src/test/common/configSettings.test.ts', + 'src/test/common/experiments/telemetry.unit.test.ts', + 'src/test/common/platform/filesystem.unit.test.ts', + 'src/test/common/platform/errors.unit.test.ts', + 'src/test/common/platform/utils.ts', + 'src/test/common/platform/fs-temp.unit.test.ts', + 'src/test/common/platform/fs-temp.functional.test.ts', + 'src/test/common/platform/filesystem.functional.test.ts', + 'src/test/common/platform/filesystem.test.ts', + 'src/test/common/utils/cacheUtils.unit.test.ts', + 'src/test/common/utils/decorators.unit.test.ts', + 'src/test/common/utils/version.unit.test.ts', + 'src/test/common/configSettings/configSettings.unit.test.ts', + 'src/test/common/serviceRegistry.unit.test.ts', + 'src/test/common/extensions.unit.test.ts', + 'src/test/common/variables/envVarsService.unit.test.ts', + 'src/test/common/helpers.test.ts', + 'src/test/common/application/commands/reloadCommand.unit.test.ts', + 'src/test/common/installer/channelManager.unit.test.ts', + 'src/test/common/installer/pipInstaller.unit.test.ts', + 'src/test/common/installer/pipEnvInstaller.unit.test.ts', + 'src/test/common/socketCallbackHandler.test.ts', + 'src/test/common/process/decoder.test.ts', + 'src/test/common/process/processFactory.unit.test.ts', + 'src/test/common/process/pythonToolService.unit.test.ts', + 'src/test/common/process/proc.observable.test.ts', + 'src/test/common/process/logger.unit.test.ts', + 'src/test/common/process/proc.exec.test.ts', + 'src/test/common/process/pythonProcess.unit.test.ts', + 'src/test/common/process/proc.unit.test.ts', + 'src/test/common/interpreterPathService.unit.test.ts', + 'src/test/debugger/extension/adapter/adapter.test.ts', + 'src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts', + 'src/test/debugger/extension/adapter/factory.unit.test.ts', + 'src/test/debugger/extension/adapter/logging.unit.test.ts', + 'src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts', + 'src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts', + 'src/test/debugger/utils.ts', + 'src/test/debugger/envVars.test.ts', + 'src/test/telemetry/index.unit.test.ts', + 'src/test/telemetry/envFileTelemetry.unit.test.ts', + 'src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts', + 'src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts', + 'src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts', + 'src/test/application/diagnostics/checks/envPathVariable.unit.test.ts', + 'src/test/application/diagnostics/applicationDiagnostics.unit.test.ts', + 'src/test/application/diagnostics/promptHandler.unit.test.ts', + 'src/test/application/diagnostics/commands/ignore.unit.test.ts', + 'src/test/performance/load.perf.test.ts', + 'src/client/interpreter/configuration/interpreterSelector/commands/base.ts', + 'src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts', + 'src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts', + 'src/client/interpreter/configuration/services/globalUpdaterService.ts', + 'src/client/interpreter/configuration/services/workspaceUpdaterService.ts', + 'src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts', + 'src/client/interpreter/helpers.ts', + 'src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts', + 'src/client/interpreter/display/index.ts', + 'src/client/extension.ts', + 'src/client/startupTelemetry.ts', + 'src/client/terminals/codeExecution/terminalCodeExecution.ts', + 'src/client/terminals/codeExecution/codeExecutionManager.ts', + 'src/client/terminals/codeExecution/djangoContext.ts', + 'src/client/activation/commands.ts', + 'src/client/activation/progress.ts', + 'src/client/activation/extensionSurvey.ts', + 'src/client/activation/common/analysisOptions.ts', + 'src/client/activation/languageClientMiddleware.ts', + 'src/client/testing/serviceRegistry.ts', + 'src/client/testing/main.ts', + 'src/client/testing/configurationFactory.ts', + 'src/client/testing/common/constants.ts', + 'src/client/testing/common/testUtils.ts', + 'src/client/common/helpers.ts', + 'src/client/common/net/browser.ts', + 'src/client/common/net/socket/socketCallbackHandler.ts', + 'src/client/common/net/socket/socketServer.ts', + 'src/client/common/net/socket/SocketStream.ts', + 'src/client/common/contextKey.ts', + 'src/client/common/experiments/telemetry.ts', + 'src/client/common/platform/serviceRegistry.ts', + 'src/client/common/platform/errors.ts', + 'src/client/common/platform/fs-temp.ts', + 'src/client/common/platform/fs-paths.ts', + 'src/client/common/platform/registry.ts', + 'src/client/common/platform/pathUtils.ts', + 'src/client/common/persistentState.ts', + 'src/client/common/terminal/activator/base.ts', + 'src/client/common/terminal/activator/powershellFailedHandler.ts', + 'src/client/common/terminal/activator/index.ts', + 'src/client/common/terminal/helper.ts', + 'src/client/common/terminal/syncTerminalService.ts', + 'src/client/common/terminal/factory.ts', + 'src/client/common/terminal/commandPrompt.ts', + 'src/client/common/terminal/service.ts', + 'src/client/common/terminal/shellDetector.ts', + 'src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts', + 'src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts', + 'src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts', + 'src/client/common/terminal/shellDetectors/settingsShellDetector.ts', + 'src/client/common/terminal/shellDetectors/baseShellDetector.ts', + 'src/client/common/utils/decorators.ts', + 'src/client/common/utils/enum.ts', + 'src/client/common/utils/platform.ts', + 'src/client/common/utils/stopWatch.ts', + 'src/client/common/utils/random.ts', + 'src/client/common/utils/sysTypes.ts', + 'src/client/common/utils/misc.ts', + 'src/client/common/utils/cacheUtils.ts', + 'src/client/common/utils/workerPool.ts', + 'src/client/common/extensions.ts', + 'src/client/common/variables/serviceRegistry.ts', + 'src/client/common/variables/environment.ts', + 'src/client/common/variables/types.ts', + 'src/client/common/variables/systemVariables.ts', + 'src/client/common/cancellation.ts', + 'src/client/common/interpreterPathService.ts', + 'src/client/common/application/applicationShell.ts', + 'src/client/common/application/languageService.ts', + 'src/client/common/application/clipboard.ts', + 'src/client/common/application/workspace.ts', + 'src/client/common/application/debugSessionTelemetry.ts', + 'src/client/common/application/documentManager.ts', + 'src/client/common/application/debugService.ts', + 'src/client/common/application/commands/reloadCommand.ts', + 'src/client/common/application/terminalManager.ts', + 'src/client/common/application/applicationEnvironment.ts', + 'src/client/common/errors/errorUtils.ts', + 'src/client/common/installer/serviceRegistry.ts', + 'src/client/common/installer/channelManager.ts', + 'src/client/common/installer/moduleInstaller.ts', + 'src/client/common/installer/types.ts', + 'src/client/common/installer/pipEnvInstaller.ts', + 'src/client/common/installer/productService.ts', + 'src/client/common/installer/pipInstaller.ts', + 'src/client/common/installer/productPath.ts', + 'src/client/common/process/currentProcess.ts', + 'src/client/common/process/processFactory.ts', + 'src/client/common/process/serviceRegistry.ts', + 'src/client/common/process/pythonToolService.ts', + 'src/client/common/process/internal/python.ts', + 'src/client/common/process/internal/scripts/testing_tools.ts', + 'src/client/common/process/types.ts', + 'src/client/common/process/logger.ts', + 'src/client/common/process/pythonProcess.ts', + 'src/client/common/process/pythonEnvironment.ts', + 'src/client/common/process/decoder.ts', + 'src/client/debugger/extension/adapter/remoteLaunchers.ts', + 'src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts', + 'src/client/debugger/extension/adapter/factory.ts', + 'src/client/debugger/extension/adapter/activator.ts', + 'src/client/debugger/extension/adapter/logging.ts', + 'src/client/debugger/extension/hooks/eventHandlerDispatcher.ts', + 'src/client/debugger/extension/hooks/childProcessAttachService.ts', + 'src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts', + 'src/client/debugger/extension/attachQuickPick/factory.ts', + 'src/client/debugger/extension/attachQuickPick/psProcessParser.ts', + 'src/client/debugger/extension/attachQuickPick/picker.ts', + 'src/client/application/serviceRegistry.ts', + 'src/client/application/diagnostics/base.ts', + 'src/client/application/diagnostics/applicationDiagnostics.ts', + 'src/client/application/diagnostics/filter.ts', + 'src/client/application/diagnostics/promptHandler.ts', + 'src/client/application/diagnostics/commands/base.ts', + 'src/client/application/diagnostics/commands/ignore.ts', + 'src/client/application/diagnostics/commands/factory.ts', + 'src/client/application/diagnostics/commands/execVSCCommand.ts', + 'src/client/application/diagnostics/commands/launchBrowser.ts', + ], + linterOptions: { + reportUnusedDisableDirectives: 'off', + }, + rules: { + ...js.configs.recommended.rules, + 'no-undef': 'off', + }, + }, + // TypeScript-specific configuration + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', 'src', 'pythonExtensionApi/src'], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + globals: { + ...(js.configs.recommended.languageOptions?.globals || {}), + mocha: true, + require: 'readonly', + process: 'readonly', + exports: 'readonly', + module: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + setTimeout: 'readonly', + setInterval: 'readonly', + clearTimeout: 'readonly', + clearInterval: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + 'no-only-tests': noOnlyTests, + import: importPlugin, + prettier: prettier, + }, + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.ts'], + }, + }, + }, + rules: { + // Base configurations + ...tseslint.configs.recommended.rules, + ...prettier.rules, + + // TypeScript-specific rules + '@typescript-eslint/ban-ts-comment': [ + 'error', + { + 'ts-ignore': 'allow-with-description', + }, + ], + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-empty-interface': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-loss-of-precision': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + varsIgnorePattern: '^_', + argsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-use-before-define': [ + 'error', + { + functions: false, + }, + ], + + // Import rules + 'import/extensions': 'off', + 'import/namespace': 'off', + 'import/no-extraneous-dependencies': 'off', + 'import/no-unresolved': 'off', + 'import/prefer-default-export': 'off', + + // Testing rules + 'no-only-tests/no-only-tests': [ + 'error', + { + block: ['test', 'suite'], + focus: ['only'], + }, + ], + + // Code style rules + 'linebreak-style': 'off', + 'no-bitwise': 'off', + 'no-console': 'off', + 'no-underscore-dangle': 'off', + 'operator-assignment': 'off', + 'func-names': 'off', + + // Error handling and control flow + 'no-empty': ['error', { allowEmptyCatch: true }], + 'no-async-promise-executor': 'off', + 'no-await-in-loop': 'off', + 'no-unreachable': 'off', + 'no-void': 'off', + + // Duplicates and overrides (TypeScript handles these) + 'no-dupe-class-members': 'off', + 'no-redeclare': 'off', + 'no-undef': 'off', + + // Miscellaneous rules + 'no-control-regex': 'off', + 'no-extend-native': 'off', + 'no-inner-declarations': 'off', + 'no-multi-str': 'off', + 'no-param-reassign': 'off', + 'no-prototype-builtins': 'off', + 'no-empty-function': 'off', + 'no-template-curly-in-string': 'off', + 'no-useless-escape': 'off', + 'no-extra-parentheses': 'off', + 'no-extra-paren': 'off', + '@typescript-eslint/no-extra-parens': 'off', + strict: 'off', + + // Restricted syntax + 'no-restricted-syntax': [ + 'error', + { + selector: 'ForInStatement', + message: + 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.', + }, + { + selector: 'LabeledStatement', + message: + 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', + }, + { + selector: 'WithStatement', + message: + '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.', + }, + ], + }, + }, +]; diff --git a/package.json b/package.json index 5c7f91de335a..04b6710a1fa0 100644 --- a/package.json +++ b/package.json @@ -1501,8 +1501,8 @@ "testSmoke": "cross-env INSTALL_JUPYTER_EXTENSION=true \"node ./out/test/smokeTest.js\"", "testInsiders": "cross-env VSC_PYTHON_CI_TEST_VSC_CHANNEL=insiders INSTALL_PYLANCE_EXTENSION=true TEST_FILES_SUFFIX=insiders.test CODE_TESTS_WORKSPACE=src/testMultiRootWkspc/smokeTests \"node ./out/test/standardTest.js\"", "lint-staged": "node gulpfile.js", - "lint": "eslint --ext .ts,.js src build pythonExtensionApi", - "lint-fix": "eslint --fix --ext .ts,.js src build pythonExtensionApi gulpfile.js", + "lint": "eslint src build pythonExtensionApi", + "lint-fix": "eslint --fix src build pythonExtensionApi gulpfile.js", "format-check": "prettier --check 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", "format-fix": "prettier --write 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", "clean": "gulp clean", diff --git a/src/client/common/extensions.ts b/src/client/common/extensions.ts index 033a375b1e56..957ec99a7ce1 100644 --- a/src/client/common/extensions.ts +++ b/src/client/common/extensions.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// eslint-disable-next-line @typescript-eslint/no-unused-vars declare interface String { /** * Appropriately formats a string so it can be used as an argument for a command in a shell. diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index a051d66f015f..e92fbd3d494f 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -94,6 +94,7 @@ export class TerminalService implements ITerminalService, Disposable { this._terminalFirstLaunched = false; const promise = new Promise((resolve) => { const disposable = this.terminalManager.onDidChangeTerminalShellIntegration(() => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define clearTimeout(timer); disposable.dispose(); resolve(true); diff --git a/src/test/common.ts b/src/test/common.ts index 00de8fd3f4a6..b6e352b9a3e8 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -462,6 +462,7 @@ export async function waitForCondition( const timeout = setTimeout(() => { clearTimeout(timeout); + // eslint-disable-next-line @typescript-eslint/no-use-before-define clearTimeout(timer); reject(new Error(errorMessage)); }, timeoutMs); From e71bfd686b98b140cb7d07af94d7f25a94f1d844 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 7 Mar 2025 19:56:33 +0000 Subject: [PATCH 0888/1136] default to XDG_RUNTIME_DIR for mac/linux in temp file for testing comms (#24859) fixes https://github.com/microsoft/vscode-python/issues/24406 --- .../testing/testController/common/utils.ts | 21 +++++++- .../testing/testController/utils.unit.test.ts | 49 +++++++++++++++++-- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index e3b37bf74e40..9923d7ec3e12 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -38,6 +38,22 @@ interface ExecutionResultMessage extends Message { params: ExecutionTestPayload; } +/** + * Retrieves the path to the temporary directory. + * + * On Windows, it returns the default temporary directory. + * On macOS/Linux, it prefers the `XDG_RUNTIME_DIR` environment variable if set, + * otherwise, it falls back to the default temporary directory. + * + * @returns {string} The path to the temporary directory. + */ +function getTempDir(): string { + if (process.platform === 'win32') { + return os.tmpdir(); // Default Windows behavior + } + return process.env.XDG_RUNTIME_DIR || os.tmpdir(); // Prefer XDG_RUNTIME_DIR on macOS/Linux +} + /** * Writes an array of test IDs to a temporary file. * @@ -50,11 +66,12 @@ export async function writeTestIdsFile(testIds: string[]): Promise { const tempName = `test-ids-${randomSuffix}.txt`; // create temp file let tempFileName: string; + const tempDir: string = getTempDir(); try { traceLog('Attempting to use temp directory for test ids file, file name:', tempName); - tempFileName = path.join(os.tmpdir(), tempName); + tempFileName = path.join(tempDir, tempName); // attempt access to written file to check permissions - await fs.promises.access(os.tmpdir()); + await fs.promises.access(tempDir); } catch (error) { // Handle the error when accessing the temp directory traceError('Error accessing temp directory:', error, ' Attempt to use extension root dir instead'); diff --git a/src/test/testing/testController/utils.unit.test.ts b/src/test/testing/testController/utils.unit.test.ts index ff1a0c707678..4d2af9da3d5a 100644 --- a/src/test/testing/testController/utils.unit.test.ts +++ b/src/test/testing/testController/utils.unit.test.ts @@ -2,7 +2,6 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import * as fs from 'fs'; import * as path from 'path'; -import * as os from 'os'; import { writeTestIdsFile } from '../../../client/testing/testController/common/utils'; import { EXTENSION_ROOT_DIR } from '../../../client/constants'; @@ -21,11 +20,13 @@ suite('writeTestIdsFile tests', () => { const testIds = ['test1', 'test2', 'test3']; const writeFileStub = sandbox.stub(fs.promises, 'writeFile').resolves(); - const result = await writeTestIdsFile(testIds); - - const tmpDir = os.tmpdir(); + // Set up XDG_RUNTIME_DIR + process.env = { + ...process.env, + XDG_RUNTIME_DIR: '/xdg/runtime/dir', + }; - assert.ok(result.startsWith(tmpDir)); + await writeTestIdsFile(testIds); assert.ok(writeFileStub.calledOnceWith(sinon.match.string, testIds.join('\n'))); }); @@ -48,3 +49,41 @@ suite('writeTestIdsFile tests', () => { assert.ok(writeFileStub.calledOnceWith(sinon.match.string, testIds.join('\n'))); }); }); + +suite('getTempDir tests', () => { + let sandbox: sinon.SinonSandbox; + let originalPlatform: NodeJS.Platform; + let originalEnv: NodeJS.ProcessEnv; + + setup(() => { + sandbox = sinon.createSandbox(); + originalPlatform = process.platform; + originalEnv = process.env; + }); + + teardown(() => { + sandbox.restore(); + Object.defineProperty(process, 'platform', { value: originalPlatform }); + process.env = originalEnv; + }); + + test('should use XDG_RUNTIME_DIR on non-Windows if available', async () => { + if (process.platform === 'win32') { + return; + } + // Force platform to be Linux + Object.defineProperty(process, 'platform', { value: 'linux' }); + + // Set up XDG_RUNTIME_DIR + process.env = { ...process.env, XDG_RUNTIME_DIR: '/xdg/runtime/dir' }; + + const testIds = ['test1', 'test2', 'test3']; + sandbox.stub(fs.promises, 'access').resolves(); + sandbox.stub(fs.promises, 'writeFile').resolves(); + + // This will use getTempDir internally + const result = await writeTestIdsFile(testIds); + + assert.ok(result.startsWith('/xdg/runtime/dir')); + }); +}); From 067e8efe7b1068f575c87d7c530c71c60bb90b8c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 7 Mar 2025 22:08:48 +0000 Subject: [PATCH 0889/1136] add no bad gdpr comments plugin (#24884) create plugin so GPDR comments that are incorrect error on linting. --- .eslintplugin/no-bad-gdpr-comment.js | 51 ++++++++++++++++++++++++++ .eslintplugin/no-bad-gdpr-comment.ts | 55 ++++++++++++++++++++++++++++ eslint.config.mjs | 3 ++ 3 files changed, 109 insertions(+) create mode 100644 .eslintplugin/no-bad-gdpr-comment.js create mode 100644 .eslintplugin/no-bad-gdpr-comment.ts diff --git a/.eslintplugin/no-bad-gdpr-comment.js b/.eslintplugin/no-bad-gdpr-comment.js new file mode 100644 index 000000000000..786259683ff6 --- /dev/null +++ b/.eslintplugin/no-bad-gdpr-comment.js @@ -0,0 +1,51 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +var noBadGDPRComment = { + create: function (context) { + var _a; + return _a = {}, + _a['Program'] = function (node) { + for (var _i = 0, _a = node.comments; _i < _a.length; _i++) { + var comment = _a[_i]; + if (comment.type !== 'Block' || !comment.loc) { + continue; + } + if (!comment.value.includes('__GDPR__')) { + continue; + } + var dataStart = comment.value.indexOf('\n'); + var data = comment.value.substring(dataStart); + var gdprData = void 0; + try { + var jsonRaw = "{ ".concat(data, " }"); + gdprData = JSON.parse(jsonRaw); + } + catch (e) { + context.report({ + loc: { start: comment.loc.start, end: comment.loc.end }, + message: 'GDPR comment is not valid JSON', + }); + } + if (gdprData) { + var len = Object.keys(gdprData).length; + if (len !== 1) { + context.report({ + loc: { start: comment.loc.start, end: comment.loc.end }, + message: "GDPR comment must contain exactly one key, not ".concat(Object.keys(gdprData).join(', ')), + }); + } + } + } + }, + _a; + }, +}; +module.exports = { + rules: { + 'no-bad-gdpr-comment': noBadGDPRComment, // Ensure correct structure + }, +}; diff --git a/.eslintplugin/no-bad-gdpr-comment.ts b/.eslintplugin/no-bad-gdpr-comment.ts new file mode 100644 index 000000000000..1eba899a7de3 --- /dev/null +++ b/.eslintplugin/no-bad-gdpr-comment.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +const noBadGDPRComment: eslint.Rule.RuleModule = { + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + return { + ['Program'](node) { + for (const comment of (node as eslint.AST.Program).comments) { + if (comment.type !== 'Block' || !comment.loc) { + continue; + } + if (!comment.value.includes('__GDPR__')) { + continue; + } + + const dataStart = comment.value.indexOf('\n'); + const data = comment.value.substring(dataStart); + + let gdprData: { [key: string]: object } | undefined; + + try { + const jsonRaw = `{ ${data} }`; + gdprData = JSON.parse(jsonRaw); + } catch (e) { + context.report({ + loc: { start: comment.loc.start, end: comment.loc.end }, + message: 'GDPR comment is not valid JSON', + }); + } + + if (gdprData) { + const len = Object.keys(gdprData).length; + if (len !== 1) { + context.report({ + loc: { start: comment.loc.start, end: comment.loc.end }, + message: `GDPR comment must contain exactly one key, not ${Object.keys(gdprData).join( + ', ', + )}`, + }); + } + } + } + }, + }; + }, +}; + +module.exports = { + rules: { + 'no-bad-gdpr-comment': noBadGDPRComment, // Ensure correct structure + }, +}; diff --git a/eslint.config.mjs b/eslint.config.mjs index 979b26459d06..8e1aa990a2c2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,6 +11,7 @@ import noOnlyTests from 'eslint-plugin-no-only-tests'; import prettier from 'eslint-config-prettier'; import importPlugin from 'eslint-plugin-import'; import js from '@eslint/js'; +import noBadGdprCommentPlugin from './.eslintplugin/no-bad-gdpr-comment.js'; // Ensure the path is correct export default [ { @@ -273,6 +274,7 @@ export default [ 'no-only-tests': noOnlyTests, import: importPlugin, prettier: prettier, + 'no-bad-gdpr-comment': noBadGdprCommentPlugin, // Register your plugin }, settings: { 'import/resolver': { @@ -282,6 +284,7 @@ export default [ }, }, rules: { + 'no-bad-gdpr-comment/no-bad-gdpr-comment': 'warn', // Enable your rule // Base configurations ...tseslint.configs.recommended.rules, ...prettier.rules, From f12d5bc5930646577cd2b181049821196776f002 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 7 Mar 2025 22:09:05 +0000 Subject: [PATCH 0890/1136] remove old .eslintrc and ignore file (#24883) - delete .eslintrc and .eslintignore files - the new config file (eslint.config.mjs) now handles both of these functionalities. --- .eslintignore | 254 -------------------------------------------------- .eslintrc | 107 --------------------- 2 files changed, 361 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index a3a6e01b0ad6..000000000000 --- a/.eslintignore +++ /dev/null @@ -1,254 +0,0 @@ -pythonExtensionApi/out/ - -# The following files were grandfathered out of eslint. They can be removed as time permits. - -src/test/analysisEngineTest.ts -src/test/ciConstants.ts -src/test/common.ts -src/test/constants.ts -src/test/core.ts -src/test/extension-version.functional.test.ts -src/test/fixtures.ts -src/test/index.ts -src/test/initialize.ts -src/test/mockClasses.ts -src/test/performanceTest.ts -src/test/proc.ts -src/test/smokeTest.ts -src/test/standardTest.ts -src/test/startupTelemetry.unit.test.ts -src/test/testBootstrap.ts -src/test/testLogger.ts -src/test/testRunner.ts -src/test/textUtils.ts -src/test/unittests.ts -src/test/vscode-mock.ts - -src/test/interpreters/mocks.ts -src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts -src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts -src/test/interpreters/activation/service.unit.test.ts -src/test/interpreters/helpers.unit.test.ts -src/test/interpreters/display.unit.test.ts - -src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts -src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts -src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts - -src/test/activation/activeResource.unit.test.ts -src/test/activation/extensionSurvey.unit.test.ts - -src/test/utils/fs.ts - -src/test/api.functional.test.ts - -src/test/testing/common/debugLauncher.unit.test.ts -src/test/testing/common/services/configSettingService.unit.test.ts - -src/test/common/exitCIAfterTestReporter.ts - - -src/test/common/terminals/activator/index.unit.test.ts -src/test/common/terminals/activator/base.unit.test.ts -src/test/common/terminals/shellDetector.unit.test.ts -src/test/common/terminals/service.unit.test.ts -src/test/common/terminals/helper.unit.test.ts -src/test/common/terminals/activation.unit.test.ts -src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts -src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts - -src/test/common/socketStream.test.ts - -src/test/common/configSettings.test.ts - -src/test/common/experiments/telemetry.unit.test.ts - -src/test/common/platform/filesystem.unit.test.ts -src/test/common/platform/errors.unit.test.ts -src/test/common/platform/utils.ts -src/test/common/platform/fs-temp.unit.test.ts -src/test/common/platform/fs-temp.functional.test.ts -src/test/common/platform/filesystem.functional.test.ts -src/test/common/platform/filesystem.test.ts - -src/test/common/utils/cacheUtils.unit.test.ts -src/test/common/utils/decorators.unit.test.ts -src/test/common/utils/version.unit.test.ts - -src/test/common/configSettings/configSettings.unit.test.ts -src/test/common/serviceRegistry.unit.test.ts -src/test/common/extensions.unit.test.ts -src/test/common/variables/envVarsService.unit.test.ts -src/test/common/helpers.test.ts -src/test/common/application/commands/reloadCommand.unit.test.ts - -src/test/common/installer/channelManager.unit.test.ts -src/test/common/installer/pipInstaller.unit.test.ts -src/test/common/installer/pipEnvInstaller.unit.test.ts - -src/test/common/socketCallbackHandler.test.ts - -src/test/common/process/decoder.test.ts -src/test/common/process/processFactory.unit.test.ts -src/test/common/process/pythonToolService.unit.test.ts -src/test/common/process/proc.observable.test.ts -src/test/common/process/logger.unit.test.ts -src/test/common/process/proc.exec.test.ts -src/test/common/process/pythonProcess.unit.test.ts -src/test/common/process/proc.unit.test.ts - -src/test/common/interpreterPathService.unit.test.ts - - - -src/test/debugger/extension/adapter/adapter.test.ts -src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts -src/test/debugger/extension/adapter/factory.unit.test.ts -src/test/debugger/extension/adapter/logging.unit.test.ts -src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts -src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts -src/test/debugger/utils.ts -src/test/debugger/envVars.test.ts - -src/test/telemetry/index.unit.test.ts -src/test/telemetry/envFileTelemetry.unit.test.ts - -src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts -src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts -src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts -src/test/application/diagnostics/checks/envPathVariable.unit.test.ts -src/test/application/diagnostics/applicationDiagnostics.unit.test.ts -src/test/application/diagnostics/promptHandler.unit.test.ts -src/test/application/diagnostics/commands/ignore.unit.test.ts - -src/test/performance/load.perf.test.ts - -src/client/interpreter/configuration/interpreterSelector/commands/base.ts -src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts -src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts -src/client/interpreter/configuration/services/globalUpdaterService.ts -src/client/interpreter/configuration/services/workspaceUpdaterService.ts -src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts -src/client/interpreter/helpers.ts -src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts -src/client/interpreter/display/index.ts - -src/client/extension.ts -src/client/startupTelemetry.ts - -src/client/terminals/codeExecution/terminalCodeExecution.ts -src/client/terminals/codeExecution/codeExecutionManager.ts -src/client/terminals/codeExecution/djangoContext.ts - -src/client/activation/commands.ts -src/client/activation/progress.ts -src/client/activation/extensionSurvey.ts -src/client/activation/common/analysisOptions.ts -src/client/activation/languageClientMiddleware.ts - - -src/client/testing/serviceRegistry.ts -src/client/testing/main.ts -src/client/testing/configurationFactory.ts -src/client/testing/common/constants.ts -src/client/testing/common/testUtils.ts - -src/client/common/helpers.ts -src/client/common/net/browser.ts -src/client/common/net/socket/socketCallbackHandler.ts -src/client/common/net/socket/socketServer.ts -src/client/common/net/socket/SocketStream.ts -src/client/common/contextKey.ts -src/client/common/experiments/telemetry.ts -src/client/common/platform/serviceRegistry.ts -src/client/common/platform/errors.ts -src/client/common/platform/fs-temp.ts -src/client/common/platform/fs-paths.ts -src/client/common/platform/registry.ts -src/client/common/platform/pathUtils.ts -src/client/common/persistentState.ts -src/client/common/terminal/activator/base.ts -src/client/common/terminal/activator/powershellFailedHandler.ts -src/client/common/terminal/activator/index.ts -src/client/common/terminal/helper.ts -src/client/common/terminal/syncTerminalService.ts -src/client/common/terminal/factory.ts -src/client/common/terminal/commandPrompt.ts -src/client/common/terminal/service.ts -src/client/common/terminal/shellDetector.ts -src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts -src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts -src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts -src/client/common/terminal/shellDetectors/settingsShellDetector.ts -src/client/common/terminal/shellDetectors/baseShellDetector.ts -src/client/common/utils/decorators.ts -src/client/common/utils/enum.ts -src/client/common/utils/platform.ts -src/client/common/utils/stopWatch.ts -src/client/common/utils/random.ts -src/client/common/utils/sysTypes.ts -src/client/common/utils/misc.ts -src/client/common/utils/cacheUtils.ts -src/client/common/utils/workerPool.ts -src/client/common/extensions.ts -src/client/common/variables/serviceRegistry.ts -src/client/common/variables/environment.ts -src/client/common/variables/types.ts -src/client/common/variables/systemVariables.ts -src/client/common/cancellation.ts -src/client/common/interpreterPathService.ts -src/client/common/application/applicationShell.ts -src/client/common/application/languageService.ts -src/client/common/application/clipboard.ts -src/client/common/application/workspace.ts -src/client/common/application/debugSessionTelemetry.ts -src/client/common/application/documentManager.ts -src/client/common/application/debugService.ts -src/client/common/application/commands/reloadCommand.ts -src/client/common/application/terminalManager.ts -src/client/common/application/applicationEnvironment.ts -src/client/common/errors/errorUtils.ts -src/client/common/installer/serviceRegistry.ts -src/client/common/installer/channelManager.ts -src/client/common/installer/moduleInstaller.ts -src/client/common/installer/types.ts -src/client/common/installer/pipEnvInstaller.ts -src/client/common/installer/productService.ts -src/client/common/installer/pipInstaller.ts -src/client/common/installer/productPath.ts -src/client/common/process/currentProcess.ts -src/client/common/process/processFactory.ts -src/client/common/process/serviceRegistry.ts -src/client/common/process/pythonToolService.ts -src/client/common/process/internal/python.ts -src/client/common/process/internal/scripts/testing_tools.ts -src/client/common/process/types.ts -src/client/common/process/logger.ts -src/client/common/process/pythonProcess.ts -src/client/common/process/pythonEnvironment.ts -src/client/common/process/decoder.ts - - -src/client/debugger/extension/adapter/remoteLaunchers.ts -src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts -src/client/debugger/extension/adapter/factory.ts -src/client/debugger/extension/adapter/activator.ts -src/client/debugger/extension/adapter/logging.ts -src/client/debugger/extension/hooks/eventHandlerDispatcher.ts -src/client/debugger/extension/hooks/childProcessAttachService.ts -src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts -src/client/debugger/extension/attachQuickPick/factory.ts -src/client/debugger/extension/attachQuickPick/psProcessParser.ts -src/client/debugger/extension/attachQuickPick/picker.ts - -src/client/application/serviceRegistry.ts -src/client/application/diagnostics/base.ts -src/client/application/diagnostics/applicationDiagnostics.ts -src/client/application/diagnostics/filter.ts -src/client/application/diagnostics/promptHandler.ts -src/client/application/diagnostics/commands/base.ts -src/client/application/diagnostics/commands/ignore.ts -src/client/application/diagnostics/commands/factory.ts -src/client/application/diagnostics/commands/execVSCCommand.ts -src/client/application/diagnostics/commands/launchBrowser.ts - diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 3535797eb92c..000000000000 --- a/.eslintrc +++ /dev/null @@ -1,107 +0,0 @@ -{ - "env": { - "node": true, - "es6": true, - "mocha": true - }, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint", - "no-only-tests" - ], - "extends": [ - "plugin:@typescript-eslint/recommended", - "plugin:import/errors", - "plugin:import/warnings", - "plugin:import/typescript", - "prettier" - ], - "rules": { - // Overriding ESLint rules with Typescript-specific ones - "@typescript-eslint/ban-ts-comment": [ - "error", - { - "ts-ignore": "allow-with-description" - } - ], - "@typescript-eslint/explicit-module-boundary-types": "error", - "no-bitwise": "off", - "no-dupe-class-members": "off", - "@typescript-eslint/no-dupe-class-members": "error", - "no-empty-function": "off", - "@typescript-eslint/no-empty-function": ["error"], - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/no-non-null-assertion": "off", - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - "args": "after-used", - "argsIgnorePattern": "^_" - } - ], - "no-use-before-define": "off", - "@typescript-eslint/no-use-before-define": [ - "error", - { - "functions": false - } - ], - "no-useless-constructor": "off", - "@typescript-eslint/no-useless-constructor": "error", - "@typescript-eslint/no-var-requires": "off", - - // Other rules - "class-methods-use-this": ["error", { "exceptMethods": ["dispose"] }], - "func-names": "off", - "import/extensions": "off", - "import/namespace": "off", - "import/no-extraneous-dependencies": "off", - "import/no-unresolved": [ - "error", - { - "ignore": ["monaco-editor", "vscode"] - } - ], - "import/prefer-default-export": "off", - "linebreak-style": "off", - "no-await-in-loop": "off", - "no-console": "off", - "no-control-regex": "off", - "no-extend-native": "off", - "no-multi-str": "off", - "no-shadow": "off", - "no-param-reassign": "off", - "no-prototype-builtins": "off", - "no-restricted-syntax": [ - "error", - { - "selector": "ForInStatement", - "message": "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array." - }, - { - "selector": "LabeledStatement", - "message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand." - }, - { - "selector": "WithStatement", - "message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize." - } - ], - "no-template-curly-in-string": "off", - "no-underscore-dangle": "off", - "no-useless-escape": "off", - "no-void": [ - "error", - { - "allowAsStatement": true - } - ], - "operator-assignment": "off", - "strict": "off", - "no-only-tests/no-only-tests": ["error", { "block": ["test", "suite"], "focus": ["only"] }], - "prefer-const": "off", - "import/no-named-as-default-member": "off" - } -} From 6b7d8d1c12189126dce4c5d6733a74ab7762fb00 Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Fri, 14 Mar 2025 09:58:49 -0700 Subject: [PATCH 0891/1136] Ensure survey notification respects telemetry.disableFeedback setting (#24903) Fixes https://github.com/microsoft/vscode-python/issues/24904 Follow up to https://github.com/microsoft/vscode/pull/243276 This is to ensure that we only show the survey feedback if VS Code's `telemetry.disableFeedback` setting isn't enabled --- src/client/activation/extensionSurvey.ts | 15 +++++++- .../activation/extensionSurvey.unit.test.ts | 34 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/client/activation/extensionSurvey.ts b/src/client/activation/extensionSurvey.ts index c5b7c525fea8..e8df4bb850da 100644 --- a/src/client/activation/extensionSurvey.ts +++ b/src/client/activation/extensionSurvey.ts @@ -6,7 +6,7 @@ import { inject, injectable } from 'inversify'; import * as querystring from 'querystring'; import { env, UIKind } from 'vscode'; -import { IApplicationEnvironment, IApplicationShell } from '../common/application/types'; +import { IApplicationEnvironment, IApplicationShell, IWorkspaceService } from '../common/application/types'; import { ShowExtensionSurveyPrompt } from '../common/experiments/groups'; import '../common/extensions'; import { IPlatformService } from '../common/platform/types'; @@ -37,6 +37,7 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService @inject(IExperimentService) private experiments: IExperimentService, @inject(IApplicationEnvironment) private appEnvironment: IApplicationEnvironment, @inject(IPlatformService) private platformService: IPlatformService, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, private sampleSizePerOneHundredUsers: number = 10, private waitTimeToShowSurvey: number = WAIT_TIME_TO_SHOW_SURVEY, ) {} @@ -57,6 +58,18 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService if (env.uiKind === UIKind?.Web) { return false; } + + let feedbackDisabled = false; + + const telemetryConfig = this.workspace.getConfiguration('telemetry'); + if (telemetryConfig) { + feedbackDisabled = telemetryConfig.get('disableFeedback', false); + } + + if (feedbackDisabled) { + return false; + } + const doNotShowSurveyAgain = this.persistentState.createGlobalPersistentState( extensionSurveyStateKeys.doNotShowAgain, false, diff --git a/src/test/activation/extensionSurvey.unit.test.ts b/src/test/activation/extensionSurvey.unit.test.ts index ba96b917aff3..8e191ec82810 100644 --- a/src/test/activation/extensionSurvey.unit.test.ts +++ b/src/test/activation/extensionSurvey.unit.test.ts @@ -8,7 +8,7 @@ import * as sinon from 'sinon'; import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { ExtensionSurveyPrompt, extensionSurveyStateKeys } from '../../client/activation/extensionSurvey'; -import { IApplicationEnvironment, IApplicationShell } from '../../client/common/application/types'; +import { IApplicationEnvironment, IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; import { ShowExtensionSurveyPrompt } from '../../client/common/experiments/groups'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { IPlatformService } from '../../client/common/platform/types'; @@ -23,6 +23,7 @@ import { createDeferred } from '../../client/common/utils/async'; import { Common, ExtensionSurveyBanner } from '../../client/common/utils/localize'; import { OSType } from '../../client/common/utils/platform'; import { sleep } from '../core'; +import { WorkspaceConfiguration } from 'vscode'; suite('Extension survey prompt - shouldShowBanner()', () => { let appShell: TypeMoq.IMock; @@ -35,6 +36,8 @@ suite('Extension survey prompt - shouldShowBanner()', () => { let disableSurveyForTime: TypeMoq.IMock>; let doNotShowAgain: TypeMoq.IMock>; let extensionSurveyPrompt: ExtensionSurveyPrompt; + let workspaceService: TypeMoq.IMock; + setup(() => { experiments = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); @@ -45,6 +48,7 @@ suite('Extension survey prompt - shouldShowBanner()', () => { doNotShowAgain = TypeMoq.Mock.ofType>(); platformService = TypeMoq.Mock.ofType(); appEnvironment = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); when( persistentStateFactory.createGlobalPersistentState( extensionSurveyStateKeys.disableSurveyForTime, @@ -63,6 +67,7 @@ suite('Extension survey prompt - shouldShowBanner()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 10, ); }); @@ -122,6 +127,23 @@ suite('Extension survey prompt - shouldShowBanner()', () => { } random.verifyAll(); }); + test('Returns false if telemetry.disableFeedback is enabled', async () => { + disableSurveyForTime.setup((d) => d.value).returns(() => false); + doNotShowAgain.setup((d) => d.value).returns(() => false); + + const telemetryConfig = TypeMoq.Mock.ofType(); + workspaceService.setup((w) => w.getConfiguration('telemetry')).returns(() => telemetryConfig.object); + telemetryConfig + .setup((t) => t.get(TypeMoq.It.isValue('disableFeedback'), TypeMoq.It.isValue(false))) + .returns(() => true); + + const result = extensionSurveyPrompt.shouldShowBanner(); + + expect(result).to.equal(false, 'Banner should not be shown when telemetry.disableFeedback is true'); + workspaceService.verify((w) => w.getConfiguration('telemetry'), TypeMoq.Times.once()); + telemetryConfig.verify((t) => t.get('disableFeedback', false), TypeMoq.Times.once()); + }); + test('Returns true if user is in the random sampling', async () => { disableSurveyForTime.setup((d) => d.value).returns(() => false); doNotShowAgain.setup((d) => d.value).returns(() => false); @@ -142,6 +164,7 @@ suite('Extension survey prompt - shouldShowBanner()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 100, ); disableSurveyForTime.setup((d) => d.value).returns(() => false); @@ -162,6 +185,7 @@ suite('Extension survey prompt - shouldShowBanner()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 0, ); disableSurveyForTime.setup((d) => d.value).returns(() => false); @@ -186,6 +210,7 @@ suite('Extension survey prompt - showSurvey()', () => { let platformService: TypeMoq.IMock; let appEnvironment: TypeMoq.IMock; let extensionSurveyPrompt: ExtensionSurveyPrompt; + let workspaceService: TypeMoq.IMock; setup(() => { appShell = TypeMoq.Mock.ofType(); browserService = TypeMoq.Mock.ofType(); @@ -195,6 +220,7 @@ suite('Extension survey prompt - showSurvey()', () => { doNotShowAgain = TypeMoq.Mock.ofType>(); platformService = TypeMoq.Mock.ofType(); appEnvironment = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); when( persistentStateFactory.createGlobalPersistentState( extensionSurveyStateKeys.disableSurveyForTime, @@ -214,6 +240,7 @@ suite('Extension survey prompt - showSurvey()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 10, ); }); @@ -406,6 +433,7 @@ suite('Extension survey prompt - activate()', () => { let extensionSurveyPrompt: ExtensionSurveyPrompt; let platformService: TypeMoq.IMock; let appEnvironment: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; setup(() => { appShell = TypeMoq.Mock.ofType(); browserService = TypeMoq.Mock.ofType(); @@ -414,6 +442,7 @@ suite('Extension survey prompt - activate()', () => { experiments = TypeMoq.Mock.ofType(); platformService = TypeMoq.Mock.ofType(); appEnvironment = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); }); teardown(() => { @@ -431,6 +460,7 @@ suite('Extension survey prompt - activate()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 10, ); experiments @@ -460,6 +490,7 @@ suite('Extension survey prompt - activate()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 10, 50, ); @@ -494,6 +525,7 @@ suite('Extension survey prompt - activate()', () => { experiments.object, appEnvironment.object, platformService.object, + workspaceService.object, 10, 50, ); From 6a60c92b8eef9a4681497ff674ac4f1a70a2e376 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 19 Mar 2025 20:12:50 +0000 Subject: [PATCH 0892/1136] move clear envCollection to after await (#24921) fixes https://github.com/microsoft/vscode-python/issues/24914 --- src/client/terminals/envCollectionActivation/service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts index 43b8ceeb8e06..880053b03d1d 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -221,9 +221,10 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ env.PS1 = await this.getPS1(shell, resource, env); const defaultPrependOptions = await this.getPrependOptions(); + const deactivate = await this.terminalDeactivateService.getScriptLocation(shell, resource); // Clear any previously set env vars from collection envVarCollection.clear(); - const deactivate = await this.terminalDeactivateService.getScriptLocation(shell, resource); + Object.keys(env).forEach((key) => { if (shouldSkip(key)) { return; From c51cdd3006011c6137b67d31f07bcdee3b928795 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 24 Mar 2025 08:35:44 -0700 Subject: [PATCH 0893/1136] fix: use vsceTarget to rustTarget conversion when pulling `pet` (#24925) --- build/azure-pipeline.pre-release.yml | 28 +++++++++++++++++++++++++++- build/azure-pipeline.stable.yml | 28 +++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index 8a631394a7fb..3236b43d0098 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -102,6 +102,32 @@ extends: chmod +x $(Build.SourcesDirectory)/python-env-tools/bin displayName: Make Directory for python-env-tool binary + - bash: | + if [ "$(vsceTarget)" == "win32-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "win32-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "linux-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "linux-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "linux-armhf" ]; then + echo "##vso[task.setvariable variable=buildTarget]armv7-unknown-linux-gnueabihf" + elif [ "$(vsceTarget)" == "darwin-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-apple-darwin" + elif [ "$(vsceTarget)" == "darwin-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-apple-darwin" + elif [ "$(vsceTarget)" == "alpine-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "alpine-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "web" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + else + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + fi + displayName: Set buildTarget variable + - task: DownloadPipelineArtifact@2 inputs: buildType: 'specific' @@ -110,7 +136,7 @@ extends: buildVersionToDownload: 'latest' branchName: 'refs/heads/main' targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' - artifactName: 'bin-$(vsceTarget)' + artifactName: 'bin-$(buildTarget)' itemPattern: | pet.exe pet diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index a5276bbb09d2..2e7ebcfea82a 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -96,6 +96,32 @@ extends: chmod +x $(Build.SourcesDirectory)/python-env-tools/bin displayName: Make Directory for python-env-tool binary + - bash: | + if [ "$(vsceTarget)" == "win32-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "win32-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "linux-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "linux-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "linux-armhf" ]; then + echo "##vso[task.setvariable variable=buildTarget]armv7-unknown-linux-gnueabihf" + elif [ "$(vsceTarget)" == "darwin-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-apple-darwin" + elif [ "$(vsceTarget)" == "darwin-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-apple-darwin" + elif [ "$(vsceTarget)" == "alpine-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "alpine-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "web" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + else + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + fi + displayName: Set buildTarget variable + - task: DownloadPipelineArtifact@2 inputs: buildType: 'specific' @@ -104,7 +130,7 @@ extends: buildVersionToDownload: 'latestFromBranch' branchName: 'refs/heads/release/2025.2' targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' - artifactName: 'bin-$(vsceTarget)' + artifactName: 'bin-$(buildTarget)' itemPattern: | pet.exe pet From 725d5395664f7130bd81ccdbce925f2944a713de Mon Sep 17 00:00:00 2001 From: Alessandro Sclafani Date: Mon, 24 Mar 2025 18:15:39 +0100 Subject: [PATCH 0894/1136] Update condarc.json (#24918) Hello! [According to the docs](https://docs.conda.io/projects/conda/en/stable/user-guide/configuration/settings.html#ssl-verify-ssl-verification), the ssl_verify option should support strings (for certificate paths and `truststore`) --- schemas/condarc.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/schemas/condarc.json b/schemas/condarc.json index 396236238c1a..a881315d3137 100644 --- a/schemas/condarc.json +++ b/schemas/condarc.json @@ -59,7 +59,14 @@ } }, "ssl_verify": { - "type": "boolean" + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] }, "offline": { "type": "boolean" From e0cbc9a577728fcb9c9e4fbba3abc1824ad42f3d Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Tue, 25 Mar 2025 18:06:57 -0700 Subject: [PATCH 0895/1136] Use new feedback setting (#24929) Due to https://github.com/microsoft/vscode/pull/244677 --- src/client/activation/extensionSurvey.ts | 6 ++--- .../activation/extensionSurvey.unit.test.ts | 25 ++++++++++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/client/activation/extensionSurvey.ts b/src/client/activation/extensionSurvey.ts index e8df4bb850da..d32ba7180c0f 100644 --- a/src/client/activation/extensionSurvey.ts +++ b/src/client/activation/extensionSurvey.ts @@ -59,14 +59,14 @@ export class ExtensionSurveyPrompt implements IExtensionSingleActivationService return false; } - let feedbackDisabled = false; + let feedbackEnabled = true; const telemetryConfig = this.workspace.getConfiguration('telemetry'); if (telemetryConfig) { - feedbackDisabled = telemetryConfig.get('disableFeedback', false); + feedbackEnabled = telemetryConfig.get('feedback.enabled', true); } - if (feedbackDisabled) { + if (!feedbackEnabled) { return false; } diff --git a/src/test/activation/extensionSurvey.unit.test.ts b/src/test/activation/extensionSurvey.unit.test.ts index 8e191ec82810..a89797bfebef 100644 --- a/src/test/activation/extensionSurvey.unit.test.ts +++ b/src/test/activation/extensionSurvey.unit.test.ts @@ -127,21 +127,38 @@ suite('Extension survey prompt - shouldShowBanner()', () => { } random.verifyAll(); }); - test('Returns false if telemetry.disableFeedback is enabled', async () => { + test('Returns true if telemetry.feedback.enabled is enabled', async () => { disableSurveyForTime.setup((d) => d.value).returns(() => false); doNotShowAgain.setup((d) => d.value).returns(() => false); const telemetryConfig = TypeMoq.Mock.ofType(); workspaceService.setup((w) => w.getConfiguration('telemetry')).returns(() => telemetryConfig.object); telemetryConfig - .setup((t) => t.get(TypeMoq.It.isValue('disableFeedback'), TypeMoq.It.isValue(false))) + .setup((t) => t.get(TypeMoq.It.isValue('feedback.enabled'), TypeMoq.It.isValue(true))) .returns(() => true); const result = extensionSurveyPrompt.shouldShowBanner(); - expect(result).to.equal(false, 'Banner should not be shown when telemetry.disableFeedback is true'); + expect(result).to.equal(true, 'Banner should be shown when telemetry.feedback.enabled is true'); workspaceService.verify((w) => w.getConfiguration('telemetry'), TypeMoq.Times.once()); - telemetryConfig.verify((t) => t.get('disableFeedback', false), TypeMoq.Times.once()); + telemetryConfig.verify((t) => t.get('feedback.enabled', true), TypeMoq.Times.once()); + }); + + test('Returns false if telemetry.feedback.enabled is disabled', async () => { + disableSurveyForTime.setup((d) => d.value).returns(() => false); + doNotShowAgain.setup((d) => d.value).returns(() => false); + + const telemetryConfig = TypeMoq.Mock.ofType(); + workspaceService.setup((w) => w.getConfiguration('telemetry')).returns(() => telemetryConfig.object); + telemetryConfig + .setup((t) => t.get(TypeMoq.It.isValue('feedback.enabled'), TypeMoq.It.isValue(true))) + .returns(() => false); + + const result = extensionSurveyPrompt.shouldShowBanner(); + + expect(result).to.equal(false, 'Banner should not be shown when feedback.enabled is false'); + workspaceService.verify((w) => w.getConfiguration('telemetry'), TypeMoq.Times.once()); + telemetryConfig.verify((t) => t.get('feedback.enabled', true), TypeMoq.Times.once()); }); test('Returns true if user is in the random sampling', async () => { From 4df044f0966a3ea69d2ca07a3ab3ef1ac6cad97f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:18:45 +0000 Subject: [PATCH 0896/1136] update to v2025.4.0 for release (#24934) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 10866a3d9ca1..08905852f2ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.3.0-dev", + "version": "2025.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.3.0-dev", + "version": "2025.4.0", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 04b6710a1fa0..3fc7894a29bd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.3.0-dev", + "version": "2025.4.0", "featureFlags": { "usingNewInterpreterStorage": true }, From 237f6b6fc3df38062ae3b6b75ccb67bb147069e9 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:40:51 +0000 Subject: [PATCH 0897/1136] bump to 2025.5.0-dev (#24936) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 08905852f2ce..e98a5b6f545f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.4.0", + "version": "2025.5.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.4.0", + "version": "2025.5.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 3fc7894a29bd..f63a8c90745f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.4.0", + "version": "2025.5.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 175a35d6a37d966f36a59fc18818961ccaec2000 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 28 Mar 2025 23:46:51 -0700 Subject: [PATCH 0898/1136] fix: temp for PR file check failure (#24939) Fix https://github.com/microsoft/vscode-python/issues/24938 --- .github/workflows/pr-file-check.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index fcdf91b4f64b..b5ba2fe1f109 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -47,7 +47,8 @@ jobs: script: | const labels = context.payload.pull_request.labels.map(label => label.name); if (!labels.includes('skip-issue-check')) { - const issueLink = context.payload.pull_request.body.match(/https:\/\/github\.com\/\S+\/issues\/\d+/); + const prBody = context.payload.pull_request.body || ''; + const issueLink = prBody.match(/https:\/\/github\.com\/\S+\/issues\/\d+/); if (!issueLink) { core.setFailed('No associated issue found in the PR description.'); } From 18efcd6f6a47f09559891c0637912d25931bbc2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 07:15:20 -0700 Subject: [PATCH 0899/1136] Bump tar-fs from 2.1.1 to 2.1.2 (#24940) Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.1 to 2.1.2.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tar-fs&package-manager=npm_and_yarn&previous-version=2.1.1&new-version=2.1.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index e98a5b6f545f..7159e2850336 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13031,10 +13031,11 @@ } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "chownr": "^1.1.1", @@ -24904,9 +24905,9 @@ "dev": true }, "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "dev": true, "optional": true, "requires": { From 944c204dc51c8fe3ea977901de1166bac8c9123c Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 1 Apr 2025 13:00:46 -0700 Subject: [PATCH 0900/1136] Set native repl to default to false, remove experiment (#24952) Resolves: https://github.com/microsoft/vscode-python/issues/24951 --- package.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/package.json b/package.json index f63a8c90745f..f27adddcc524 100644 --- a/package.json +++ b/package.json @@ -639,11 +639,7 @@ "default": false, "description": "%python.REPL.sendToNativeREPL.description%", "scope": "resource", - "type": "boolean", - "tags": [ - "onExP", - "preview" - ] + "type": "boolean" }, "python.REPL.provideVariables": { "default": true, From 41e66244fc5bf57c764d79955932c151b9cbc59e Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 1 Apr 2025 14:08:33 -0700 Subject: [PATCH 0901/1136] fix: log only the required environment variables (#24937) Fixes https://github.com/microsoft/vscode-python/issues/24764 This was 100% AI generated fix. All I did was "#fetch https://github.com/microsoft/vscode-python/issues/24764 and fix". --- .../testing/testController/pytest/pytestDiscoveryAdapter.ts | 2 +- .../testing/testController/pytest/pytestExecutionAdapter.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 71d71997c57e..777adfb985d4 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -112,7 +112,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { mutableEnv.PYTHONPATH = pythonPathCommand; mutableEnv.TEST_RUN_PIPE = discoveryPipeName; traceInfo( - `All environment variables set for pytest discovery, PYTHONPATH: ${JSON.stringify(mutableEnv.PYTHONPATH)}`, + `Environment variables set for pytest discovery: PYTHONPATH=${mutableEnv.PYTHONPATH}, TEST_RUN_PIPE=${mutableEnv.TEST_RUN_PIPE}`, ); // delete UUID following entire discovery finishing. diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 3a824f79ac63..ba9d26af6e4c 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -138,9 +138,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const testIdsFileName = await utils.writeTestIdsFile(testIds); mutableEnv.RUN_TEST_IDS_PIPE = testIdsFileName; traceInfo( - `All environment variables set for pytest execution, PYTHONPATH: ${JSON.stringify( - mutableEnv.PYTHONPATH, - )}`, + `Environment variables set for pytest execution: PYTHONPATH=${mutableEnv.PYTHONPATH}, TEST_RUN_PIPE=${mutableEnv.TEST_RUN_PIPE}, RUN_TEST_IDS_PIPE=${mutableEnv.RUN_TEST_IDS_PIPE}`, ); const spawnOptions: SpawnOptions = { From 6a20c9c8401e598f28346d56fe61494b19ce5113 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 2 Apr 2025 07:27:43 -0700 Subject: [PATCH 0902/1136] fix: use wrapper functions for easier testing (#24941) Fixes https://github.com/microsoft/vscode-python/issues/24426 --- src/client/common/vscodeApis/commandApis.ts | 15 ++++++++------ src/client/common/vscodeApis/windowApis.ts | 10 ++++++++++ src/client/common/vscodeApis/workspaceApis.ts | 9 +++++++++ src/client/repl/replCommandHandler.ts | 20 +++++++++---------- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/client/common/vscodeApis/commandApis.ts b/src/client/common/vscodeApis/commandApis.ts index 580760e106e1..908cb761c538 100644 --- a/src/client/common/vscodeApis/commandApis.ts +++ b/src/client/common/vscodeApis/commandApis.ts @@ -3,13 +3,16 @@ import { commands, Disposable } from 'vscode'; -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/** + * Wrapper for vscode.commands.executeCommand to make it easier to mock in tests + */ +export function executeCommand(command: string, ...rest: any[]): Thenable { + return commands.executeCommand(command, ...rest); +} +/** + * Wrapper for vscode.commands.registerCommand to make it easier to mock in tests + */ export function registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { return commands.registerCommand(command, callback, thisArg); } - -export function executeCommand(command: string, ...rest: any[]): Thenable { - return commands.executeCommand(command, ...rest); -} diff --git a/src/client/common/vscodeApis/windowApis.ts b/src/client/common/vscodeApis/windowApis.ts index fc63a189f2ff..fade0a028487 100644 --- a/src/client/common/vscodeApis/windowApis.ts +++ b/src/client/common/vscodeApis/windowApis.ts @@ -22,6 +22,9 @@ import { LogOutputChannel, OutputChannel, TerminalLinkProvider, + NotebookDocument, + NotebookEditor, + NotebookDocumentShowOptions, } from 'vscode'; import { createDeferred, Deferred } from '../utils/async'; import { Resource } from '../types'; @@ -31,6 +34,13 @@ export function showTextDocument(uri: Uri): Thenable { return window.showTextDocument(uri); } +export function showNotebookDocument( + document: NotebookDocument, + options?: NotebookDocumentShowOptions, +): Thenable { + return window.showNotebookDocument(document, options); +} + export function showQuickPick( items: readonly T[] | Thenable, options?: QuickPickOptions, diff --git a/src/client/common/vscodeApis/workspaceApis.ts b/src/client/common/vscodeApis/workspaceApis.ts index cb516da73075..ef06d982c9a7 100644 --- a/src/client/common/vscodeApis/workspaceApis.ts +++ b/src/client/common/vscodeApis/workspaceApis.ts @@ -98,6 +98,15 @@ export function createDirectory(uri: vscode.Uri): Thenable { return vscode.workspace.fs.createDirectory(uri); } +export function openNotebookDocument(uri: vscode.Uri): Thenable; +export function openNotebookDocument( + notebookType: string, + content?: vscode.NotebookData, +): Thenable; +export function openNotebookDocument(notebook: any, content?: vscode.NotebookData): Thenable { + return vscode.workspace.openNotebookDocument(notebook, content); +} + export function copy(source: vscode.Uri, dest: vscode.Uri, options?: { overwrite?: boolean }): Thenable { return vscode.workspace.fs.copy(source, dest, options); } diff --git a/src/client/repl/replCommandHandler.ts b/src/client/repl/replCommandHandler.ts index 89ccbe11c337..f65580dd1e17 100644 --- a/src/client/repl/replCommandHandler.ts +++ b/src/client/repl/replCommandHandler.ts @@ -1,6 +1,4 @@ import { - commands, - window, NotebookController, NotebookEditor, ViewColumn, @@ -9,11 +7,13 @@ import { NotebookCellKind, NotebookEdit, WorkspaceEdit, - workspace, Uri, } from 'vscode'; import { getExistingReplViewColumn, getTabNameForUri } from './replUtils'; import { PVSC_EXTENSION_ID } from '../common/constants'; +import { showNotebookDocument } from '../common/vscodeApis/windowApis'; +import { openNotebookDocument, applyEdit } from '../common/vscodeApis/workspaceApis'; +import { executeCommand } from '../common/vscodeApis/commandApis'; /** * Function that opens/show REPL using IW UI. @@ -26,7 +26,7 @@ export async function openInteractiveREPL( let viewColumn = ViewColumn.Beside; if (notebookDocument instanceof Uri) { // Case where NotebookDocument is undefined, but workspace mementoURI exists. - notebookDocument = await workspace.openNotebookDocument(notebookDocument); + notebookDocument = await openNotebookDocument(notebookDocument); } else if (notebookDocument) { // Case where NotebookDocument (REPL document already exists in the tab) const existingReplViewColumn = getExistingReplViewColumn(notebookDocument); @@ -34,9 +34,9 @@ export async function openInteractiveREPL( } else if (!notebookDocument) { // Case where NotebookDocument doesnt exist, or // became outdated (untitled.ipynb created without Python extension knowing, effectively taking over original Python REPL's URI) - notebookDocument = await workspace.openNotebookDocument('jupyter-notebook'); + notebookDocument = await openNotebookDocument('jupyter-notebook'); } - const editor = await window.showNotebookDocument(notebookDocument!, { + const editor = await showNotebookDocument(notebookDocument!, { viewColumn, asRepl: 'Python REPL', preserveFocus, @@ -52,7 +52,7 @@ export async function openInteractiveREPL( return undefined; } - await commands.executeCommand('notebook.selectKernel', { + await executeCommand('notebook.selectKernel', { editor, id: notebookController.id, extension: PVSC_EXTENSION_ID, @@ -69,7 +69,7 @@ export async function selectNotebookKernel( notebookControllerId: string, extensionId: string, ): Promise { - await commands.executeCommand('notebook.selectKernel', { + await executeCommand('notebook.selectKernel', { notebookEditor, id: notebookControllerId, extension: extensionId, @@ -84,7 +84,7 @@ export async function executeNotebookCell(notebookEditor: NotebookEditor, code: const cellIndex = replOptions?.appendIndex ?? notebook.cellCount; await addCellToNotebook(notebook, cellIndex, code); // Execute the cell - commands.executeCommand('notebook.cell.execute', { + executeCommand('notebook.cell.execute', { ranges: [{ start: cellIndex, end: cellIndex + 1 }], document: notebook.uri, }); @@ -100,5 +100,5 @@ async function addCellToNotebook(notebookDocument: NotebookDocument, index: numb const notebookEdit = NotebookEdit.insertCells(index, [notebookCellData]); const workspaceEdit = new WorkspaceEdit(); workspaceEdit.set(notebookDocument!.uri, [notebookEdit]); - await workspace.applyEdit(workspaceEdit); + await applyEdit(workspaceEdit); } From 3275c34acfa9be393e0239769e7e587e94db477a Mon Sep 17 00:00:00 2001 From: Paul <53956863+hutch3232@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:38:33 -0500 Subject: [PATCH 0903/1136] prevent native REPL from caching state between sessions (#24857) Resolves: #24359 Definitely warrants scrutiny / input as I've never written typescript before. Solved with trial and error + LLMs. --------- Co-authored-by: Anthony Kim Co-authored-by: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> --- .github/actions/smoke-tests/action.yml | 4 +- src/client/common/vscodeApis/workspaceApis.ts | 4 + src/client/repl/nativeRepl.ts | 15 ++- src/client/repl/pythonServer.ts | 1 + src/test/repl/nativeRepl.test.ts | 115 ++++++++++++++++-- 5 files changed, 121 insertions(+), 18 deletions(-) diff --git a/.github/actions/smoke-tests/action.yml b/.github/actions/smoke-tests/action.yml index 2463f83ee90c..ed760e8b8202 100644 --- a/.github/actions/smoke-tests/action.yml +++ b/.github/actions/smoke-tests/action.yml @@ -13,13 +13,13 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: ${{ inputs.node_version }} cache: 'npm' - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: '3.x' cache: 'pip' diff --git a/src/client/common/vscodeApis/workspaceApis.ts b/src/client/common/vscodeApis/workspaceApis.ts index ef06d982c9a7..cd45f655702d 100644 --- a/src/client/common/vscodeApis/workspaceApis.ts +++ b/src/client/common/vscodeApis/workspaceApis.ts @@ -60,6 +60,10 @@ export function onDidChangeConfiguration(handler: (e: vscode.ConfigurationChange return vscode.workspace.onDidChangeConfiguration(handler); } +export function onDidCloseNotebookDocument(handler: (e: vscode.NotebookDocument) => void): vscode.Disposable { + return vscode.workspace.onDidCloseNotebookDocument(handler); +} + export function createFileSystemWatcher( globPattern: vscode.GlobPattern, ignoreCreateEvents?: boolean, diff --git a/src/client/repl/nativeRepl.ts b/src/client/repl/nativeRepl.ts index 6edd3cbd70a7..9b002655d714 100644 --- a/src/client/repl/nativeRepl.ts +++ b/src/client/repl/nativeRepl.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + // Native Repl class that holds instance of pythonServer and replController import { @@ -7,13 +10,12 @@ import { QuickPickItem, TextEditor, Uri, - workspace, WorkspaceFolder, } from 'vscode'; import { Disposable } from 'vscode-jsonrpc'; import { PVSC_EXTENSION_ID } from '../common/constants'; import { showQuickPick } from '../common/vscodeApis/windowApis'; -import { getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; +import { getWorkspaceFolders, onDidCloseNotebookDocument } from '../common/vscodeApis/workspaceApis'; import { PythonEnvironment } from '../pythonEnvironments/info'; import { createPythonServer, PythonServer } from './pythonServer'; import { executeNotebookCell, openInteractiveREPL, selectNotebookKernel } from './replCommandHandler'; @@ -69,11 +71,18 @@ export class NativeRepl implements Disposable { */ private watchNotebookClosed(): void { this.disposables.push( - workspace.onDidCloseNotebookDocument(async (nb) => { + onDidCloseNotebookDocument(async (nb) => { if (this.notebookDocument && nb.uri.toString() === this.notebookDocument.uri.toString()) { this.notebookDocument = undefined; this.newReplSession = true; await updateWorkspaceStateValue(NATIVE_REPL_URI_MEMENTO, undefined); + this.pythonServer.dispose(); + this.pythonServer = createPythonServer([this.interpreter.path as string], this.cwd); + this.disposables.push(this.pythonServer); + if (this.replController) { + this.replController.dispose(); + } + nativeRepl = undefined; } }), ); diff --git a/src/client/repl/pythonServer.ts b/src/client/repl/pythonServer.ts index 570433714f98..74e2d6ae7251 100644 --- a/src/client/repl/pythonServer.ts +++ b/src/client/repl/pythonServer.ts @@ -104,6 +104,7 @@ class PythonServerImpl implements PythonServer, Disposable { this.connection.sendNotification('exit'); this.disposables.forEach((d) => d.dispose()); this.connection.dispose(); + serverInstance = undefined; } } diff --git a/src/test/repl/nativeRepl.test.ts b/src/test/repl/nativeRepl.test.ts index 999bc656c64d..c05bb311a839 100644 --- a/src/test/repl/nativeRepl.test.ts +++ b/src/test/repl/nativeRepl.test.ts @@ -1,14 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + /* eslint-disable no-unused-expressions */ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as TypeMoq from 'typemoq'; import * as sinon from 'sinon'; -import { Disposable } from 'vscode'; +import { Disposable, EventEmitter, NotebookDocument, Uri } from 'vscode'; import { expect } from 'chai'; import { IInterpreterService } from '../../client/interpreter/contracts'; import { PythonEnvironment } from '../../client/pythonEnvironments/info'; -import { getNativeRepl, NativeRepl } from '../../client/repl/nativeRepl'; +import * as NativeReplModule from '../../client/repl/nativeRepl'; import * as persistentState from '../../client/common/persistentState'; +import * as PythonServer from '../../client/repl/pythonServer'; +import * as vscodeWorkspaceApis from '../../client/common/vscodeApis/workspaceApis'; +import * as replController from '../../client/repl/replController'; +import { executeCommand } from '../../client/common/vscodeApis/commandApis'; suite('REPL - Native REPL', () => { let interpreterService: TypeMoq.IMock; @@ -19,8 +26,20 @@ suite('REPL - Native REPL', () => { let setReplControllerSpy: sinon.SinonSpy; let getWorkspaceStateValueStub: sinon.SinonStub; let updateWorkspaceStateValueStub: sinon.SinonStub; + let createReplControllerStub: sinon.SinonStub; + let mockNotebookController: any; setup(() => { + (NativeReplModule as any).nativeRepl = undefined; + + mockNotebookController = { + id: 'mockController', + dispose: sinon.stub(), + updateNotebookAffinity: sinon.stub(), + createNotebookCellExecution: sinon.stub(), + variableProvider: null, + }; + interpreterService = TypeMoq.Mock.ofType(); interpreterService .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) @@ -28,13 +47,13 @@ suite('REPL - Native REPL', () => { disposable = TypeMoq.Mock.ofType(); disposableArray = [disposable.object]; - setReplDirectoryStub = sinon.stub(NativeRepl.prototype as any, 'setReplDirectory').resolves(); // Stubbing private method - // Use a spy instead of a stub for setReplController - setReplControllerSpy = sinon.spy(NativeRepl.prototype, 'setReplController'); + createReplControllerStub = sinon.stub(replController, 'createReplController').returns(mockNotebookController); + setReplDirectoryStub = sinon.stub(NativeReplModule.NativeRepl.prototype as any, 'setReplDirectory').resolves(); + setReplControllerSpy = sinon.spy(NativeReplModule.NativeRepl.prototype, 'setReplController'); updateWorkspaceStateValueStub = sinon.stub(persistentState, 'updateWorkspaceStateValue').resolves(); }); - teardown(() => { + teardown(async () => { disposableArray.forEach((d) => { if (d) { d.dispose(); @@ -42,15 +61,16 @@ suite('REPL - Native REPL', () => { }); disposableArray = []; sinon.restore(); + executeCommand('workbench.action.closeActiveEditor'); }); test('getNativeRepl should call create constructor', async () => { - const createMethodStub = sinon.stub(NativeRepl, 'create'); + const createMethodStub = sinon.stub(NativeReplModule.NativeRepl, 'create'); interpreterService .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); const interpreter = await interpreterService.object.getActiveInterpreter(); - await getNativeRepl(interpreter as PythonEnvironment, disposableArray); + await NativeReplModule.getNativeRepl(interpreter as PythonEnvironment, disposableArray); expect(createMethodStub.calledOnce).to.be.true; }); @@ -61,7 +81,7 @@ suite('REPL - Native REPL', () => { .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); const interpreter = await interpreterService.object.getActiveInterpreter(); - const nativeRepl = await getNativeRepl(interpreter as PythonEnvironment, disposableArray); + const nativeRepl = await NativeReplModule.getNativeRepl(interpreter as PythonEnvironment, disposableArray); nativeRepl.sendToNativeRepl(undefined, false); @@ -74,7 +94,7 @@ suite('REPL - Native REPL', () => { .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); const interpreter = await interpreterService.object.getActiveInterpreter(); - const nativeRepl = await getNativeRepl(interpreter as PythonEnvironment, disposableArray); + const nativeRepl = await NativeReplModule.getNativeRepl(interpreter as PythonEnvironment, disposableArray); nativeRepl.sendToNativeRepl(undefined, false); @@ -87,12 +107,81 @@ suite('REPL - Native REPL', () => { .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); - await NativeRepl.create(interpreter as PythonEnvironment); + await NativeReplModule.NativeRepl.create(interpreter as PythonEnvironment); expect(setReplDirectoryStub.calledOnce).to.be.true; expect(setReplControllerSpy.calledOnce).to.be.true; + expect(createReplControllerStub.calledOnce).to.be.true; + }); + + test('watchNotebookClosed should clean up resources when notebook is closed', async () => { + const notebookCloseEmitter = new EventEmitter(); + sinon.stub(vscodeWorkspaceApis, 'onDidCloseNotebookDocument').callsFake((handler) => { + const disposable = notebookCloseEmitter.event(handler); + return disposable; + }); + + const mockPythonServer = { + onCodeExecuted: new EventEmitter().event, + execute: sinon.stub().resolves({ status: true, output: 'test output' }), + executeSilently: sinon.stub().resolves({ status: true, output: 'test output' }), + interrupt: sinon.stub(), + input: sinon.stub(), + checkValidCommand: sinon.stub().resolves(true), + dispose: sinon.stub(), + }; + + // Track the number of times createPythonServer was called + let createPythonServerCallCount = 0; + sinon.stub(PythonServer, 'createPythonServer').callsFake(() => { + // eslint-disable-next-line no-plusplus + createPythonServerCallCount++; + return mockPythonServer; + }); + + const interpreter = await interpreterService.object.getActiveInterpreter(); - setReplDirectoryStub.restore(); - setReplControllerSpy.restore(); + // Create NativeRepl directly to have more control over its state, go around private constructor. + const nativeRepl = new (NativeReplModule.NativeRepl as any)(); + nativeRepl.interpreter = interpreter as PythonEnvironment; + nativeRepl.cwd = '/helloJustMockedCwd/cwd'; + nativeRepl.pythonServer = mockPythonServer; + nativeRepl.replController = mockNotebookController; + nativeRepl.disposables = []; + + // Make the singleton point to our instance for testing + // Otherwise, it gets mixed with Native Repl from .create from test above. + (NativeReplModule as any).nativeRepl = nativeRepl; + + // Reset call count after initial setup + createPythonServerCallCount = 0; + + // Set notebookDocument to a mock document + const mockReplUri = Uri.parse('untitled:Untitled-999.ipynb?jupyter-notebook'); + const mockNotebookDocument = ({ + uri: mockReplUri, + toString: () => mockReplUri.toString(), + } as unknown) as NotebookDocument; + + nativeRepl.notebookDocument = mockNotebookDocument; + + // Create a mock notebook document for closing event with same URI + const closingNotebookDocument = ({ + uri: mockReplUri, + toString: () => mockReplUri.toString(), + } as unknown) as NotebookDocument; + + notebookCloseEmitter.fire(closingNotebookDocument); + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect( + updateWorkspaceStateValueStub.calledWith(NativeReplModule.NATIVE_REPL_URI_MEMENTO, undefined), + 'updateWorkspaceStateValue should be called with NATIVE_REPL_URI_MEMENTO and undefined', + ).to.be.true; + expect(mockPythonServer.dispose.calledOnce, 'pythonServer.dispose() should be called once').to.be.true; + expect(createPythonServerCallCount, 'createPythonServer should be called to create a new server').to.equal(1); + expect(nativeRepl.notebookDocument, 'notebookDocument should be undefined after closing').to.be.undefined; + expect(nativeRepl.newReplSession, 'newReplSession should be set to true after closing').to.be.true; + expect(mockNotebookController.dispose.calledOnce, 'replController.dispose() should be called once').to.be.true; }); }); From affbd1b523ded0a8ac3045cfacf9987d411a6f4d Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Wed, 2 Apr 2025 08:16:26 -0700 Subject: [PATCH 0904/1136] Remove debris from smart send experiment (#24957) This should've been part of https://github.com/microsoft/vscode-python/pull/23067 Removing debris, making package.json look cleaner. /cc @cwebster-99 --- package.json | 12 ++++-------- package.nls.json | 1 - 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index f27adddcc524..f2a199d705c5 100644 --- a/package.json +++ b/package.json @@ -448,8 +448,7 @@ "pythonPromptNewToolsExt", "pythonTerminalEnvVarActivation", "pythonDiscoveryUsingWorkers", - "pythonTestAdapter", - "pythonREPLSmartSend" + "pythonTestAdapter" ], "enumDescriptions": [ "%python.experiments.All.description%", @@ -457,8 +456,7 @@ "%python.experiments.pythonPromptNewToolsExt.description%", "%python.experiments.pythonTerminalEnvVarActivation.description%", "%python.experiments.pythonDiscoveryUsingWorkers.description%", - "%python.experiments.pythonTestAdapter.description%", - "%python.experiments.pythonREPLSmartSend.description%" + "%python.experiments.pythonTestAdapter.description%" ] }, "scope": "window", @@ -475,8 +473,7 @@ "pythonPromptNewToolsExt", "pythonTerminalEnvVarActivation", "pythonDiscoveryUsingWorkers", - "pythonTestAdapter", - "pythonREPLSmartSend" + "pythonTestAdapter" ], "enumDescriptions": [ "%python.experiments.All.description%", @@ -484,8 +481,7 @@ "%python.experiments.pythonPromptNewToolsExt.description%", "%python.experiments.pythonTerminalEnvVarActivation.description%", "%python.experiments.pythonDiscoveryUsingWorkers.description%", - "%python.experiments.pythonTestAdapter.description%", - "%python.experiments.pythonREPLSmartSend.description%" + "%python.experiments.pythonTestAdapter.description%" ] }, "scope": "window", diff --git a/package.nls.json b/package.nls.json index 8bff60a4b07d..2d4028063006 100644 --- a/package.nls.json +++ b/package.nls.json @@ -42,7 +42,6 @@ "python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.", "python.experiments.pythonDiscoveryUsingWorkers.description": "Enables use of worker threads to do heavy computation when discovering interpreters.", "python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.", - "python.experiments.pythonREPLSmartSend.description": "Denotes the Python REPL Smart Send experiment.", "python.experiments.pythonRecommendTensorboardExt.description": "Denotes the Tensorboard Extension recommendation experiment.", "python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.", "python.languageServer.description": "Defines type of the language server.", From cd714bf25cde196854d7e78e8e0c20be54edaf55 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 2 Apr 2025 18:06:16 +0000 Subject: [PATCH 0905/1136] force absolute path for coverage results (#24948) fixes https://github.com/microsoft/vscode-python/issues/24943 --- python_files/vscode_pytest/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 00f356e20dcd..c953a52d8a50 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -474,6 +474,9 @@ def pytest_sessionfinish(session, exitstatus): "lines_covered": list(lines_covered), # list of int "lines_missed": list(lines_missed), # list of int } + # convert relative path to absolute path + if not pathlib.Path(file).is_absolute(): + file = str(pathlib.Path(file).resolve()) file_coverage_map[file] = file_info payload: CoveragePayloadDict = CoveragePayloadDict( From cf91dc8a82af1e8baecbdd632ba44641f40f4459 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 2 Apr 2025 18:07:52 +0000 Subject: [PATCH 0906/1136] remove ITestLogChannel (#24954) --- python_files/vscode_pytest/__init__.py | 8 +- src/client/common/types.ts | 3 - src/client/common/utils/localize.ts | 1 - src/client/extensionInit.ts | 11 +- .../testing/testController/common/utils.ts | 7 - .../testing/testController/controller.ts | 7 +- .../pytest/pytestDiscoveryAdapter.ts | 12 +- .../pytest/pytestExecutionAdapter.ts | 11 +- .../unittest/testDiscoveryAdapter.ts | 20 +-- .../unittest/testExecutionAdapter.ts | 15 +-- src/test/serviceRegistry.ts | 5 +- .../testConfigurationManager.unit.test.ts | 4 +- .../testing/common/testingAdapter.test.ts | 127 +++--------------- src/test/testing/configuration.unit.test.ts | 4 +- .../testing/configurationFactory.unit.test.ts | 6 +- .../pytestDiscoveryAdapter.unit.test.ts | 16 +-- .../pytestExecutionAdapter.unit.test.ts | 17 +-- .../testCancellationRunAdapters.unit.test.ts | 11 +- .../testDiscoveryAdapter.unit.test.ts | 12 +- .../testExecutionAdapter.unit.test.ts | 17 +-- .../workspaceTestAdapter.unit.test.ts | 46 +++---- 21 files changed, 86 insertions(+), 274 deletions(-) diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index c953a52d8a50..bf9466991383 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -121,7 +121,7 @@ def pytest_internalerror(excrepr, excinfo): # noqa: ARG001 excinfo -- the exception information of type ExceptionInfo. """ # call.excinfo.exconly() returns the exception as a string. - ERRORS.append(excinfo.exconly() + "\n Check Python Test Logs for more details.") + ERRORS.append(excinfo.exconly() + "\n Check Python Logs for more details.") def pytest_exception_interact(node, call, report): @@ -139,9 +139,9 @@ def pytest_exception_interact(node, call, report): if call.excinfo and call.excinfo.typename != "AssertionError": if report.outcome == "skipped" and "SkipTest" in str(call): return - ERRORS.append(call.excinfo.exconly() + "\n Check Python Test Logs for more details.") + ERRORS.append(call.excinfo.exconly() + "\n Check Python Logs for more details.") else: - ERRORS.append(report.longreprtext + "\n Check Python Test Logs for more details.") + ERRORS.append(report.longreprtext + "\n Check Python Logs for more details.") else: # If during execution, send this data that the given node failed. report_value = "error" @@ -204,7 +204,7 @@ def pytest_keyboard_interrupt(excinfo): excinfo -- the exception information of type ExceptionInfo. """ # The function execonly() returns the exception as a string. - ERRORS.append(excinfo.exconly() + "\n Check Python Test Logs for more details.") + ERRORS.append(excinfo.exconly() + "\n Check Python Logs for more details.") class TestOutcome(Dict): diff --git a/src/client/common/types.ts b/src/client/common/types.ts index cec297f8329a..2cb393d89bdf 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -16,7 +16,6 @@ import { Memento, LogOutputChannel, Uri, - OutputChannel, } from 'vscode'; import { LanguageServerType } from '../activation/types'; import type { InstallOptions, InterpreterUri, ModuleInstallFlags } from './installer/types'; @@ -29,8 +28,6 @@ export interface IDisposable { export const ILogOutputChannel = Symbol('ILogOutputChannel'); export interface ILogOutputChannel extends LogOutputChannel {} -export const ITestOutputChannel = Symbol('ITestOutputChannel'); -export interface ITestOutputChannel extends OutputChannel {} export const IDocumentSymbolProvider = Symbol('IDocumentSymbolProvider'); export interface IDocumentSymbolProvider extends DocumentSymbolProvider {} export const IsWindows = Symbol('IS_WINDOWS'); diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 18ab501f241b..97fe6201e4fb 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -257,7 +257,6 @@ export namespace InterpreterQuickPickList { export namespace OutputChannelNames { export const languageServer = l10n.t('Python Language Server'); export const python = l10n.t('Python'); - export const pythonTest = l10n.t('Python Test Log'); } export namespace Linters { diff --git a/src/client/extensionInit.ts b/src/client/extensionInit.ts index 1332dc6bd070..b161643d2d97 100644 --- a/src/client/extensionInit.ts +++ b/src/client/extensionInit.ts @@ -4,7 +4,7 @@ 'use strict'; import { Container } from 'inversify'; -import { Disposable, l10n, Memento, window } from 'vscode'; +import { Disposable, Memento, window } from 'vscode'; import { registerTypes as platformRegisterTypes } from './common/platform/serviceRegistry'; import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; @@ -15,7 +15,6 @@ import { IExtensionContext, IMemento, ILogOutputChannel, - ITestOutputChannel, WORKSPACE_MEMENTO, } from './common/types'; import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; @@ -28,7 +27,6 @@ import * as pythonEnvironments from './pythonEnvironments'; import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { registerLogger } from './logging'; import { OutputChannelLogger } from './logging/outputChannelLogger'; -import { isTrusted, isVirtualWorkspace } from './common/vscodeApis/workspaceApis'; // The code in this module should do nothing more complex than register // objects to DI and simple init (e.g. no side effects). That implies @@ -56,14 +54,7 @@ export function initializeGlobals( disposables.push(standardOutputChannel); disposables.push(registerLogger(new OutputChannelLogger(standardOutputChannel))); - const unitTestOutChannel = window.createOutputChannel(OutputChannelNames.pythonTest); - disposables.push(unitTestOutChannel); - if (isVirtualWorkspace() || !isTrusted()) { - unitTestOutChannel.appendLine(l10n.t('Unit tests are not supported in this environment.')); - } - serviceManager.addSingletonInstance(ILogOutputChannel, standardOutputChannel); - serviceManager.addSingletonInstance(ITestOutputChannel, unitTestOutChannel); return { context, diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 9923d7ec3e12..c624ef034cf1 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -23,13 +23,6 @@ export function fixLogLinesNoTrailing(content: string): string { const lines = content.split(/\r?\n/g); return `${lines.join('\r\n')}`; } - -export const MESSAGE_ON_TESTING_OUTPUT_MOVE = - 'Starting now, all test run output will be sent to the Test Result panel,' + - ' while test discovery output will be sent to the "Python" output channel instead of the "Python Test Log" channel.' + - ' The "Python Test Log" channel will be deprecated within the next month.' + - ' See https://github.com/microsoft/vscode-python/wiki/New-Method-for-Output-Handling-in-Python-Testing for details.'; - export function createTestingDeferred(): Deferred { return createDeferred(); } diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 98a7f909a8e2..fe384709c371 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -25,7 +25,7 @@ import { IExtensionSingleActivationService } from '../../activation/types'; import { ICommandManager, IWorkspaceService } from '../../common/application/types'; import * as constants from '../../common/constants'; import { IPythonExecutionFactory } from '../../common/process/types'; -import { IConfigurationService, IDisposableRegistry, ITestOutputChannel, Resource } from '../../common/types'; +import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; import { DelayedTrigger, IDelayedTrigger } from '../../common/utils/delayTrigger'; import { noop } from '../../common/utils/misc'; import { IInterpreterService } from '../../interpreter/contracts'; @@ -98,7 +98,6 @@ export class PythonTestController implements ITestController, IExtensionSingleAc @inject(ICommandManager) private readonly commandManager: ICommandManager, @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, - @inject(ITestOutputChannel) private readonly testOutputChannel: ITestOutputChannel, @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider, ) { this.refreshCancellation = new CancellationTokenSource(); @@ -176,13 +175,11 @@ export class PythonTestController implements ITestController, IExtensionSingleAc resultResolver = new PythonResultResolver(this.testController, testProvider, workspace.uri); discoveryAdapter = new UnittestTestDiscoveryAdapter( this.configSettings, - this.testOutputChannel, resultResolver, this.envVarsService, ); executionAdapter = new UnittestTestExecutionAdapter( this.configSettings, - this.testOutputChannel, resultResolver, this.envVarsService, ); @@ -191,13 +188,11 @@ export class PythonTestController implements ITestController, IExtensionSingleAc resultResolver = new PythonResultResolver(this.testController, testProvider, workspace.uri); discoveryAdapter = new PytestTestDiscoveryAdapter( this.configSettings, - this.testOutputChannel, resultResolver, this.envVarsService, ); executionAdapter = new PytestTestExecutionAdapter( this.configSettings, - this.testOutputChannel, resultResolver, this.envVarsService, ); diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 777adfb985d4..04258ddbddf2 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -9,13 +9,12 @@ import { IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; +import { IConfigurationService } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { traceError, traceInfo, traceVerbose, traceWarn } from '../../../logging'; import { DiscoveredTestPayload, ITestDiscoveryAdapter, ITestResultResolver } from '../common/types'; import { - MESSAGE_ON_TESTING_OUTPUT_MOVE, createDiscoveryErrorPayload, createTestingDeferred, fixLogLinesNoTrailing, @@ -33,7 +32,6 @@ import { useEnvExtension, getEnvironment, runInBackground } from '../../../envEx export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { constructor( public configSettings: IConfigurationService, - private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} @@ -138,15 +136,12 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { proc.stdout.on('data', (data) => { const out = fixLogLinesNoTrailing(data.toString()); traceInfo(out); - this.outputChannel?.append(out); }); proc.stderr.on('data', (data) => { const out = fixLogLinesNoTrailing(data.toString()); traceError(out); - this.outputChannel?.append(out); }); proc.onExit((code, signal) => { - this.outputChannel?.append(MESSAGE_ON_TESTING_OUTPUT_MOVE); if (code !== 0) { traceError( `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, @@ -165,7 +160,6 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { const spawnOptions: SpawnOptions = { cwd, throwOnStdErr: true, - outputChannel: this.outputChannel, env: mutableEnv, token, }; @@ -200,20 +194,16 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. - // TODO: after a release, remove discovery output from the "Python Test Log" channel and send it to the "Python" channel instead. result?.proc?.stdout?.on('data', (data) => { const out = fixLogLinesNoTrailing(data.toString()); traceInfo(out); - spawnOptions?.outputChannel?.append(`${out}`); }); result?.proc?.stderr?.on('data', (data) => { const out = fixLogLinesNoTrailing(data.toString()); traceError(out); - spawnOptions?.outputChannel?.append(`${out}`); }); result?.proc?.on('exit', (code, signal) => { - this.outputChannel?.append(MESSAGE_ON_TESTING_OUTPUT_MOVE); if (code !== 0) { traceError( `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}.`, diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index ba9d26af6e4c..053c497c56e0 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -4,7 +4,7 @@ import { CancellationTokenSource, DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; import * as path from 'path'; import { ChildProcess } from 'child_process'; -import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; +import { IConfigurationService } from '../../../common/types'; import { Deferred } from '../../../common/utils/async'; import { traceError, traceInfo, traceVerbose } from '../../../logging'; import { ExecutionTestPayload, ITestExecutionAdapter, ITestResultResolver } from '../common/types'; @@ -25,7 +25,6 @@ import { getEnvironment, runInBackground, useEnvExtension } from '../../../envEx export class PytestTestExecutionAdapter implements ITestExecutionAdapter { constructor( public configSettings: IConfigurationService, - private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} @@ -144,7 +143,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const spawnOptions: SpawnOptions = { cwd, throwOnStdErr: true, - outputChannel: this.outputChannel, env: mutableEnv, token: runInstance?.token, }; @@ -192,15 +190,12 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { proc.stdout.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); runInstance?.appendOutput(out); - this.outputChannel?.append(out); }); proc.stderr.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); runInstance?.appendOutput(out); - this.outputChannel?.append(out); }); proc.onExit((code, signal) => { - this.outputChannel?.append(utils.MESSAGE_ON_TESTING_OUTPUT_MOVE); if (code !== 0) { traceError( `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, @@ -238,19 +233,15 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. // Displays output to user and ensure the subprocess doesn't run into buffer overflow. - // TODO: after a release, remove run output from the "Python Test Log" channel and send it to the "Test Result" channel instead. result?.proc?.stdout?.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); runInstance?.appendOutput(out); - this.outputChannel?.append(out); }); result?.proc?.stderr?.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); runInstance?.appendOutput(out); - this.outputChannel?.append(out); }); result?.proc?.on('exit', (code, signal) => { - this.outputChannel?.append(utils.MESSAGE_ON_TESTING_OUTPUT_MOVE); if (code !== 0) { traceError( `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 7e478b25735a..23d70568687f 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import { CancellationTokenSource, Uri } from 'vscode'; import { CancellationToken } from 'vscode-jsonrpc'; import { ChildProcess } from 'child_process'; -import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; +import { IConfigurationService } from '../../../common/types'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { DiscoveredTestPayload, @@ -22,12 +22,7 @@ import { IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { - MESSAGE_ON_TESTING_OUTPUT_MOVE, - createDiscoveryErrorPayload, - fixLogLinesNoTrailing, - startDiscoveryNamedPipe, -} from '../common/utils'; +import { createDiscoveryErrorPayload, fixLogLinesNoTrailing, startDiscoveryNamedPipe } from '../common/utils'; import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; import { getEnvironment, runInBackground, useEnvExtension } from '../../../envExt/api.internal'; @@ -37,7 +32,6 @@ import { getEnvironment, runInBackground, useEnvExtension } from '../../../envEx export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { constructor( public configSettings: IConfigurationService, - private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} @@ -79,7 +73,6 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { workspaceFolder: uri, command, cwd, - outChannel: this.outputChannel, token, }; @@ -128,15 +121,12 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { proc.stdout.on('data', (data) => { const out = fixLogLinesNoTrailing(data.toString()); traceInfo(out); - this.outputChannel?.append(out); }); proc.stderr.on('data', (data) => { const out = fixLogLinesNoTrailing(data.toString()); traceError(out); - this.outputChannel?.append(out); }); proc.onExit((code, signal) => { - this.outputChannel?.append(MESSAGE_ON_TESTING_OUTPUT_MOVE); if (code !== 0) { traceError( `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, @@ -155,7 +145,6 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { token: options.token, cwd: options.cwd, throwOnStdErr: true, - outputChannel: options.outChannel, env: mutableEnv, }; @@ -187,22 +176,17 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { resultProc = result?.proc; // Displays output to user and ensure the subprocess doesn't run into buffer overflow. - // TODO: after a release, remove discovery output from the "Python Test Log" channel and send it to the "Python" channel instead. - // TODO: after a release, remove run output from the "Python Test Log" channel and send it to the "Test Result" channel instead. result?.proc?.stdout?.on('data', (data) => { const out = fixLogLinesNoTrailing(data.toString()); - spawnOptions?.outputChannel?.append(`${out}`); traceInfo(out); }); result?.proc?.stderr?.on('data', (data) => { const out = fixLogLinesNoTrailing(data.toString()); - spawnOptions?.outputChannel?.append(`${out}`); traceError(out); }); result?.proc?.on('exit', (code, signal) => { // if the child has testIds then this is a run request - spawnOptions?.outputChannel?.append(MESSAGE_ON_TESTING_OUTPUT_MOVE); if (code !== 0) { // This occurs when we are running discovery diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index e46e8c436583..74572ea5c63c 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { CancellationTokenSource, DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; import { ChildProcess } from 'child_process'; -import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; +import { IConfigurationService } from '../../../common/types'; import { Deferred, createDeferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { @@ -15,7 +15,7 @@ import { TestExecutionCommand, } from '../common/types'; import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; -import { MESSAGE_ON_TESTING_OUTPUT_MOVE, fixLogLinesNoTrailing } from '../common/utils'; +import { fixLogLinesNoTrailing } from '../common/utils'; import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../../common/variables/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, @@ -35,7 +35,6 @@ import { getEnvironment, runInBackground, useEnvExtension } from '../../../envEx export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { constructor( public configSettings: IConfigurationService, - private readonly outputChannel: ITestOutputChannel, private readonly resultResolver?: ITestResultResolver, private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} @@ -122,7 +121,6 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { cwd, profileKind, testIds, - outChannel: this.outputChannel, token: runInstance?.token, }; traceLog(`Running UNITTEST execution for the following test ids: ${testIds}`); @@ -140,7 +138,6 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { token: options.token, cwd: options.cwd, throwOnStdErr: true, - outputChannel: options.outChannel, env: mutableEnv, }; // Create the Python environment in which to execute the command. @@ -205,15 +202,12 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { proc.stdout.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); runInstance?.appendOutput(out); - this.outputChannel?.append(out); }); proc.stderr.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); runInstance?.appendOutput(out); - this.outputChannel?.append(out); }); proc.onExit((code, signal) => { - this.outputChannel?.append(utils.MESSAGE_ON_TESTING_OUTPUT_MOVE); if (code !== 0) { traceError( `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, @@ -249,23 +243,18 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { resultProc = result?.proc; // Displays output to user and ensure the subprocess doesn't run into buffer overflow. - // TODO: after a release, remove discovery output from the "Python Test Log" channel and send it to the "Python" channel instead. - // TODO: after a release, remove run output from the "Python Test Log" channel and send it to the "Test Result" channel instead. result?.proc?.stdout?.on('data', (data) => { const out = fixLogLinesNoTrailing(data.toString()); runInstance?.appendOutput(`${out}`); - spawnOptions?.outputChannel?.append(out); }); result?.proc?.stderr?.on('data', (data) => { const out = fixLogLinesNoTrailing(data.toString()); runInstance?.appendOutput(`${out}`); - spawnOptions?.outputChannel?.append(out); }); result?.proc?.on('exit', (code, signal) => { // if the child has testIds then this is a run request - spawnOptions?.outputChannel?.append(MESSAGE_ON_TESTING_OUTPUT_MOVE); if (code !== 0 && testIds) { // This occurs when we are running the test and there is an error which occurs. diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index a0919752cefd..382659b3f838 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -27,11 +27,10 @@ import { ICurrentProcess, IDisposableRegistry, IMemento, - ILogOutputChannel, IPathUtils, IsWindows, WORKSPACE_MEMENTO, - ITestOutputChannel, + ILogOutputChannel, } from '../client/common/types'; import { registerTypes as variableRegisterTypes } from '../client/common/variables/serviceRegistry'; import { EnvironmentActivationService } from '../client/interpreter/activation/service'; @@ -84,7 +83,7 @@ export class IocContainer { this.serviceManager.addSingletonInstance(ILogOutputChannel, stdOutputChannel); const testOutputChannel = new MockOutputChannel('Python Test - UnitTests'); this.disposables.push(testOutputChannel); - this.serviceManager.addSingletonInstance(ITestOutputChannel, testOutputChannel); + this.serviceManager.addSingletonInstance(ILogOutputChannel, testOutputChannel); this.serviceManager.addSingleton( IInterpreterAutoSelectionService, diff --git a/src/test/testing/common/managers/testConfigurationManager.unit.test.ts b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts index c8b6085e599d..1b049d4f3fbe 100644 --- a/src/test/testing/common/managers/testConfigurationManager.unit.test.ts +++ b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts @@ -5,7 +5,7 @@ import * as TypeMoq from 'typemoq'; import { OutputChannel, Uri } from 'vscode'; -import { IInstaller, ITestOutputChannel, Product } from '../../../../client/common/types'; +import { IInstaller, ILogOutputChannel, Product } from '../../../../client/common/types'; import { getNamesAndValues } from '../../../../client/common/utils/enum'; import { IServiceContainer } from '../../../../client/ioc/types'; import { UNIT_TEST_PRODUCTS } from '../../../../client/testing/common/constants'; @@ -41,7 +41,7 @@ suite('Unit Test Configuration Manager (unit)', () => { const installer = TypeMoq.Mock.ofType().object; const serviceContainer = TypeMoq.Mock.ofType(); serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(ITestOutputChannel))) + .setup((s) => s.get(TypeMoq.It.isValue(ILogOutputChannel))) .returns(() => outputChannel); serviceContainer .setup((s) => s.get(TypeMoq.It.isValue(ITestConfigSettingsService))) diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index ec19ce00f13f..834bccbd905f 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -11,7 +11,7 @@ import * as sinon from 'sinon'; import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; import { ITestController, ITestResultResolver } from '../../../client/testing/testController/common/types'; import { IPythonExecutionFactory } from '../../../client/common/process/types'; -import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; +import { IConfigurationService } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize'; import { traceError, traceLog } from '../../../client/logging'; @@ -22,7 +22,6 @@ import { PythonResultResolver } from '../../../client/testing/testController/com import { TestProvider } from '../../../client/testing/types'; import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; -import { createTypeMoq } from '../../mocks/helper'; import * as pixi from '../../../client/pythonEnvironments/common/environmentManagers/pixi'; suite('End to End Tests: test adapters', () => { @@ -32,7 +31,6 @@ suite('End to End Tests: test adapters', () => { let serviceContainer: IServiceContainer; let envVarsService: IEnvironmentVariablesProvider; let workspaceUri: Uri; - let testOutputChannel: typeMoq.IMock; let testController: TestController; let getPixiStub: sinon.SinonStub; const unittestProvider: TestProvider = UNITTEST_PROVIDER; @@ -116,24 +114,6 @@ suite('End to End Tests: test adapters', () => { envVarsService = serviceContainer.get(IEnvironmentVariablesProvider); // create objects that were not injected - - testOutputChannel = createTypeMoq(); - testOutputChannel - .setup((x) => x.append(typeMoq.It.isAny())) - .callback((appendVal: any) => { - traceLog('output channel - ', appendVal.toString()); - }) - .returns(() => { - // Whatever you need to return - }); - testOutputChannel - .setup((x) => x.appendLine(typeMoq.It.isAny())) - .callback((appendVal: any) => { - traceLog('output channel ', appendVal.toString()); - }) - .returns(() => { - // Whatever you need to return - }); }); teardown(() => { sinon.restore(); @@ -189,12 +169,7 @@ suite('End to End Tests: test adapters', () => { configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; // run unittest discovery - const discoveryAdapter = new UnittestTestDiscoveryAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const discoveryAdapter = new UnittestTestDiscoveryAdapter(configService, resultResolver, envVarsService); await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { // verification after discovery is complete @@ -234,12 +209,7 @@ suite('End to End Tests: test adapters', () => { workspaceUri = Uri.parse(rootPathLargeWorkspace); configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; // run discovery - const discoveryAdapter = new UnittestTestDiscoveryAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const discoveryAdapter = new UnittestTestDiscoveryAdapter(configService, resultResolver, envVarsService); await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { // 1. Check the status is "success" @@ -274,12 +244,7 @@ suite('End to End Tests: test adapters', () => { return Promise.resolve(); }; // run pytest discovery - const discoveryAdapter = new PytestTestDiscoveryAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { // verification after discovery is complete @@ -329,12 +294,7 @@ suite('End to End Tests: test adapters', () => { return Promise.resolve(); }; // run pytest discovery - const discoveryAdapter = new PytestTestDiscoveryAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); configService.getSettings(workspaceUri).testing.pytestArgs = []; await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { @@ -418,12 +378,7 @@ suite('End to End Tests: test adapters', () => { return Promise.resolve(); }; // run pytest discovery - const discoveryAdapter = new PytestTestDiscoveryAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); configService.getSettings(workspaceUri).testing.pytestArgs = []; await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { @@ -494,12 +449,7 @@ suite('End to End Tests: test adapters', () => { return Promise.resolve(); }; // run pytest discovery - const discoveryAdapter = new PytestTestDiscoveryAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathLargeWorkspace); @@ -548,12 +498,7 @@ suite('End to End Tests: test adapters', () => { workspaceUri = Uri.parse(rootPathSmallWorkspace); configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; // run execution - const executionAdapter = new UnittestTestExecutionAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const executionAdapter = new UnittestTestExecutionAdapter(configService, resultResolver, envVarsService); const testRun = typeMoq.Mock.ofType(); testRun .setup((t) => t.token) @@ -628,12 +573,7 @@ suite('End to End Tests: test adapters', () => { configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; // run unittest execution - const executionAdapter = new UnittestTestExecutionAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const executionAdapter = new UnittestTestExecutionAdapter(configService, resultResolver, envVarsService); const testRun = typeMoq.Mock.ofType(); testRun .setup((t) => t.token) @@ -703,12 +643,7 @@ suite('End to End Tests: test adapters', () => { configService.getSettings(workspaceUri).testing.pytestArgs = []; // run pytest execution - const executionAdapter = new PytestTestExecutionAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); const testRun = typeMoq.Mock.ofType(); testRun .setup((t) => t.token) @@ -783,12 +718,7 @@ suite('End to End Tests: test adapters', () => { workspaceUri = Uri.parse(rootPathCoverageWorkspace); configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; // run execution - const executionAdapter = new UnittestTestExecutionAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const executionAdapter = new UnittestTestExecutionAdapter(configService, resultResolver, envVarsService); const testRun = typeMoq.Mock.ofType(); testRun .setup((t) => t.token) @@ -837,12 +767,7 @@ suite('End to End Tests: test adapters', () => { configService.getSettings(workspaceUri).testing.pytestArgs = []; // run pytest execution - const executionAdapter = new PytestTestExecutionAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); const testRun = typeMoq.Mock.ofType(); testRun .setup((t) => t.token) @@ -908,12 +833,7 @@ suite('End to End Tests: test adapters', () => { } // run pytest execution - const executionAdapter = new PytestTestExecutionAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); const testRun = typeMoq.Mock.ofType(); testRun .setup((t) => t.token) @@ -982,12 +902,7 @@ suite('End to End Tests: test adapters', () => { workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; - const discoveryAdapter = new UnittestTestDiscoveryAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const discoveryAdapter = new UnittestTestDiscoveryAdapter(configService, resultResolver, envVarsService); const testRun = typeMoq.Mock.ofType(); testRun .setup((t) => t.token) @@ -1041,12 +956,7 @@ suite('End to End Tests: test adapters', () => { return Promise.resolve(); }; // run pytest discovery - const discoveryAdapter = new PytestTestDiscoveryAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); @@ -1102,12 +1012,7 @@ suite('End to End Tests: test adapters', () => { configService.getSettings(workspaceUri).testing.pytestArgs = []; // run pytest execution - const executionAdapter = new PytestTestExecutionAdapter( - configService, - testOutputChannel.object, - resultResolver, - envVarsService, - ); + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); const testRun = typeMoq.Mock.ofType(); testRun .setup((t) => t.token) diff --git a/src/test/testing/configuration.unit.test.ts b/src/test/testing/configuration.unit.test.ts index 6682abf019b8..98d19dca9cbc 100644 --- a/src/test/testing/configuration.unit.test.ts +++ b/src/test/testing/configuration.unit.test.ts @@ -10,7 +10,7 @@ import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../cli import { IConfigurationService, IInstaller, - ITestOutputChannel, + ILogOutputChannel, IPythonSettings, Product, } from '../../client/common/types'; @@ -61,7 +61,7 @@ suite('Unit Tests - ConfigurationService', () => { configurationService.setup((c) => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); serviceContainer - .setup((c) => c.get(typeMoq.It.isValue(ITestOutputChannel))) + .setup((c) => c.get(typeMoq.It.isValue(ILogOutputChannel))) .returns(() => outputChannel.object); serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); serviceContainer diff --git a/src/test/testing/configurationFactory.unit.test.ts b/src/test/testing/configurationFactory.unit.test.ts index 0813e3f4aae1..493dfcc00b95 100644 --- a/src/test/testing/configurationFactory.unit.test.ts +++ b/src/test/testing/configurationFactory.unit.test.ts @@ -7,7 +7,7 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as typeMoq from 'typemoq'; import { OutputChannel, Uri } from 'vscode'; -import { IInstaller, ITestOutputChannel, Product } from '../../client/common/types'; +import { IInstaller, ILogOutputChannel, Product } from '../../client/common/types'; import { IServiceContainer } from '../../client/ioc/types'; import { ITestConfigSettingsService, ITestConfigurationManagerFactory } from '../../client/testing/common/types'; import { TestConfigurationManagerFactory } from '../../client/testing/configurationFactory'; @@ -24,9 +24,7 @@ suite('Unit Tests - ConfigurationManagerFactory', () => { const installer = typeMoq.Mock.ofType(); const testConfigService = typeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(typeMoq.It.isValue(ITestOutputChannel))) - .returns(() => outputChannel.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(ILogOutputChannel))).returns(() => outputChannel.object); serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); serviceContainer .setup((c) => c.get(typeMoq.It.isValue(ITestConfigSettingsService))) diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index 157134cdf276..ec155ee3107d 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import { Observable } from 'rxjs/Observable'; import * as fs from 'fs'; import * as sinon from 'sinon'; -import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +import { IConfigurationService } from '../../../../client/common/types'; import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; import { IPythonExecutionFactory, @@ -29,7 +29,6 @@ suite('pytest test discovery adapter', () => { let adapter: PytestTestDiscoveryAdapter; let execService: typeMoq.IMock; let deferred: Deferred; - let outputChannel: typeMoq.IMock; let expectedPath: string; let uri: Uri; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -72,7 +71,6 @@ suite('pytest test discovery adapter', () => { execService = typeMoq.Mock.ofType(); execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); - outputChannel = typeMoq.Mock.ofType(); const output = new Observable>(() => { /* no op */ @@ -117,7 +115,7 @@ suite('pytest test discovery adapter', () => { ); sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); - adapter = new PytestTestDiscoveryAdapter(configService, outputChannel.object); + adapter = new PytestTestDiscoveryAdapter(configService); adapter.discoverTests(uri, execFactory.object); // add in await and trigger await deferred.promise; @@ -175,7 +173,7 @@ suite('pytest test discovery adapter', () => { return Promise.resolve(execService.object); }); - adapter = new PytestTestDiscoveryAdapter(configServiceNew, outputChannel.object); + adapter = new PytestTestDiscoveryAdapter(configServiceNew); adapter.discoverTests(uri, execFactory.object); // add in await and trigger await deferred.promise; @@ -239,7 +237,7 @@ suite('pytest test discovery adapter', () => { return Promise.resolve(execService.object); }); - adapter = new PytestTestDiscoveryAdapter(configServiceNew, outputChannel.object); + adapter = new PytestTestDiscoveryAdapter(configServiceNew); adapter.discoverTests(uri, execFactory.object); // add in await and trigger await deferred.promise; @@ -307,7 +305,7 @@ suite('pytest test discovery adapter', () => { return Promise.resolve(execService.object); }); - adapter = new PytestTestDiscoveryAdapter(configServiceNew, outputChannel.object); + adapter = new PytestTestDiscoveryAdapter(configServiceNew); adapter.discoverTests(uri, execFactory.object); // add in await and trigger await deferred.promise; @@ -356,7 +354,7 @@ suite('pytest test discovery adapter', () => { ); sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); - adapter = new PytestTestDiscoveryAdapter(configService, outputChannel.object); + adapter = new PytestTestDiscoveryAdapter(configService); const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); // Trigger cancellation before exec observable call finishes @@ -406,7 +404,7 @@ suite('pytest test discovery adapter', () => { ); sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); - adapter = new PytestTestDiscoveryAdapter(configService, outputChannel.object); + adapter = new PytestTestDiscoveryAdapter(configService); const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); // add in await and trigger diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index 413c0af9406d..e0401edc7b41 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -7,7 +7,7 @@ import * as typeMoq from 'typemoq'; import * as sinon from 'sinon'; import * as path from 'path'; import { Observable } from 'rxjs/Observable'; -import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +import { IConfigurationService } from '../../../../client/common/types'; import { IPythonExecutionFactory, IPythonExecutionService, @@ -117,8 +117,7 @@ suite('pytest test execution adapter', () => { const testRun = typeMoq.Mock.ofType(); testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); - const outputChannel = typeMoq.Mock.ofType(); - adapter = new PytestTestExecutionAdapter(configService, outputChannel.object); + adapter = new PytestTestExecutionAdapter(configService); const testIds = ['test1id', 'test2id']; adapter.runTests(uri, testIds, TestRunProfileKind.Run, testRun.object, execFactory.object); @@ -148,8 +147,7 @@ suite('pytest test execution adapter', () => { const testRun = typeMoq.Mock.ofType(); testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); - const outputChannel = typeMoq.Mock.ofType(); - adapter = new PytestTestExecutionAdapter(configService, outputChannel.object); + adapter = new PytestTestExecutionAdapter(configService); adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); await deferred2.promise; @@ -207,8 +205,7 @@ suite('pytest test execution adapter', () => { isTestExecution: () => false, } as unknown) as IConfigurationService; const uri = Uri.file(myTestPath); - const outputChannel = typeMoq.Mock.ofType(); - adapter = new PytestTestExecutionAdapter(configService, outputChannel.object); + adapter = new PytestTestExecutionAdapter(configService); adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); await deferred2.promise; @@ -263,8 +260,7 @@ suite('pytest test execution adapter', () => { } as any), ); const uri = Uri.file(myTestPath); - const outputChannel = typeMoq.Mock.ofType(); - adapter = new PytestTestExecutionAdapter(configService, outputChannel.object); + adapter = new PytestTestExecutionAdapter(configService); adapter.runTests(uri, [], TestRunProfileKind.Debug, testRun.object, execFactory.object, debugLauncher.object); await deferred3.promise; debugLauncher.verify( @@ -305,8 +301,7 @@ suite('pytest test execution adapter', () => { const testRun = typeMoq.Mock.ofType(); testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); - const outputChannel = typeMoq.Mock.ofType(); - adapter = new PytestTestExecutionAdapter(configService, outputChannel.object); + adapter = new PytestTestExecutionAdapter(configService); adapter.runTests(uri, [], TestRunProfileKind.Coverage, testRun.object, execFactory.object); await deferred2.promise; diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts index 81480d08b2b8..ceee7f54f447 100644 --- a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -7,7 +7,7 @@ import * as sinon from 'sinon'; import * as path from 'path'; import { Observable } from 'rxjs'; import { IPythonExecutionFactory, IPythonExecutionService, Output } from '../../../client/common/process/types'; -import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; +import { IConfigurationService } from '../../../client/common/types'; import { Deferred, createDeferred } from '../../../client/common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../client/constants'; import { ITestDebugLauncher } from '../../../client/testing/common/types'; @@ -121,7 +121,7 @@ suite('Execution Flow Run Adapters', () => { }); // define adapter and run tests - const testAdapter = createAdapter(adapter, configService, typeMoq.Mock.ofType().object); + const testAdapter = createAdapter(adapter, configService); await testAdapter.runTests( Uri.file(myTestPath), [], @@ -202,7 +202,7 @@ suite('Execution Flow Run Adapters', () => { }); // define adapter and run tests - const testAdapter = createAdapter(adapter, configService, typeMoq.Mock.ofType().object); + const testAdapter = createAdapter(adapter, configService); await testAdapter.runTests( Uri.file(myTestPath), [], @@ -221,9 +221,8 @@ suite('Execution Flow Run Adapters', () => { function createAdapter( adapterType: string, configService: IConfigurationService, - outputChannel: ITestOutputChannel, ): PytestTestExecutionAdapter | UnittestTestExecutionAdapter { - if (adapterType === 'pytest') return new PytestTestExecutionAdapter(configService, outputChannel); - if (adapterType === 'unittest') return new UnittestTestExecutionAdapter(configService, outputChannel); + if (adapterType === 'pytest') return new PytestTestExecutionAdapter(configService); + if (adapterType === 'unittest') return new UnittestTestExecutionAdapter(configService); throw Error('un-compatible adapter type'); } diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index 0a2cfad866d5..4dae070bccbe 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -9,7 +9,7 @@ import * as fs from 'fs'; import { CancellationTokenSource, Uri } from 'vscode'; import { Observable } from 'rxjs'; import * as sinon from 'sinon'; -import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +import { IConfigurationService } from '../../../../client/common/types'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; import { UnittestTestDiscoveryAdapter } from '../../../../client/testing/testController/unittest/testDiscoveryAdapter'; import { Deferred, createDeferred } from '../../../../client/common/utils/async'; @@ -25,7 +25,6 @@ import * as extapi from '../../../../client/envExt/api.internal'; suite('Unittest test discovery adapter', () => { let configService: IConfigurationService; - let outputChannel: typeMoq.IMock; let mockProc: MockChildProcess; let execService: typeMoq.IMock; let execFactory = typeMoq.Mock.ofType(); @@ -47,7 +46,6 @@ suite('Unittest test discovery adapter', () => { testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, }), } as unknown) as IConfigurationService; - outputChannel = typeMoq.Mock.ofType(); // set up exec service with child process mockProc = new MockChildProcess('', ['']); @@ -94,7 +92,7 @@ suite('Unittest test discovery adapter', () => { }); test('DiscoverTests should send the discovery command to the test server with the correct args', async () => { - const adapter = new UnittestTestDiscoveryAdapter(configService, outputChannel.object); + const adapter = new UnittestTestDiscoveryAdapter(configService); adapter.discoverTests(uri, execFactory.object); const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; @@ -137,7 +135,7 @@ suite('Unittest test discovery adapter', () => { testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'], cwd: expectedNewPath.toString() }, }), } as unknown) as IConfigurationService; - const adapter = new UnittestTestDiscoveryAdapter(configService, outputChannel.object); + const adapter = new UnittestTestDiscoveryAdapter(configService); adapter.discoverTests(uri, execFactory.object); const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; @@ -189,7 +187,7 @@ suite('Unittest test discovery adapter', () => { ); sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); - const adapter = new UnittestTestDiscoveryAdapter(configService, outputChannel.object); + const adapter = new UnittestTestDiscoveryAdapter(configService); const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); // Trigger cancellation before exec observable call finishes @@ -239,7 +237,7 @@ suite('Unittest test discovery adapter', () => { ); sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); - const adapter = new UnittestTestDiscoveryAdapter(configService, outputChannel.object); + const adapter = new UnittestTestDiscoveryAdapter(configService); const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); // add in await and trigger diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index 688d6d398101..ab492736f0ad 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -7,7 +7,7 @@ import * as typeMoq from 'typemoq'; import * as sinon from 'sinon'; import * as path from 'path'; import { Observable } from 'rxjs/Observable'; -import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +import { IConfigurationService } from '../../../../client/common/types'; import { IPythonExecutionFactory, IPythonExecutionService, @@ -116,8 +116,7 @@ suite('Unittest test execution adapter', () => { const testRun = typeMoq.Mock.ofType(); testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); - const outputChannel = typeMoq.Mock.ofType(); - adapter = new UnittestTestExecutionAdapter(configService, outputChannel.object); + adapter = new UnittestTestExecutionAdapter(configService); const testIds = ['test1id', 'test2id']; adapter.runTests(uri, testIds, TestRunProfileKind.Run, testRun.object, execFactory.object); @@ -147,8 +146,7 @@ suite('Unittest test execution adapter', () => { const testRun = typeMoq.Mock.ofType(); testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); - const outputChannel = typeMoq.Mock.ofType(); - adapter = new UnittestTestExecutionAdapter(configService, outputChannel.object); + adapter = new UnittestTestExecutionAdapter(configService); adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); await deferred2.promise; @@ -205,8 +203,7 @@ suite('Unittest test execution adapter', () => { isTestExecution: () => false, } as unknown) as IConfigurationService; const uri = Uri.file(myTestPath); - const outputChannel = typeMoq.Mock.ofType(); - adapter = new UnittestTestExecutionAdapter(configService, outputChannel.object); + adapter = new UnittestTestExecutionAdapter(configService); adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); await deferred2.promise; @@ -261,8 +258,7 @@ suite('Unittest test execution adapter', () => { } as any), ); const uri = Uri.file(myTestPath); - const outputChannel = typeMoq.Mock.ofType(); - adapter = new UnittestTestExecutionAdapter(configService, outputChannel.object); + adapter = new UnittestTestExecutionAdapter(configService); adapter.runTests(uri, [], TestRunProfileKind.Debug, testRun.object, execFactory.object, debugLauncher.object); await deferred3.promise; debugLauncher.verify( @@ -302,8 +298,7 @@ suite('Unittest test execution adapter', () => { const testRun = typeMoq.Mock.ofType(); testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); const uri = Uri.file(myTestPath); - const outputChannel = typeMoq.Mock.ofType(); - adapter = new UnittestTestExecutionAdapter(configService, outputChannel.object); + adapter = new UnittestTestExecutionAdapter(configService); adapter.runTests(uri, [], TestRunProfileKind.Coverage, testRun.object, execFactory.object); await deferred2.promise; diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts index 9a07d4451e85..aac07793ca66 100644 --- a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -6,7 +6,7 @@ import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; import { TestController, TestItem, TestItemCollection, TestRun, Uri } from 'vscode'; -import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; +import { IConfigurationService } from '../../../client/common/types'; import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; // 7/7 import { WorkspaceTestAdapter } from '../../../client/testing/testController/workspaceTestAdapter'; @@ -25,7 +25,6 @@ suite('Workspace test adapter', () => { let discoverTestsStub: sinon.SinonStub; let sendTelemetryStub: sinon.SinonStub; - let outputChannel: typemoq.IMock; let telemetryEvent: { eventName: EventName; properties: Record }[] = []; let execFactory: typemoq.IMock; @@ -106,7 +105,6 @@ suite('Workspace test adapter', () => { discoverTestsStub = sinon.stub(UnittestTestDiscoveryAdapter.prototype, 'discoverTests'); sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); - outputChannel = typemoq.Mock.ofType(); }); teardown(() => { @@ -119,8 +117,8 @@ suite('Workspace test adapter', () => { test('If discovery failed correctly create error node', async () => { discoverTestsStub.rejects(new Error('foo')); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings, outputChannel.object); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings, outputChannel.object); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); const uriFoo = Uri.parse('foo'); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', @@ -158,8 +156,8 @@ suite('Workspace test adapter', () => { test("When discovering tests, the workspace test adapter should call the test discovery adapter's discoverTest method", async () => { discoverTestsStub.resolves(); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings, outputChannel.object); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings, outputChannel.object); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', testDiscoveryAdapter, @@ -184,8 +182,8 @@ suite('Workspace test adapter', () => { }), ); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings, outputChannel.object); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings, outputChannel.object); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', testDiscoveryAdapter, @@ -206,8 +204,8 @@ suite('Workspace test adapter', () => { test('If discovery succeeds, send a telemetry event with the "failed" key set to false', async () => { discoverTestsStub.resolves({ status: 'success' }); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings, outputChannel.object); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings, outputChannel.object); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', @@ -229,8 +227,8 @@ suite('Workspace test adapter', () => { test('If discovery failed, send a telemetry event with the "failed" key set to true, and add an error node to the test controller', async () => { discoverTestsStub.rejects(new Error('foo')); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings, outputChannel.object); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings, outputChannel.object); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', @@ -254,7 +252,6 @@ suite('Workspace test adapter', () => { let stubResultResolver: ITestResultResolver; let executionTestsStub: sinon.SinonStub; let sendTelemetryStub: sinon.SinonStub; - let outputChannel: typemoq.IMock; let runInstance: typemoq.IMock; let testControllerMock: typemoq.IMock; let telemetryEvent: { eventName: EventName; properties: Record }[] = []; @@ -331,7 +328,6 @@ suite('Workspace test adapter', () => { executionTestsStub = sandbox.stub(UnittestTestExecutionAdapter.prototype, 'runTests'); sendTelemetryStub = sandbox.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); - outputChannel = typemoq.Mock.ofType(); runInstance = typemoq.Mock.ofType(); const testProvider = 'pytest'; @@ -346,8 +342,8 @@ suite('Workspace test adapter', () => { sandbox.restore(); }); test('When executing tests, the right tests should be sent to be executed', async () => { - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings, outputChannel.object); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings, outputChannel.object); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', testDiscoveryAdapter, @@ -394,8 +390,8 @@ suite('Workspace test adapter', () => { }); test("When executing tests, the workspace test adapter should call the test execute adapter's executionTest method", async () => { - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings, outputChannel.object); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings, outputChannel.object); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', testDiscoveryAdapter, @@ -420,8 +416,8 @@ suite('Workspace test adapter', () => { }), ); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings, outputChannel.object); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings, outputChannel.object); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', testDiscoveryAdapter, @@ -442,8 +438,8 @@ suite('Workspace test adapter', () => { test('If execution failed correctly create error node', async () => { executionTestsStub.rejects(new Error('foo')); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings, outputChannel.object); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings, outputChannel.object); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', @@ -480,8 +476,8 @@ suite('Workspace test adapter', () => { test('If execution failed, send a telemetry event with the "failed" key set to true, and add an error node to the test controller', async () => { executionTestsStub.rejects(new Error('foo')); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings, outputChannel.object); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings, outputChannel.object); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', From 70a36f568d42d644712a26f260ea9554085df141 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 3 Apr 2025 15:11:47 -0700 Subject: [PATCH 0907/1136] fix: use latest `pet` (#24964) --- build/azure-pipeline.stable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 2e7ebcfea82a..f9a37c5a9ec5 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -128,7 +128,7 @@ extends: project: 'Monaco' definition: 593 buildVersionToDownload: 'latestFromBranch' - branchName: 'refs/heads/release/2025.2' + branchName: 'refs/heads/release/2025.4' targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' artifactName: 'bin-$(buildTarget)' itemPattern: | From e9fb2bf1462689dc717bf611aa344dc6950ee144 Mon Sep 17 00:00:00 2001 From: Danila Grobov Date: Fri, 4 Apr 2025 02:29:09 +0300 Subject: [PATCH 0908/1136] Django Test Runs with Coverage (#24927) https://github.com/microsoft/vscode-python/issues/24199 Co-authored-by: Danila Grobov (s4642g) --- build/test-requirements.txt | 1 + python_files/tests/pytestadapter/helpers.py | 2 +- .../tests/unittestadapter/test_coverage.py | 40 +++++++++++++ .../unittestadapter/django_handler.py | 60 ++++++++++++------- 4 files changed, 79 insertions(+), 24 deletions(-) diff --git a/build/test-requirements.txt b/build/test-requirements.txt index 8b0ea1636157..097b18256764 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -32,6 +32,7 @@ django-stubs coverage pytest-cov pytest-json +pytest-timeout # for pytest-describe related tests diff --git a/python_files/tests/pytestadapter/helpers.py b/python_files/tests/pytestadapter/helpers.py index 7a75e6248844..4c337585bece 100644 --- a/python_files/tests/pytestadapter/helpers.py +++ b/python_files/tests/pytestadapter/helpers.py @@ -244,7 +244,7 @@ def runner_with_cwd_env( """ process_args: List[str] pipe_name: str - if "MANAGE_PY_PATH" in env_add: + if "MANAGE_PY_PATH" in env_add and "COVERAGE_ENABLED" not in env_add: # If we are running Django, generate a unittest-specific pipe name. process_args = [sys.executable, *args] pipe_name = generate_random_pipe_name("unittest-discovery-test") diff --git a/python_files/tests/unittestadapter/test_coverage.py b/python_files/tests/unittestadapter/test_coverage.py index 594aa764370e..d357d95ad111 100644 --- a/python_files/tests/unittestadapter/test_coverage.py +++ b/python_files/tests/unittestadapter/test_coverage.py @@ -8,6 +8,8 @@ import pathlib import sys +import pytest + sys.path.append(os.fspath(pathlib.Path(__file__).parent)) python_files_path = pathlib.Path(__file__).parent.parent.parent @@ -49,3 +51,41 @@ def test_basic_coverage(): assert focal_function_coverage.get("lines_missed") is not None assert set(focal_function_coverage.get("lines_covered")) == {4, 5, 7, 9, 10, 11, 12, 13, 14} assert set(focal_function_coverage.get("lines_missed")) == {6} + + +@pytest.mark.timeout(30) +def test_basic_django_coverage(): + """This test validates that the coverage is correctly calculated for a Django project.""" + data_path: pathlib.Path = TEST_DATA_PATH / "simple_django" + manage_py_path: str = os.fsdecode(data_path / "manage.py") + execution_script: pathlib.Path = python_files_path / "unittestadapter" / "execution.py" + + test_ids = [ + "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question", + "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question_2", + "polls.tests.QuestionModelTests.test_question_creation_and_retrieval", + ] + + script_str = os.fsdecode(execution_script) + actual = helpers.runner_with_cwd_env( + [script_str, "--udiscovery", "-p", "*test*.py", *test_ids], + data_path, + { + "MANAGE_PY_PATH": manage_py_path, + "_TEST_VAR_UNITTEST": "True", + "COVERAGE_ENABLED": os.fspath(data_path), + }, + ) + + assert actual + coverage = actual[-1] + assert coverage + results = coverage["result"] + assert results + assert len(results) == 15 + polls_views_coverage = results.get(str(data_path / "polls" / "views.py")) + assert polls_views_coverage + assert polls_views_coverage.get("lines_covered") is not None + assert polls_views_coverage.get("lines_missed") is not None + assert set(polls_views_coverage.get("lines_covered")) == {3, 4, 6} + assert set(polls_views_coverage.get("lines_missed")) == {7} diff --git a/python_files/unittestadapter/django_handler.py b/python_files/unittestadapter/django_handler.py index 9daa816d0918..4230c951e162 100644 --- a/python_files/unittestadapter/django_handler.py +++ b/python_files/unittestadapter/django_handler.py @@ -1,11 +1,17 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import importlib.util import os import pathlib import subprocess import sys -from typing import List +from contextlib import contextmanager, suppress +from typing import TYPE_CHECKING, Generator, List + +if TYPE_CHECKING: + from importlib.machinery import ModuleSpec + script_dir = pathlib.Path(__file__).parent sys.path.append(os.fspath(script_dir)) @@ -16,6 +22,17 @@ ) +@contextmanager +def override_argv(argv: List[str]) -> Generator: + """Context manager to temporarily override sys.argv with the provided arguments.""" + original_argv = sys.argv + sys.argv = argv + try: + yield + finally: + sys.argv = original_argv + + def django_discovery_runner(manage_py_path: str, args: List[str]) -> None: # Attempt a small amount of validation on the manage.py path. if not pathlib.Path(manage_py_path).exists(): @@ -72,31 +89,28 @@ def django_execution_runner(manage_py_path: str, test_ids: List[str], args: List else: env["PYTHONPATH"] = os.fspath(custom_test_runner_dir) - # Build command to run 'python manage.py test'. - command: List[str] = [ - sys.executable, + django_project_dir: pathlib.Path = pathlib.Path(manage_py_path).parent + sys.path.insert(0, os.fspath(django_project_dir)) + print(f"Django project directory: {django_project_dir}") + + manage_spec: ModuleSpec | None = importlib.util.spec_from_file_location( + "manage", manage_py_path + ) + if manage_spec is None or manage_spec.loader is None: + raise VSCodeUnittestError("Error importing manage.py when running Django testing.") + manage_module = importlib.util.module_from_spec(manage_spec) + manage_spec.loader.exec_module(manage_module) + + manage_argv: List[str] = [ manage_py_path, "test", "--testrunner=django_test_runner.CustomExecutionTestRunner", + *args, + *test_ids, ] - # Add any additional arguments to the command provided by the user. - command.extend(args) - # Add the test_ids to the command. - print("Test IDs: ", test_ids) - print("args: ", args) - command.extend(test_ids) - print("Running Django run tests with command: ", command) - subprocess_execution = subprocess.run( - command, - capture_output=True, - text=True, - env=env, - ) - print(subprocess_execution.stderr, file=sys.stderr) - print(subprocess_execution.stdout, file=sys.stdout) - # Zero return code indicates success, 1 indicates test failures, so both are considered successful. - if subprocess_execution.returncode not in (0, 1): - error_msg = "Django test execution process exited with non-zero error code See stderr above for more details." - print(error_msg, file=sys.stderr) + print(f"Django manage.py arguments: {manage_argv}") + + with override_argv(manage_argv), suppress(SystemExit): + manage_module.main() except Exception as e: print(f"Error during Django test execution: {e}", file=sys.stderr) From bb6e9093557bae6d47f87bc24ac4c02130fa9385 Mon Sep 17 00:00:00 2001 From: Danila Grobov Date: Mon, 7 Apr 2025 23:43:40 +0300 Subject: [PATCH 0909/1136] Test Coverage for older django versions (#24968) Related to this issue: https://github.com/microsoft/vscode-python/issues/24199 @mcobalchinisoftfocus Discovered an issue with older django versions, which didn't have the main function in the manage.py https://github.com/microsoft/vscode-python/pull/24927#issuecomment-2779480139 I've fixed this issue by executing the code in manage.py with __name__ set to __main__ instead of relying on main function being there. I've also adjusted the test, so that it would cover this case. --- .../.data/simple_django/old_manage.py | 21 ++++++++++++ .../tests/unittestadapter/test_coverage.py | 7 ++-- .../unittestadapter/django_handler.py | 33 ++++++++----------- 3 files changed, 39 insertions(+), 22 deletions(-) create mode 100755 python_files/tests/unittestadapter/.data/simple_django/old_manage.py diff --git a/python_files/tests/unittestadapter/.data/simple_django/old_manage.py b/python_files/tests/unittestadapter/.data/simple_django/old_manage.py new file mode 100755 index 000000000000..844b98b4edba --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/old_manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +import os +import sys +if __name__ == "__main__": + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/python_files/tests/unittestadapter/test_coverage.py b/python_files/tests/unittestadapter/test_coverage.py index d357d95ad111..8fce53c1854a 100644 --- a/python_files/tests/unittestadapter/test_coverage.py +++ b/python_files/tests/unittestadapter/test_coverage.py @@ -53,11 +53,12 @@ def test_basic_coverage(): assert set(focal_function_coverage.get("lines_missed")) == {6} +@pytest.mark.parametrize("manage_py_file", ["manage.py", "old_manage.py"]) @pytest.mark.timeout(30) -def test_basic_django_coverage(): +def test_basic_django_coverage(manage_py_file): """This test validates that the coverage is correctly calculated for a Django project.""" data_path: pathlib.Path = TEST_DATA_PATH / "simple_django" - manage_py_path: str = os.fsdecode(data_path / "manage.py") + manage_py_path: str = os.fsdecode(data_path / manage_py_file) execution_script: pathlib.Path = python_files_path / "unittestadapter" / "execution.py" test_ids = [ @@ -82,7 +83,7 @@ def test_basic_django_coverage(): assert coverage results = coverage["result"] assert results - assert len(results) == 15 + assert len(results) == 16 polls_views_coverage = results.get(str(data_path / "polls" / "views.py")) assert polls_views_coverage assert polls_views_coverage.get("lines_covered") is not None diff --git a/python_files/unittestadapter/django_handler.py b/python_files/unittestadapter/django_handler.py index 4230c951e162..77c50efc27d0 100644 --- a/python_files/unittestadapter/django_handler.py +++ b/python_files/unittestadapter/django_handler.py @@ -1,17 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import importlib.util import os import pathlib import subprocess import sys from contextlib import contextmanager, suppress -from typing import TYPE_CHECKING, Generator, List - -if TYPE_CHECKING: - from importlib.machinery import ModuleSpec - +from typing import Generator, List script_dir = pathlib.Path(__file__).parent sys.path.append(os.fspath(script_dir)) @@ -75,8 +70,9 @@ def django_discovery_runner(manage_py_path: str, args: List[str]) -> None: def django_execution_runner(manage_py_path: str, test_ids: List[str], args: List[str]) -> None: + manage_path: pathlib.Path = pathlib.Path(manage_py_path) # Attempt a small amount of validation on the manage.py path. - if not pathlib.Path(manage_py_path).exists(): + if not manage_path.exists(): raise VSCodeUnittestError("Error running Django, manage.py path does not exist.") try: @@ -89,20 +85,12 @@ def django_execution_runner(manage_py_path: str, test_ids: List[str], args: List else: env["PYTHONPATH"] = os.fspath(custom_test_runner_dir) - django_project_dir: pathlib.Path = pathlib.Path(manage_py_path).parent + django_project_dir: pathlib.Path = manage_path.parent sys.path.insert(0, os.fspath(django_project_dir)) print(f"Django project directory: {django_project_dir}") - manage_spec: ModuleSpec | None = importlib.util.spec_from_file_location( - "manage", manage_py_path - ) - if manage_spec is None or manage_spec.loader is None: - raise VSCodeUnittestError("Error importing manage.py when running Django testing.") - manage_module = importlib.util.module_from_spec(manage_spec) - manage_spec.loader.exec_module(manage_module) - manage_argv: List[str] = [ - manage_py_path, + str(manage_path), "test", "--testrunner=django_test_runner.CustomExecutionTestRunner", *args, @@ -110,7 +98,14 @@ def django_execution_runner(manage_py_path: str, test_ids: List[str], args: List ] print(f"Django manage.py arguments: {manage_argv}") - with override_argv(manage_argv), suppress(SystemExit): - manage_module.main() + try: + argv_context = override_argv(manage_argv) + suppress_context = suppress(SystemExit) + manage_file = manage_path.open() + with argv_context, suppress_context, manage_file: + manage_code = manage_file.read() + exec(manage_code, {"__name__": "__main__"}) + except OSError as e: + raise VSCodeUnittestError("Error running Django, unable to read manage.py") from e except Exception as e: print(f"Error during Django test execution: {e}", file=sys.stderr) From a9c915228ed04f4568dc582364255f1753d9f856 Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Wed, 9 Apr 2025 16:09:24 -0700 Subject: [PATCH 0910/1136] Update pylance.ts (#24959) Add GDPR tags for telemetry event --------- Co-authored-by: Karthik Nadig --- src/client/telemetry/pylance.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index 42d177488790..d07cc293791d 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -148,6 +148,13 @@ "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } } */ +/* __GDPR__ + "language_server/completion_context_items" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "context" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } +*/ /* __GDPR__ "language_server/exception_intellicode" : { "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, From 8478bf08c2e5600db1ab92d88429495ea396611f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:38:22 +0000 Subject: [PATCH 0911/1136] Bump typing-extensions from 4.12.2 to 4.13.2 (#24977) Bumps [typing-extensions](https://github.com/python/typing_extensions) from 4.12.2 to 4.13.2.
Release notes

Sourced from typing-extensions's releases.

4.13.2

  • Fix TypeError when taking the union of typing_extensions.TypeAliasType and a typing.TypeAliasType on Python 3.12 and 3.13. Patch by Joren Hammudoglu.
  • Backport from CPython PR #132160 to avoid having user arguments shadowed in generated __new__ by @typing_extensions.deprecated. Patch by Victorien Plot.

4.13.1

This is a bugfix release fixing two edge cases that appear on old bugfix releases of CPython.

Bugfixes:

  • Fix regression in 4.13.0 on Python 3.10.2 causing a TypeError when using Concatenate. Patch by Daraan.
  • Fix TypeError when using evaluate_forward_ref on Python 3.10.1-2 and 3.9.8-10. Patch by Daraan.

4.13.0

New features:

  • Add typing_extensions.TypeForm from PEP 747. Patch by Jelle Zijlstra.
  • Add typing_extensions.get_annotations, a backport of inspect.get_annotations that adds features specified by PEP 649. Patches by Jelle Zijlstra and Alex Waygood.
  • Backport evaluate_forward_ref from CPython PR #119891 to evaluate ForwardRefs. Patch by Daraan, backporting a CPython PR by Jelle Zijlstra.

Bugfixes and changed features:

  • Update PEP 728 implementation to a newer version of the PEP. Patch by Jelle Zijlstra.
  • Copy the coroutine status of functions and methods wrapped with @typing_extensions.deprecated. Patch by Sebastian Rittau.
  • Fix bug where TypeAliasType instances could be subscripted even where they were not generic. Patch by Daraan.
  • Fix bug where a subscripted TypeAliasType instance did not have all attributes of the original TypeAliasType instance on older Python versions. Patch by Daraan and Alex Waygood.
  • Fix bug where subscripted TypeAliasType instances (and some other subscripted objects) had wrong parameters if they were directly subscripted with an Unpack object. Patch by Daraan.
  • Backport to Python 3.10 the ability to substitute ... in generic Callable aliases that have a Concatenate special form as their argument. Patch by Daraan.
  • Extended the Concatenate backport for Python 3.8-3.10 to now accept Ellipsis as an argument. Patch by Daraan.
  • Fix backport of get_type_hints to reflect Python 3.11+ behavior which does not add

... (truncated)

Changelog

Sourced from typing-extensions's changelog.

Release 4.13.2 (April 10, 2025)

  • Fix TypeError when taking the union of typing_extensions.TypeAliasType and a typing.TypeAliasType on Python 3.12 and 3.13. Patch by Joren Hammudoglu.
  • Backport from CPython PR #132160 to avoid having user arguments shadowed in generated __new__ by @typing_extensions.deprecated. Patch by Victorien Plot.

Release 4.13.1 (April 3, 2025)

Bugfixes:

  • Fix regression in 4.13.0 on Python 3.10.2 causing a TypeError when using Concatenate. Patch by Daraan.
  • Fix TypeError when using evaluate_forward_ref on Python 3.10.1-2 and 3.9.8-10. Patch by Daraan.

Release 4.13.0 (March 25, 2025)

No user-facing changes since 4.13.0rc1.

Release 4.13.0rc1 (March 18, 2025)

New features:

  • Add typing_extensions.TypeForm from PEP 747. Patch by Jelle Zijlstra.
  • Add typing_extensions.get_annotations, a backport of inspect.get_annotations that adds features specified by PEP 649. Patches by Jelle Zijlstra and Alex Waygood.
  • Backport evaluate_forward_ref from CPython PR #119891 to evaluate ForwardRefs. Patch by Daraan, backporting a CPython PR by Jelle Zijlstra.

Bugfixes and changed features:

  • Update PEP 728 implementation to a newer version of the PEP. Patch by Jelle Zijlstra.
  • Copy the coroutine status of functions and methods wrapped with @typing_extensions.deprecated. Patch by Sebastian Rittau.
  • Fix bug where TypeAliasType instances could be subscripted even where they were not generic. Patch by Daraan.
  • Fix bug where a subscripted TypeAliasType instance did not have all attributes of the original TypeAliasType instance on older Python versions. Patch by Daraan and Alex Waygood.
  • Fix bug where subscripted TypeAliasType instances (and some other subscripted objects) had wrong parameters if they were directly subscripted with an Unpack object. Patch by Daraan.
  • Backport to Python 3.10 the ability to substitute ... in generic Callable

... (truncated)

Commits
  • 4525e9d Prepare release 4.13.2 (#583)
  • 88a0c20 Do not shadow user arguments in generated __new__ by @deprecated (#581)
  • 281d7b0 Add 3rd party tests for litestar (#578)
  • 8092c39 fix TypeAliasType union with typing.TypeAliasType (#575)
  • 45a8847 Prepare release 4.13.1 (#573)
  • f264e58 Move CI to "ubuntu-latest" (round 2) (#570)
  • 5ce0e69 Fix TypeError with evaluate_forward_ref on some 3.10 and 3.9 versions (#558)
  • 304f5cb Add SQLAlchemy to third-party daily tests (#561)
  • ebe2b94 Fix duplicated keywords for typing._ConcatenateGenericAlias in 3.10.2 (#557)
  • 9f93d6f Add intersphinx links for 3.13 typing features (#550)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typing-extensions&package-manager=pip&previous-version=4.12.2&new-version=4.13.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.in b/requirements.in index d0e553cb9a5b..566b012c27d9 100644 --- a/requirements.in +++ b/requirements.in @@ -4,7 +4,7 @@ # 2) uv pip compile --generate-hashes --upgrade requirements.in > requirements.txt # Unittest test adapter -typing-extensions==4.12.2 +typing-extensions==4.13.2 # Fallback env creator for debian microvenv From 2a3566879903724877fb8b9835feba46916156c0 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 14 Apr 2025 10:01:14 -0700 Subject: [PATCH 0912/1136] fix: ensure that we use new extension when available for terminal creation (#24983) fixes https://github.com/microsoft/vscode-python-environments/issues/291 --- src/client/common/application/commands.ts | 1 + src/client/providers/terminalProvider.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 2195fe09aabf..98ea2669d773 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -107,4 +107,5 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu }, ]; ['cursorEnd']: []; + ['python-envs.createTerminal']: [undefined | Uri]; } diff --git a/src/client/providers/terminalProvider.ts b/src/client/providers/terminalProvider.ts index 407a5520b29a..841f479269ac 100644 --- a/src/client/providers/terminalProvider.ts +++ b/src/client/providers/terminalProvider.ts @@ -60,8 +60,13 @@ export class TerminalProvider implements Disposable { @captureTelemetry(EventName.TERMINAL_CREATE, { triggeredBy: 'commandpalette' }) private async onCreateTerminal() { - const terminalService = this.serviceContainer.get(ITerminalServiceFactory); const activeResource = this.activeResourceService.getActiveResource(); + if (useEnvExtension()) { + const commandManager = this.serviceContainer.get(ICommandManager); + await commandManager.executeCommand('python-envs.createTerminal', activeResource); + } + + const terminalService = this.serviceContainer.get(ITerminalServiceFactory); await terminalService.createTerminalService(activeResource, 'Python').show(false); } } From f319416345f84e4fb4dd577a0761cbceac039da8 Mon Sep 17 00:00:00 2001 From: Erik De Bonte Date: Mon, 14 Apr 2025 11:14:58 -0700 Subject: [PATCH 0913/1136] Add metadata for Pylance's documentcolor_slow telemetry event (#24979) --- src/client/telemetry/pylance.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index d07cc293791d..e299947e1456 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -155,6 +155,25 @@ "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ +/* __GDPR__ + "language_server/documentcolor_slow" : { + "bindcallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "bindtime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "parsetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfilecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "readfiletime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "resolvetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizecallcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "tokenizetime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "totaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevalcount" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typeevaltime" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ /* __GDPR__ "language_server/exception_intellicode" : { "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, From 4b7650e0428b0107ba499835754eb5908d0017bd Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:04:06 -0700 Subject: [PATCH 0914/1136] support branch coverage for testing (#24980) fixes https://github.com/microsoft/vscode-python/issues/24976 --- build/test-requirements.txt | 1 - .../tests/pytestadapter/test_coverage.py | 31 +++++++++++----- .../tests/unittestadapter/test_coverage.py | 26 ++++++++++---- python_files/unittestadapter/execution.py | 22 +++++++++++- python_files/unittestadapter/pvsc_utils.py | 2 ++ python_files/vscode_pytest/__init__.py | 35 +++++++++++++++++-- .../vscode_pytest/run_pytest_script.py | 2 +- .../testController/common/resultResolver.ts | 21 +++++++++-- .../testing/testController/common/types.ts | 2 ++ .../testing/common/testingAdapter.test.ts | 4 +++ 10 files changed, 122 insertions(+), 24 deletions(-) diff --git a/build/test-requirements.txt b/build/test-requirements.txt index 097b18256764..df9fd2b08c6e 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -28,7 +28,6 @@ namedpipe; platform_system == "Windows" # typing for Django files django-stubs -# for coverage coverage pytest-cov pytest-json diff --git a/python_files/tests/pytestadapter/test_coverage.py b/python_files/tests/pytestadapter/test_coverage.py index d0f802a23672..d2d276172a8d 100644 --- a/python_files/tests/pytestadapter/test_coverage.py +++ b/python_files/tests/pytestadapter/test_coverage.py @@ -5,7 +5,9 @@ import pathlib import sys +import coverage import pytest +from packaging.version import Version script_dir = pathlib.Path(__file__).parent.parent sys.path.append(os.fspath(script_dir)) @@ -34,9 +36,9 @@ def test_simple_pytest_coverage(): cov_folder_path = TEST_DATA_PATH / "coverage_gen" actual = runner_with_cwd_env(args, cov_folder_path, env_add) assert actual - coverage = actual[-1] - assert coverage - results = coverage["result"] + cov = actual[-1] + assert cov + results = cov["result"] assert results assert len(results) == 3 focal_function_coverage = results.get(os.fspath(TEST_DATA_PATH / "coverage_gen" / "reverse.py")) @@ -46,6 +48,12 @@ def test_simple_pytest_coverage(): assert set(focal_function_coverage.get("lines_covered")) == {4, 5, 7, 9, 10, 11, 12, 13, 14, 17} assert len(set(focal_function_coverage.get("lines_missed"))) >= 3 + coverage_version = Version(coverage.__version__) + # only include check for branches if the version is >= 7.7.0 + if coverage_version >= Version("7.7.0"): + assert focal_function_coverage.get("executed_branches") == 4 + assert focal_function_coverage.get("total_branches") == 6 + coverage_gen_file_path = TEST_DATA_PATH / "coverage_gen" / "coverage.json" @@ -77,9 +85,9 @@ def test_coverage_gen_report(cleanup_coverage_gen_file): # noqa: ARG001 print("cov_folder_path", cov_folder_path) actual = runner_with_cwd_env(args, cov_folder_path, env_add) assert actual - coverage = actual[-1] - assert coverage - results = coverage["result"] + cov = actual[-1] + assert cov + results = cov["result"] assert results assert len(results) == 3 focal_function_coverage = results.get(os.fspath(TEST_DATA_PATH / "coverage_gen" / "reverse.py")) @@ -88,6 +96,11 @@ def test_coverage_gen_report(cleanup_coverage_gen_file): # noqa: ARG001 assert focal_function_coverage.get("lines_missed") is not None assert set(focal_function_coverage.get("lines_covered")) == {4, 5, 7, 9, 10, 11, 12, 13, 14, 17} assert set(focal_function_coverage.get("lines_missed")) == {18, 19, 6} + coverage_version = Version(coverage.__version__) + # only include check for branches if the version is >= 7.7.0 + if coverage_version >= Version("7.7.0"): + assert focal_function_coverage.get("executed_branches") == 4 + assert focal_function_coverage.get("total_branches") == 6 # assert that the coverage file was created at the right path assert os.path.exists(coverage_gen_file_path) # noqa: PTH110 @@ -123,9 +136,9 @@ def test_coverage_w_omit_config(): actual = runner_with_cwd_env([], cov_folder_path, env_add) assert actual print("actual", json.dumps(actual, indent=2)) - coverage = actual[-1] - assert coverage - results = coverage["result"] + cov = actual[-1] + assert cov + results = cov["result"] assert results # assert one file is reported and one file (as specified in pyproject.toml) is omitted assert len(results) == 1 diff --git a/python_files/tests/unittestadapter/test_coverage.py b/python_files/tests/unittestadapter/test_coverage.py index 8fce53c1854a..76fdfec43376 100644 --- a/python_files/tests/unittestadapter/test_coverage.py +++ b/python_files/tests/unittestadapter/test_coverage.py @@ -8,7 +8,9 @@ import pathlib import sys +import coverage import pytest +from packaging.version import Version sys.path.append(os.fspath(pathlib.Path(__file__).parent)) @@ -40,9 +42,9 @@ def test_basic_coverage(): ) assert actual - coverage = actual[-1] - assert coverage - results = coverage["result"] + cov = actual[-1] + assert cov + results = cov["result"] assert results assert len(results) == 3 focal_function_coverage = results.get(os.fspath(TEST_DATA_PATH / "coverage_ex" / "reverse.py")) @@ -51,6 +53,11 @@ def test_basic_coverage(): assert focal_function_coverage.get("lines_missed") is not None assert set(focal_function_coverage.get("lines_covered")) == {4, 5, 7, 9, 10, 11, 12, 13, 14} assert set(focal_function_coverage.get("lines_missed")) == {6} + coverage_version = Version(coverage.__version__) + # only include check for branches if the version is >= 7.7.0 + if coverage_version >= Version("7.7.0"): + assert focal_function_coverage.get("executed_branches") == 3 + assert focal_function_coverage.get("total_branches") == 4 @pytest.mark.parametrize("manage_py_file", ["manage.py", "old_manage.py"]) @@ -79,9 +86,9 @@ def test_basic_django_coverage(manage_py_file): ) assert actual - coverage = actual[-1] - assert coverage - results = coverage["result"] + cov = actual[-1] + assert cov + results = cov["result"] assert results assert len(results) == 16 polls_views_coverage = results.get(str(data_path / "polls" / "views.py")) @@ -90,3 +97,10 @@ def test_basic_django_coverage(manage_py_file): assert polls_views_coverage.get("lines_missed") is not None assert set(polls_views_coverage.get("lines_covered")) == {3, 4, 6} assert set(polls_views_coverage.get("lines_missed")) == {7} + + model_cov = results.get(str(data_path / "polls" / "models.py")) + coverage_version = Version(coverage.__version__) + # only include check for branches if the version is >= 7.7.0 + if coverage_version >= Version("7.7.0"): + assert model_cov.get("executed_branches") == 1 + assert model_cov.get("total_branches") == 2 diff --git a/python_files/unittestadapter/execution.py b/python_files/unittestadapter/execution.py index 644b233fc530..8df2f279aa71 100644 --- a/python_files/unittestadapter/execution.py +++ b/python_files/unittestadapter/execution.py @@ -12,6 +12,8 @@ from types import TracebackType from typing import Dict, List, Optional, Set, Tuple, Type, Union +from packaging.version import Version + # Adds the scripts directory to the PATH as a workaround for enabling shell for test execution. path_var_name = "PATH" if "PATH" in os.environ else "Path" os.environ[path_var_name] = ( @@ -316,6 +318,7 @@ def send_run_data(raw_data, test_run_pipe): # For unittest COVERAGE_ENABLED is to the root of the workspace so correct data is collected cov = None is_coverage_run = os.environ.get("COVERAGE_ENABLED") is not None + include_branches = False if is_coverage_run: print( "COVERAGE_ENABLED env var set, starting coverage. workspace_root used as parent dir:", @@ -323,6 +326,11 @@ def send_run_data(raw_data, test_run_pipe): ) import coverage + coverage_version = Version(coverage.__version__) + # only include branches if coverage version is 7.7.0 or greater (as this was when the api saves) + if coverage_version >= Version("7.7.0"): + include_branches = True + source_ar: List[str] = [] if workspace_root: source_ar.append(workspace_root) @@ -330,7 +338,9 @@ def send_run_data(raw_data, test_run_pipe): source_ar.append(top_level_dir) if start_dir: source_ar.append(os.path.abspath(start_dir)) # noqa: PTH100 - cov = coverage.Coverage(branch=True, source=source_ar) # is at least 1 of these required?? + cov = coverage.Coverage( + branch=include_branches, source=source_ar + ) # is at least 1 of these required?? cov.start() # If no error occurred, we will have test ids to run. @@ -362,12 +372,22 @@ def send_run_data(raw_data, test_run_pipe): file_coverage_map: Dict[str, FileCoverageInfo] = {} for file in file_set: analysis = cov.analysis2(file) + taken_file_branches = 0 + total_file_branches = -1 + + if include_branches: + branch_stats: dict[int, tuple[int, int]] = cov.branch_stats(file) + total_file_branches = sum([total_exits for total_exits, _ in branch_stats.values()]) + taken_file_branches = sum([taken_exits for _, taken_exits in branch_stats.values()]) + lines_executable = {int(line_no) for line_no in analysis[1]} lines_missed = {int(line_no) for line_no in analysis[3]} lines_covered = lines_executable - lines_missed file_info: FileCoverageInfo = { "lines_covered": list(lines_covered), # list of int "lines_missed": list(lines_missed), # list of int + "executed_branches": taken_file_branches, + "total_branches": total_file_branches, } file_coverage_map[file] = file_info diff --git a/python_files/unittestadapter/pvsc_utils.py b/python_files/unittestadapter/pvsc_utils.py index 4d1cbfb5e110..017bad38966a 100644 --- a/python_files/unittestadapter/pvsc_utils.py +++ b/python_files/unittestadapter/pvsc_utils.py @@ -75,6 +75,8 @@ class ExecutionPayloadDict(TypedDict): class FileCoverageInfo(TypedDict): lines_covered: List[int] lines_missed: List[int] + executed_branches: int + total_branches: int class CoveragePayloadDict(Dict): diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index bf9466991383..649e5bc59058 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -13,6 +13,7 @@ from typing import TYPE_CHECKING, Any, Dict, Generator, Literal, TypedDict import pytest +from packaging.version import Version if TYPE_CHECKING: from pluggy import Result @@ -61,6 +62,7 @@ def __init__(self, message): collected_tests_so_far = [] TEST_RUN_PIPE = os.getenv("TEST_RUN_PIPE") SYMLINK_PATH = None +INCLUDE_BRANCHES = False def pytest_load_initial_conftests(early_config, parser, args): # noqa: ARG001 @@ -70,6 +72,9 @@ def pytest_load_initial_conftests(early_config, parser, args): # noqa: ARG001 raise VSCodePytestError( "\n \nERROR: pytest-cov is not installed, please install this before running pytest with coverage as pytest-cov is required. \n" ) + if "--cov-branch" in args: + global INCLUDE_BRANCHES + INCLUDE_BRANCHES = True global TEST_RUN_PIPE TEST_RUN_PIPE = os.getenv("TEST_RUN_PIPE") @@ -363,6 +368,8 @@ def check_skipped_condition(item): class FileCoverageInfo(TypedDict): lines_covered: list[int] lines_missed: list[int] + executed_branches: int + total_branches: int def pytest_sessionfinish(session, exitstatus): @@ -436,6 +443,15 @@ def pytest_sessionfinish(session, exitstatus): # load the report and build the json result to return import coverage + coverage_version = Version(coverage.__version__) + global INCLUDE_BRANCHES + # only include branches if coverage version is 7.7.0 or greater (as this was when the api saves) + if coverage_version < Version("7.7.0") and INCLUDE_BRANCHES: + print( + "Plugin warning[vscode-pytest]: Branch coverage not supported in this coverage versions < 7.7.0. Please upgrade coverage package if you would like to see branch coverage." + ) + INCLUDE_BRANCHES = False + try: from coverage.exceptions import NoSource except ImportError: @@ -448,9 +464,8 @@ def pytest_sessionfinish(session, exitstatus): file_coverage_map: dict[str, FileCoverageInfo] = {} # remove files omitted per coverage report config if any - omit_files = cov.config.report_omit - if omit_files: - print("Plugin info[vscode-pytest]: Omit files/rules: ", omit_files) + omit_files: list[str] | None = cov.config.report_omit + if omit_files is not None: for pattern in omit_files: for file in list(file_set): if pathlib.Path(file).match(pattern): @@ -459,6 +474,18 @@ def pytest_sessionfinish(session, exitstatus): for file in file_set: try: analysis = cov.analysis2(file) + taken_file_branches = 0 + total_file_branches = -1 + + if INCLUDE_BRANCHES: + branch_stats: dict[int, tuple[int, int]] = cov.branch_stats(file) + total_file_branches = sum( + [total_exits for total_exits, _ in branch_stats.values()] + ) + taken_file_branches = sum( + [taken_exits for _, taken_exits in branch_stats.values()] + ) + except NoSource: # as per issue 24308 this best way to handle this edge case continue @@ -473,6 +500,8 @@ def pytest_sessionfinish(session, exitstatus): file_info: FileCoverageInfo = { "lines_covered": list(lines_covered), # list of int "lines_missed": list(lines_missed), # list of int + "executed_branches": taken_file_branches, + "total_branches": total_file_branches, } # convert relative path to absolute path if not pathlib.Path(file).is_absolute(): diff --git a/python_files/vscode_pytest/run_pytest_script.py b/python_files/vscode_pytest/run_pytest_script.py index 1abfb8b27004..c0f5114b375c 100644 --- a/python_files/vscode_pytest/run_pytest_script.py +++ b/python_files/vscode_pytest/run_pytest_script.py @@ -47,7 +47,7 @@ def run_pytest(args): coverage_enabled = True break if not coverage_enabled: - args = [*args, "--cov=."] + args = [*args, "--cov=.", "--cov-branch"] run_test_ids_pipe = os.environ.get("RUN_TEST_IDS_PIPE") if run_test_ids_pipe: diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 80e57edbabd2..82856627e0c9 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -17,7 +17,13 @@ import { Range, } from 'vscode'; import * as util from 'util'; -import { CoveragePayload, DiscoveredTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; +import { + CoveragePayload, + DiscoveredTestPayload, + ExecutionTestPayload, + FileCoverageMetrics, + ITestResultResolver, +} from './types'; import { TestProvider } from '../../types'; import { traceError, traceVerbose } from '../../../logging'; import { Testing } from '../../../common/utils/localize'; @@ -120,16 +126,25 @@ export class PythonResultResolver implements ITestResultResolver { } for (const [key, value] of Object.entries(payload.result)) { const fileNameStr = key; - const fileCoverageMetrics = value; + const fileCoverageMetrics: FileCoverageMetrics = value; const linesCovered = fileCoverageMetrics.lines_covered ? fileCoverageMetrics.lines_covered : []; // undefined if no lines covered const linesMissed = fileCoverageMetrics.lines_missed ? fileCoverageMetrics.lines_missed : []; // undefined if no lines missed + const executedBranches = fileCoverageMetrics.executed_branches; + const totalBranches = fileCoverageMetrics.total_branches; const lineCoverageCount = new TestCoverageCount( linesCovered.length, linesCovered.length + linesMissed.length, ); + let fileCoverage: FileCoverage; const uri = Uri.file(fileNameStr); - const fileCoverage = new FileCoverage(uri, lineCoverageCount); + if (totalBranches === -1) { + // branch coverage was not enabled and should not be displayed + fileCoverage = new FileCoverage(uri, lineCoverageCount); + } else { + const branchCoverageCount = new TestCoverageCount(executedBranches, totalBranches); + fileCoverage = new FileCoverage(uri, lineCoverageCount, branchCoverageCount); + } runInstance.addCoverage(fileCoverage); // create detailed coverage array for each file (only line coverage on detailed, not branch) diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 7139788a8177..282379abdb85 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -222,6 +222,8 @@ export type FileCoverageMetrics = { lines_covered: number[]; // eslint-disable-next-line camelcase lines_missed: number[]; + executed_branches: number; + total_branches: number; }; export type ExecutionTestPayload = { diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 834bccbd905f..dcd78dc23dba 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -711,6 +711,8 @@ suite('End to End Tests: test adapters', () => { // since only one test was run, the other test in the same file will have missed coverage lines assert.strictEqual(simpleFileCov.lines_covered.length, 3, 'Expected 1 line to be covered in even.py'); assert.strictEqual(simpleFileCov.lines_missed.length, 1, 'Expected 3 lines to be missed in even.py'); + assert.strictEqual(simpleFileCov.executed_branches, 1, 'Expected 1 branch to be executed in even.py'); + assert.strictEqual(simpleFileCov.total_branches, 2, 'Expected 2 branches in even.py'); return Promise.resolve(); }; @@ -759,6 +761,8 @@ suite('End to End Tests: test adapters', () => { // since only one test was run, the other test in the same file will have missed coverage lines assert.strictEqual(simpleFileCov.lines_covered.length, 3, 'Expected 1 line to be covered in even.py'); assert.strictEqual(simpleFileCov.lines_missed.length, 1, 'Expected 3 lines to be missed in even.py'); + assert.strictEqual(simpleFileCov.executed_branches, 1, 'Expected 1 branch to be executed in even.py'); + assert.strictEqual(simpleFileCov.total_branches, 2, 'Expected 2 branches in even.py'); return Promise.resolve(); }; From c3f601dfe1caed2b5547b602ed0f943551a9fff5 Mon Sep 17 00:00:00 2001 From: Heejae Chang <1333179+heejaechang@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:18:29 -0700 Subject: [PATCH 0915/1136] Added some pylance telemetry (#24984) added some pylance specific telemetries --- src/client/telemetry/pylance.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index e299947e1456..2b50e65f9ee9 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -378,6 +378,9 @@ "language_server/settings" : { "addimportexactmatchonly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "aicodeactionsimplementabstractclasses" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "aiCodeActionsGenerateDocstring" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "aiCodeActionsGenerateSymbols" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "aiCodeActionsConvertFormatString" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "autoimportcompletions" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "autosearchpaths" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "callArgumentNameInlayHints" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, From 6608f9ab9c836fdfe4fa3b9596ecf54e7cc2bd78 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 17 Apr 2025 10:53:43 -0700 Subject: [PATCH 0916/1136] fix: let Python Envs extension handle missing `python` in conda envs (#24986) fixes https://github.com/microsoft/vscode-python-environments/issues/289 --- src/client/envExt/api.legacy.ts | 34 ++------------------ src/client/interpreter/interpreterService.ts | 5 +-- 2 files changed, 3 insertions(+), 36 deletions(-) diff --git a/src/client/envExt/api.legacy.ts b/src/client/envExt/api.legacy.ts index 7546a429c76a..fb01e73bdfcf 100644 --- a/src/client/envExt/api.legacy.ts +++ b/src/client/envExt/api.legacy.ts @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Terminal, Uri, WorkspaceFolder } from 'vscode'; +import { Terminal, Uri } from 'vscode'; import { getEnvExtApi, getEnvironment } from './api.internal'; import { EnvironmentType, PythonEnvironment as PythonEnvironmentLegacy } from '../pythonEnvironments/info'; import { PythonEnvironment, PythonTerminalOptions } from './types'; import { Architecture } from '../common/utils/platform'; import { parseVersion } from '../pythonEnvironments/base/info/pythonVersion'; import { PythonEnvType } from '../pythonEnvironments/base/info'; -import { traceError, traceInfo } from '../logging'; +import { traceError } from '../logging'; import { reportActiveInterpreterChanged } from '../environmentApi'; import { getWorkspaceFolder, getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; @@ -120,36 +120,6 @@ export async function getActiveInterpreterLegacy(resource?: Uri): Promise void, -): Promise { - const api = await getEnvExtApi(); - const pythonEnv = await api.resolveEnvironment(Uri.file(pythonPath)); - if (!pythonEnv) { - traceError(`EnvExt: Failed to resolve environment for ${pythonPath}`); - return; - } - - const envType = toEnvironmentType(pythonEnv); - if (envType === EnvironmentType.Conda) { - const packages = await api.getPackages(pythonEnv); - if (packages && packages.length > 0 && packages.some((pkg) => pkg.name.toLowerCase() === 'python')) { - return; - } - traceInfo(`EnvExt: Python not found in ${envType} environment ${pythonPath}`); - traceInfo(`EnvExt: Installing Python in ${envType} environment ${pythonPath}`); - await api.installPackages(pythonEnv, ['python']); - previousEnvMap.set(workspaceFolder?.uri.fsPath || '', pythonEnv); - reportActiveInterpreterChanged({ - path: pythonPath, - resource: workspaceFolder, - }); - callback(); - } -} - export async function setInterpreterLegacy(pythonPath: string, uri: Uri | undefined): Promise { const api = await getEnvExtApi(); const pythonEnv = await api.resolveEnvironment(Uri.file(pythonPath)); diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 3a1aaed312ff..ad06fd7d051d 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -45,7 +45,7 @@ import { } from '../pythonEnvironments/base/locator'; import { sleep } from '../common/utils/async'; import { useEnvExtension } from '../envExt/api.internal'; -import { ensureEnvironmentContainsPythonLegacy, getActiveInterpreterLegacy } from '../envExt/api.legacy'; +import { getActiveInterpreterLegacy } from '../envExt/api.legacy'; type StoredPythonEnvironment = PythonEnvironment & { store?: boolean }; @@ -290,9 +290,6 @@ export class InterpreterService implements Disposable, IInterpreterService { @cache(-1, true) private async ensureEnvironmentContainsPython(pythonPath: string, workspaceFolder: WorkspaceFolder | undefined) { if (useEnvExtension()) { - await ensureEnvironmentContainsPythonLegacy(pythonPath, workspaceFolder, () => { - this.didChangeInterpreterEmitter.fire(workspaceFolder?.uri); - }); return; } From cf894bb91dd146c54dbcc6d2e446d267263b86fa Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 17 Apr 2025 11:15:49 -0700 Subject: [PATCH 0917/1136] Revert "move clear envCollection to after await (#24921)" (#24988) This reverts commit 6a60c92b8eef9a4681497ff674ac4f1a70a2e376. fixes https://github.com/microsoft/vscode-python/issues/24982 --- src/client/terminals/envCollectionActivation/service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts index 880053b03d1d..43b8ceeb8e06 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -221,10 +221,9 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ env.PS1 = await this.getPS1(shell, resource, env); const defaultPrependOptions = await this.getPrependOptions(); - const deactivate = await this.terminalDeactivateService.getScriptLocation(shell, resource); // Clear any previously set env vars from collection envVarCollection.clear(); - + const deactivate = await this.terminalDeactivateService.getScriptLocation(shell, resource); Object.keys(env).forEach((key) => { if (shouldSkip(key)) { return; From cfc65abef21f0e19309cff0a377ed8d4890f5b3c Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 18 Apr 2025 13:02:25 -0700 Subject: [PATCH 0918/1136] fix: terminal error notification in untrusted workspace (#24993) Fixes https://github.com/microsoft/vscode-python/issues/24770 --- .../shellIntegrationService.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/client/terminals/envCollectionActivation/shellIntegrationService.ts b/src/client/terminals/envCollectionActivation/shellIntegrationService.ts index 71cfb18dd437..92bb98029892 100644 --- a/src/client/terminals/envCollectionActivation/shellIntegrationService.ts +++ b/src/client/terminals/envCollectionActivation/shellIntegrationService.ts @@ -15,6 +15,7 @@ import { IDisposableRegistry, IPersistentStateFactory } from '../../common/types import { sleep } from '../../common/utils/async'; import { traceError, traceVerbose } from '../../logging'; import { IShellIntegrationDetectionService } from '../types'; +import { isTrusted } from '../../common/vscodeApis/workspaceApis'; /** * This is a list of shells which support shell integration: @@ -151,10 +152,12 @@ export class ShellIntegrationDetectionService implements IShellIntegrationDetect * Creates a dummy terminal so that we are guaranteed a data write event for this shell type. */ private createDummyHiddenTerminal(shell: string) { - this.terminalManager.createTerminal({ - shellPath: shell, - hideFromUser: true, - }); + if (isTrusted()) { + this.terminalManager.createTerminal({ + shellPath: shell, + hideFromUser: true, + }); + } } } From ee8f2300214a848cde1e671aaf82004b20fe86ff Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 21 Apr 2025 20:17:06 -0700 Subject: [PATCH 0919/1136] chore: update required dependencies (#24998) --- .config/CredScanSuppressions.json | 12 ------------ .github/actions/build-vsix/action.yml | 4 ++-- .github/workflows/build.yml | 2 +- .github/workflows/pr-check.yml | 2 +- build/azure-pipeline.pre-release.yml | 2 +- build/azure-pipeline.stable.yml | 2 +- build/ci/conda_env_1.yml | 2 +- build/ci/conda_env_2.yml | 2 +- .../jedilsp_requirements/requirements.in | 2 +- requirements.in | 2 +- requirements.txt | Bin 8096 -> 3999 bytes 11 files changed, 10 insertions(+), 22 deletions(-) delete mode 100644 .config/CredScanSuppressions.json diff --git a/.config/CredScanSuppressions.json b/.config/CredScanSuppressions.json deleted file mode 100644 index cc237f71d86c..000000000000 --- a/.config/CredScanSuppressions.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "tool": "Credential Scanner", - "suppressions": [ - { - "file": [ - "file:///mnt/vss/_work/1/a/extension/extension/.nox/install_python_libs/lib/python3.8/site-packages/setuptools/_distutils/command%5Cregister.py", - "file:///mnt/vss/_work/1/b/extension/extension/.nox/install_python_libs/lib/python3.8/site-packages/setuptools/_distutils/command%5Cregister.py" - ], - "_justification": "These are not real passwords. For documentation purposes only." - } - ] - } diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index 929ecb31a6d3..c2515247de97 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -31,10 +31,10 @@ runs: uses: dtolnay/rust-toolchain@stable # Jedi LS depends on dataclasses which is not in the stdlib in Python 3.7. - - name: Use Python 3.8 for JediLSP + - name: Use Python 3.9 for JediLSP uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 cache: 'pip' cache-dependency-path: | requirements.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 53ee0f003668..4b65b91a2cdf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -165,7 +165,7 @@ jobs: # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. os: [ubuntu-latest, windows-latest] # Run the tests on the oldest and most recent versions of Python. - python: ['3.8', '3.x', '3.13-dev'] + python: ['3.9', '3.x', '3.13'] steps: - name: Checkout diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index b6bdaa8e250b..4b1ea54618b8 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -147,7 +147,7 @@ jobs: # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. os: [ubuntu-latest, windows-latest] # Run the tests on the oldest and most recent versions of Python. - python: ['3.8', '3.x', '3.13-dev'] # run for 3 pytest versions, most recent stable, oldest version supported and pre-release + python: ['3.9', '3.x', '3.13'] # run for 3 pytest versions, most recent stable, oldest version supported and pre-release pytest-version: ['pytest', 'pytest@pre-release', 'pytest==6.2.0'] steps: diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index 3236b43d0098..6c6600365529 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -71,7 +71,7 @@ extends: - task: UsePythonVersion@0 inputs: - versionSpec: '3.8' + versionSpec: '3.9' addToPath: true architecture: 'x64' displayName: Select Python version diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index f9a37c5a9ec5..cae56854118e 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -65,7 +65,7 @@ extends: - task: UsePythonVersion@0 inputs: - versionSpec: '3.8' + versionSpec: '3.9' addToPath: true architecture: 'x64' displayName: Select Python version diff --git a/build/ci/conda_env_1.yml b/build/ci/conda_env_1.yml index e9d08d0820a4..4f9ceefd27fb 100644 --- a/build/ci/conda_env_1.yml +++ b/build/ci/conda_env_1.yml @@ -1,4 +1,4 @@ name: conda_env_1 dependencies: - - python=3.8 + - python=3.9 - pip diff --git a/build/ci/conda_env_2.yml b/build/ci/conda_env_2.yml index 80b946c3cc14..af9d7a46ba3e 100644 --- a/build/ci/conda_env_2.yml +++ b/build/ci/conda_env_2.yml @@ -1,4 +1,4 @@ name: conda_env_2 dependencies: - - python=3.8 + - python=3.9 - pip diff --git a/python_files/jedilsp_requirements/requirements.in b/python_files/jedilsp_requirements/requirements.in index 8bafda64375e..74cfb91585eb 100644 --- a/python_files/jedilsp_requirements/requirements.in +++ b/python_files/jedilsp_requirements/requirements.in @@ -2,7 +2,7 @@ # To update requirements.txt, run the following commands. # Use Python 3.8 when creating the environment or using pip-tools # 1) Install `uv` https://docs.astral.sh/uv/getting-started/installation/ -# 2) uv pip compile --generate-hashes --upgrade python_files\jedilsp_requirements\requirements.in > python_files\jedilsp_requirements\requirements.txt +# 2) uv pip compile --generate-hashes --upgrade python_files\jedilsp_requirements\requirements.in -o python_files\jedilsp_requirements\requirements.txt jedi-language-server>=0.34.3 pygls>=0.10.3 diff --git a/requirements.in b/requirements.in index 566b012c27d9..a1e2243c553e 100644 --- a/requirements.in +++ b/requirements.in @@ -1,7 +1,7 @@ # This file is used to generate requirements.txt. # To update requirements.txt, run the following commands. # 1) Install `uv` https://docs.astral.sh/uv/getting-started/installation/ -# 2) uv pip compile --generate-hashes --upgrade requirements.in > requirements.txt +# 2) uv pip compile --generate-hashes --upgrade requirements.in -o requirements.txt # Unittest test adapter typing-extensions==4.13.2 diff --git a/requirements.txt b/requirements.txt index 464d3abb13155bac6622e863977b56b33bdf55b1..3983d5414c5446d530896bb884ec1701fd492c9d 100644 GIT binary patch literal 3999 zcmai%%Wfq%4uF28k2AE*Vi5G^CU1o{o zf76%CKc221mwo-_m*4c`qTg??U;V}3_2%m`|G0erartr8%k9Z8`}Nzm*Wa!$UoZCh z{H!nQm%A?)zIf#A`gZ*C?I?49|K9@lPx|rXAD4Ik_51bRpZ(?b@o;^)+`s<$SGV79 zch~2)*Z14E>%4#Vn=ZZS<70oQ58?99yW=JIKZ`wnJZUWT7m3kuAEbE(MDgjwYe+P(y-(17Hrap_wB9573Ur+9Y82$ z49+rTTi$=&*6sEA+m)6c;-^SVsv+dHdd#|9f&jecGp4|pqwXZbixytWeCq_9tAeA% zctz`JPyE~T81wR~pxMIMGU%%>%^uqfqD*n57}84BcFtZsSkbw7bgk2(vDeLLAq@qzNjqp#pzNM79L9+ z)&=NI>-E@GP#rt7rI|*qq0Mo~m%40-x?4LxX=Oa(f>dRS1%_Ko7&eCFomzw!NpjgN zhtZB^zK?A#51VoCbM|NLyLtvvY`1u|TAHC|JC`9{-NpU=5HQzs%pgTDyptmb-t zQcqbz2KP-7;*@N5?qTz6GoEIHPgfc^;X;D*U4IQD`y995FhDTP_+nzO7 zn2-XR)Ke2dbp6~g+BV5PqK8!>f7Y6;)!f)L3g1hsBga*lNfC`8ct}?!hB0|+)!4{EAc^^XtAKkZ9YG#rmo;x03wZxCu656XFjdH zYaRy|$1H(BGQvWA`VmZQpl&>Sj#C9qoP+?9n|E*$HM9i)NF`0tx1s~M;W3@yZp|3e0K@WYS`T-o2U29@HDJ}FtbReZU6^%$_iF0jj80NW|10qMG zts9uGI}(VTO_wStdruXE&pqt4ELAZdnqmXM1qI8oe-gov@O&Uq*gfECh{+-WDbBHb zh>EhAtr+|%oY@!)6_70BORe}8BuLxiS6W2jLBi6C;~K4bjBMAq))s6 zd59eV_hRS9XDg#b%NmNGASJN_sN+sOfN(}5b)`N72}(o8d68*oh3DlW6M1xq{$gP3M6EQhwhy-4#;gIh#P=L*M#Rblbf;Fhf zwrcO5Fh6Bq=zi+Hj@ST!GJ3lD+5+SosEBl@2tWI#fRB_p1K}4OpPqDQNdq`eV+ft>8!8�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 Date: Mon, 21 Apr 2025 22:15:01 -0700 Subject: [PATCH 0920/1136] feat: update to `jedi-language-server` v0.45.0 (#24997) --- .../jedilsp_requirements/requirements.in | 2 +- .../jedilsp_requirements/requirements.txt | Bin 5178 -> 3051 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/python_files/jedilsp_requirements/requirements.in b/python_files/jedilsp_requirements/requirements.in index 74cfb91585eb..794e9c8ea686 100644 --- a/python_files/jedilsp_requirements/requirements.in +++ b/python_files/jedilsp_requirements/requirements.in @@ -1,6 +1,6 @@ # This file is used to generate requirements.txt. # To update requirements.txt, run the following commands. -# Use Python 3.8 when creating the environment or using pip-tools +# Use Python 3.9 when creating the environment or using pip-tools # 1) Install `uv` https://docs.astral.sh/uv/getting-started/installation/ # 2) uv pip compile --generate-hashes --upgrade python_files\jedilsp_requirements\requirements.in -o python_files\jedilsp_requirements\requirements.txt diff --git a/python_files/jedilsp_requirements/requirements.txt b/python_files/jedilsp_requirements/requirements.txt index ef4c7e1de6aeffbaaed7ea0e22fda0020304a00a..0fc5cd76810f99a403168857dd016b2ff5852c63 100644 GIT binary patch literal 3051 zcmbVO%W@;h4ZQO!I%3Y`7Jzzgg#KcW6S09pp(NT8Nt2w{e!a8hS&w$ya)iwj*=kiG z6NyZGSblpt-j;cM_T`J-7X5TTKl;h9diSyPZ_B68%jcsm_a|TG`T6<$<#>8r?ELbg zr}69V!-5Z9xg0O+!^?Wie*fnI`zO6U`E9v;yFZ;zzpt0vKK{+e@%eW7{p$btbiDeD zpYFGhZ+{$)r)7U$4j=!=f4hIZ@AQ7Z-X0z}9pn&}kGr+J{c9Nyw{Lm3oYkM;uWJ z_q7mOk;KIU(V!q<4(T)Mr2bmxhn48>-LcW! z+2Q~erpA_fF}9F6Ie?8yLZfPxnKnSKTG&79>G6|3`u^tE&wky4ytZ+b+U48h^KCcI zcDrAp!~Oldf6?pT$N9_Y;UOF-Zx#;T`%Ho7m?4H9MxX|uTFgj$uRvedCxK$Dg{h#` zRaHENk}{XgLnp9WWJznUoA4~bl)YPNv(%7+G?j=}>yS!DJ*!EquMp7-M zrM`J+L>GO`V!242I(ux8YvdM6$b*!WXB=dCKnE#t?wPIksG6jS*&BG@m3(};oUixi zW8c5{U5DQF;h`S#fi~|CthGrCMotNh8x}BPxpT5Eo;fG9F}4^&vQ`Gv94ngKTY==E zz0E^~LXR|1Z;Y7Om~xwKt#(5ERVueM+H{;5IfWLdj+SJUNy%!e;SH!w4er)Xz6!Cu z)2moY7$(CbX2YfB+yg{Y#=G_5|FabArrOdR6L zN`?i9Rk5CGEpg~9++(Q`*W!lM;Sbpd+@ZaJwJF7|hbzjM-tkx%pLeNY)?!kc;12RA ztzrJ^z_cXqWq?s`wAx@dw>flVG+Q!nD{{6uN6uVJ5sq`>3&Yo7;;kf#R3iqsLleq_ zEef>&eTUjZrYWtwLAk&FC6azw0sb6Gzx)(QyB`h=>kW*2c}Kbi0kT+gSUXQf;BcpC zSxSO9BV-0ja%0IhQpTvZJYq`(&(sWC-IT6bGD=`;a13Hpdyyz1iZoAzOJM0#71I!B z8DUbZ;M0Q*hTqfpf&TlqW!rZC>a-WV-p*^Q>mhEIrQki6pdh5tP~3)iuo}?zf{M|6 zl2YAadxN+mRt2*aa+DEeQr$ch+R*~p0_p6qDkQQ*3VeV7I(10`)OM*g+)y5iG3oaTB$)K3AY9awIAO^Fh z1ll)IfPA!puob|3c^!9*oLY!84|GIm<`k`<^=O~OsLeZKxDJ&uFfSV&`@eMiXGgcD zcz6GHS##9C-u-k#*kE_URw3`Wmtuw;`%7HYzzqR8#JRgFs7c9m^bU7xlv{MJX4nd{ zDfXWj>a4cOlV_|-SRMMvwIR1^h6%eAZjB6`?jS@oKuQA|5S-be2W3GxsruaD>8IUk z+pGU8FMk{_$XxJ1?^ql%zoj}M9_$w->u93fq{k!(IgoA>NF!V1dAK#2NlbIrtR*ZL$_mm literal 5178 zcmciGS#KOw5C!1-jKqIH%JT%z>KUZ`3mzjy<5^4~iB0S*{CVJfw`e;I9+4IjYE3V9 z>$-JLovNNcet*`!Yd^N1+Us_gyJN0z+97wJ+P1fB+lO}FZrXKP-{to{?f1FvGv=!O zmh*@9G3Sq&xy|)HYngw{oHsdcveN6!yiVILPj7Q<^Sp0Av~Sxt?OEFEsC8~L`u1V0 zr?O|#CjYQul6SGv8(DA6hs?h2R_?nUx3Q5;H+ko)hyC5Pm+j}i3+vxy&a2Ehv|ls- zb-#O?r+1lYbeP?#TC-)h5*Iwk9wMBcL^YeBQt6uhf4!dOIVD*cPcRy>(wu(QN z@z60|o#wa6dC}InTjqC>yHy{x$pJ#rGJD$B-efj^&wJ}M?_Q^Gne(o<=%40k8fzCB zxliw9w_?@zw$ECuS;d-VUuT&y7a4DT`!#!&Ihc3IY+fIH`YdJYt?phZuv6S>>25R*g|o%-0% zrz>K_ImEx@lY826-A7wx-`R236PrIye=orzHi+&H0K~KimDk3pY zgkt9=t1q*rcxNvaE4#GTuh=54Sw)+~!aDn4Hyxver5K(?`@kf7v2S~u=l&GeA^(iw z#i4!9+YZ6`K1wb_$Q(C4hrqC6osy673=MS9@O`eHcUeOeQDXPlF0!ZqPwFj?iVQIg zWx8DXz>}yGmFpbKti6eEpuWtEZM*_=`ZC5=2(V{h3gzHm)`{DzShC60yJZsZyzH3R z8P#Gf_3>P~BPY=&Xvs-(%q~}Jv6P-rdl~8+ieGB4F$1cPk+m7E!}iMdecp>fhu8z9 z%3CrNYpJkoNlWdY652~emgr-*47AE#Fx*-^VV6{%KV)t3W#71f&0}z(sx*Tm~GzMs>e-?0_lH zjjfnu5&Rb!GM{+bWRK#fc!|$A&M&wpEA8R~UVuKVMF7o}aq(Vu!)CaPOL>@&j4oE= z&?c>8Fs0?Ns;n=%DV^Lca=mp>bIcS=wWoD@Ss<$Dyz0PRsV^PCS}S0HoI}&aYI%W* z;O4nln?&;}*F9AyfBz7B|5hh|pLL$Dpdahxr!p`6Rk0!o{_^G+zls8=m=6`+qg}A% zrSh9NEU(HoVyGgdY8wbZou^$f1VW|KRozzorcU#=ZF)uHxZHq;;ucCW12tl!TD~%8 z)#Q3qe{!dNs1?*`^6}FgIBiw?d~NhTHTUmz z({1eWWpwjU7p;TwVid+|h1vxLmvit%{YZOjmttC6s%$8H#XS$XH`Y}oi+p)h&JuH~ zT%#&(R2)3N@8@I2jEi%MGLKd2U`ky0Qjf~n&Z504VyZ=pYp952feD?3dO3( zaTxoo0_FNjkO8c(N@vr9+6qcVp7q-yK)3uty{`C)Bu$Uu}S2~h`wjUjS9{) zT&N9Z;1guwNN+@`8i!V37KHK9xWgGtPLUFK|EC(Ce(#Kl`TKku`FBRmJjo6}r-t^A z$=zR-bo~?Je-M5eTSG&jDjb}`A6|r?Ot0pZAM0y)h!;80_YR!I1}?%5FR=+)T2-)A zUhaiCJZXh|NnOkvlYhjXFBPhbofyLQvI18szN>D>MC#y4Ma(<0y7;XK7fl#cm7aVc bGspw7MvW0IR7*v1%A)h%%%@fLf5HDxIDgb+ From 5a33fc166184e47e06009953252f2f10e5b0f123 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 21 Apr 2025 22:31:46 -0700 Subject: [PATCH 0921/1136] fix: ensure options are passed to python envs (#25001) --- src/client/pythonEnvironments/creation/createEnvApi.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/pythonEnvironments/creation/createEnvApi.ts b/src/client/pythonEnvironments/creation/createEnvApi.ts index ab0f0db317c3..b5df9232dd4b 100644 --- a/src/client/pythonEnvironments/creation/createEnvApi.ts +++ b/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -72,7 +72,10 @@ export function registerCreateEnvironmentFeatures( ): Promise => { if (useEnvExtension()) { try { - const result = await executeCommand('python-envs.createAny'); + const result = await executeCommand( + 'python-envs.createAny', + options, + ); if (result) { return { path: result.environmentPath.path }; } From 7fea432ebe694ad4ae5931452d1a435bceacf609 Mon Sep 17 00:00:00 2001 From: Kyle Into Date: Wed, 23 Apr 2025 10:28:30 -0500 Subject: [PATCH 0922/1136] Disable language services if Pyrefly extension installed + active (#24987) For https://github.com/microsoft/vscode-python/issues/24850 Summary: Background: A new typechecker called Pyrefly will be featured at Pycon with a [talk](https://us.pycon.org/2025/schedule/presentation/118/), [website/sandbox](https://pyrefly.org/) (still WIP), and [extension](https://marketplace.visualstudio.com/items?itemName=meta.pyrefly) (still WIP). This extension will provide ultrafast typechecking and language services. When the Pyrefly extension is installed, `ms-python.python` should not start Jedi or Pylance unless [`python.pyrefly.disableLanguageServices`](https://github.com/facebook/pyrefly/commit/4d7e23c4716332234035627ef3009814d7f4cd23) is set to `true`. Because of the separation of vscode's `getExtensions` API and config reading logic, I chose to augment `DefaultLSType` with fallback information in case Pyrefly is disabled. This lets `configSettings` pick the correct jedi/pylance without knowing if Pyrefly will be enabled or disabled. Test Plan: still can't get pyright to work in the local extension build but I do see my breakpoints hit and the correct languageServer set https://github.com/user-attachments/assets/395bacbb-7ad0-4357-b084-cd5e88062801 --- src/client/common/configSettings.ts | 15 +++++- src/client/common/configuration/service.ts | 10 +++- src/client/common/constants.ts | 1 + .../configSettings.pythonPath.unit.test.ts | 10 ++++ .../configSettings.unit.test.ts | 54 +++++++++++++++++-- src/test/extensionSettings.ts | 3 ++ src/test/mocks/extension.ts | 16 ++++++ src/test/mocks/extensions.ts | 23 ++++++++ 8 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 src/test/mocks/extension.ts create mode 100644 src/test/mocks/extensions.ts diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 7ae3467b2cfd..634e0106fe7b 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -21,11 +21,12 @@ import { sendSettingTelemetry } from '../telemetry/envFileTelemetry'; import { ITestingSettings } from '../testing/configuration/types'; import { IWorkspaceService } from './application/types'; import { WorkspaceService } from './application/workspace'; -import { DEFAULT_INTERPRETER_SETTING, isTestExecution } from './constants'; +import { DEFAULT_INTERPRETER_SETTING, isTestExecution, PYREFLY_EXTENSION_ID } from './constants'; import { IAutoCompleteSettings, IDefaultLanguageServer, IExperiments, + IExtensions, IInterpreterPathService, IInterpreterSettings, IPythonSettings, @@ -140,6 +141,7 @@ export class PythonSettings implements IPythonSettings { workspace: IWorkspaceService, private readonly interpreterPathService: IInterpreterPathService, private readonly defaultLS: IDefaultLanguageServer | undefined, + private readonly extensions: IExtensions, ) { this.workspace = workspace || new WorkspaceService(); this.workspaceRoot = workspaceFolder; @@ -152,6 +154,7 @@ export class PythonSettings implements IPythonSettings { workspace: IWorkspaceService, interpreterPathService: IInterpreterPathService, defaultLS: IDefaultLanguageServer | undefined, + extensions: IExtensions, ): PythonSettings { workspace = workspace || new WorkspaceService(); const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource, workspace).uri; @@ -164,6 +167,7 @@ export class PythonSettings implements IPythonSettings { workspace, interpreterPathService, defaultLS, + extensions, ); PythonSettings.pythonSettings.set(workspaceFolderKey, settings); settings.onDidChange((event) => PythonSettings.debounceConfigChangeNotification(event)); @@ -275,7 +279,14 @@ export class PythonSettings implements IPythonSettings { userLS === 'Microsoft' || !Object.values(LanguageServerType).includes(userLS as LanguageServerType) ) { - this.languageServer = this.defaultLS?.defaultLSType ?? LanguageServerType.None; + if ( + this.extensions.getExtension(PYREFLY_EXTENSION_ID) && + pythonSettings.get('pyrefly.disableLanguageServices') !== true + ) { + this.languageServer = LanguageServerType.None; + } else { + this.languageServer = this.defaultLS?.defaultLSType ?? LanguageServerType.None; + } this.languageServerIsDefault = true; } else if (userLS === 'JediLSP') { // Switch JediLSP option to Jedi. diff --git a/src/client/common/configuration/service.ts b/src/client/common/configuration/service.ts index 219c8727ca16..443990b2e5da 100644 --- a/src/client/common/configuration/service.ts +++ b/src/client/common/configuration/service.ts @@ -8,7 +8,13 @@ import { IServiceContainer } from '../../ioc/types'; import { IWorkspaceService } from '../application/types'; import { PythonSettings } from '../configSettings'; import { isUnitTestExecution } from '../constants'; -import { IConfigurationService, IDefaultLanguageServer, IInterpreterPathService, IPythonSettings } from '../types'; +import { + IConfigurationService, + IDefaultLanguageServer, + IExtensions, + IInterpreterPathService, + IPythonSettings, +} from '../types'; @injectable() export class ConfigurationService implements IConfigurationService { @@ -29,12 +35,14 @@ export class ConfigurationService implements IConfigurationService { ); const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); const defaultLS = this.serviceContainer.tryGet(IDefaultLanguageServer); + const extensions = this.serviceContainer.get(IExtensions); return PythonSettings.getInstance( resource, InterpreterAutoSelectionService, this.workspaceService, interpreterPathService, defaultLS, + extensions, ); } diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 5ffa775bf04a..4a8962e86b58 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -22,6 +22,7 @@ export const PYTHON_NOTEBOOKS = [ export const PVSC_EXTENSION_ID = 'ms-python.python'; export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance'; +export const PYREFLY_EXTENSION_ID = 'meta.pyrefly'; export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter'; export const TENSORBOARD_EXTENSION_ID = 'ms-toolsai.tensorboard'; export const AppinsightsKey = '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255'; diff --git a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts index 29082bb5854f..8a2a90b288a3 100644 --- a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts +++ b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts @@ -17,6 +17,7 @@ import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; import { untildify } from '../../../client/common/helpers'; +import { MockExtensions } from '../../mocks/extensions'; suite('Python Settings - pythonPath', () => { class CustomPythonSettings extends PythonSettings { @@ -64,6 +65,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -78,6 +80,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -93,6 +96,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -110,6 +114,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -126,6 +131,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -145,6 +151,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -166,6 +173,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -184,6 +192,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); interpreterPathService.setup((i) => i.get(typemoq.It.isAny())).returns(() => 'custom'); pythonSettings.setup((p) => p.get(typemoq.It.isValue('defaultInterpreterPath'))).returns(() => 'python'); @@ -204,6 +213,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); interpreterPathService.setup((i) => i.get(resource)).returns(() => 'python'); configSettings.update(pythonSettings.object); diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index 43fbf17e970e..65afc782d7bb 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -27,6 +27,7 @@ import { ITestingSettings } from '../../../client/testing/configuration/types'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; import { MockMemento } from '../../mocks/mementos'; import { untildify } from '../../../client/common/helpers'; +import { MockExtensions } from '../../mocks/extensions'; suite('Python Settings', async () => { class CustomPythonSettings extends PythonSettings { @@ -40,6 +41,7 @@ suite('Python Settings', async () => { let config: TypeMoq.IMock; let expected: CustomPythonSettings; let settings: CustomPythonSettings; + let extensions: MockExtensions; setup(() => { sinon.stub(EnvFileTelemetry, 'sendSettingTelemetry').returns(); config = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Loose); @@ -47,6 +49,7 @@ suite('Python Settings', async () => { const workspaceService = new WorkspaceService(); const workspaceMemento = new MockMemento(); const globalMemento = new MockMemento(); + extensions = new MockExtensions(); const persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento); expected = new CustomPythonSettings( undefined, @@ -55,7 +58,8 @@ suite('Python Settings', async () => { new InterpreterPathService(persistentStateFactory, workspaceService, [], { remoteName: undefined, } as IApplicationEnvironment), - undefined, + { defaultLSType: LanguageServerType.Jedi }, + extensions, ); settings = new CustomPythonSettings( undefined, @@ -64,7 +68,8 @@ suite('Python Settings', async () => { new InterpreterPathService(persistentStateFactory, workspaceService, [], { remoteName: undefined, } as IApplicationEnvironment), - undefined, + { defaultLSType: LanguageServerType.Jedi }, + extensions, ); expected.defaultInterpreterPath = 'python'; }); @@ -226,7 +231,7 @@ suite('Python Settings', async () => { const values = [ { ls: LanguageServerType.Jedi, expected: LanguageServerType.Jedi, default: false }, { ls: LanguageServerType.JediLSP, expected: LanguageServerType.Jedi, default: false }, - { ls: LanguageServerType.Microsoft, expected: LanguageServerType.None, default: true }, + { ls: LanguageServerType.Microsoft, expected: LanguageServerType.Jedi, default: true }, { ls: LanguageServerType.Node, expected: LanguageServerType.Node, default: false }, { ls: LanguageServerType.None, expected: LanguageServerType.None, default: false }, ]; @@ -235,7 +240,48 @@ suite('Python Settings', async () => { testLanguageServer(v.ls, v.expected, v.default); }); - testLanguageServer('invalid' as LanguageServerType, LanguageServerType.None, true); + testLanguageServer('invalid' as LanguageServerType, LanguageServerType.Jedi, true); + }); + + function testPyreflySettings(pyreflyInstalled: boolean, pyreflyDisabled: boolean, languageServerDisabled: boolean) { + test(`pyrefly ${pyreflyInstalled ? 'installed' : 'not installed'} and ${ + pyreflyDisabled ? 'disabled' : 'enabled' + }`, () => { + if (pyreflyInstalled) { + extensions.extensionIdsToFind = ['meta.pyrefly']; + } else { + extensions.extensionIdsToFind = []; + } + config.setup((c) => c.get('pyrefly.disableLanguageServices')).returns(() => pyreflyDisabled); + + config + .setup((c) => c.get('languageServer')) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + + settings.update(config.object); + + if (languageServerDisabled) { + expect(settings.languageServer).to.equal(LanguageServerType.None); + } else { + expect(settings.languageServer).not.to.equal(LanguageServerType.None); + } + expect(settings.languageServerIsDefault).to.equal(true); + config.verifyAll(); + }); + } + + suite('pyrefly languageServer settings', async () => { + const values = [ + { pyreflyInstalled: true, pyreflyDisabled: false, languageServerDisabled: true }, + { pyreflyInstalled: true, pyreflyDisabled: true, languageServerDisabled: false }, + { pyreflyInstalled: false, pyreflyDisabled: true, languageServerDisabled: false }, + { pyreflyInstalled: false, pyreflyDisabled: false, languageServerDisabled: false }, + ]; + + values.forEach((v) => { + testPyreflySettings(v.pyreflyInstalled, v.pyreflyDisabled, v.languageServerDisabled); + }); }); function testExperiments(enabled: boolean) { diff --git a/src/test/extensionSettings.ts b/src/test/extensionSettings.ts index 66a77589a770..2d35dcb5f4ca 100644 --- a/src/test/extensionSettings.ts +++ b/src/test/extensionSettings.ts @@ -13,6 +13,7 @@ import { PersistentStateFactory } from '../client/common/persistentState'; import { IPythonSettings, Resource } from '../client/common/types'; import { PythonEnvironment } from '../client/pythonEnvironments/info'; import { MockMemento } from './mocks/mementos'; +import { MockExtensions } from './mocks/extensions'; export function getExtensionSettings(resource: Uri | undefined): IPythonSettings { const vscode = require('vscode') as typeof import('vscode'); @@ -41,6 +42,7 @@ export function getExtensionSettings(resource: Uri | undefined): IPythonSettings const workspaceMemento = new MockMemento(); const globalMemento = new MockMemento(); const persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento); + const extensions = new MockExtensions(); return pythonSettings.PythonSettings.getInstance( resource, new AutoSelectionService(), @@ -49,5 +51,6 @@ export function getExtensionSettings(resource: Uri | undefined): IPythonSettings remoteName: undefined, } as IApplicationEnvironment), undefined, + extensions, ); } diff --git a/src/test/mocks/extension.ts b/src/test/mocks/extension.ts new file mode 100644 index 000000000000..61d70eb5ee9e --- /dev/null +++ b/src/test/mocks/extension.ts @@ -0,0 +1,16 @@ +import { injectable } from 'inversify'; +import { Extension, ExtensionKind, Uri } from 'vscode'; + +@injectable() +export class MockExtension implements Extension { + id!: string; + extensionUri!: Uri; + extensionPath!: string; + isActive!: boolean; + packageJSON: any; + extensionKind!: ExtensionKind; + exports!: T; + activate(): Thenable { + throw new Error('Method not implemented.'); + } +} diff --git a/src/test/mocks/extensions.ts b/src/test/mocks/extensions.ts new file mode 100644 index 000000000000..efe9b6b8ca31 --- /dev/null +++ b/src/test/mocks/extensions.ts @@ -0,0 +1,23 @@ +import { injectable } from 'inversify'; +import { IExtensions } from '../../client/common/types'; +import { Extension, Event } from 'vscode'; +import { MockExtension } from './extension'; + +@injectable() +export class MockExtensions implements IExtensions { + extensionIdsToFind: unknown[] = []; + all: readonly Extension[] = []; + onDidChange: Event = () => { + throw new Error('Method not implemented'); + }; + getExtension(extensionId: string): Extension | undefined; + getExtension(extensionId: string): Extension | undefined; + getExtension(extensionId: unknown): import('vscode').Extension | undefined { + if (this.extensionIdsToFind.includes(extensionId)) { + return new MockExtension(); + } + } + determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> { + throw new Error('Method not implemented.'); + } +} From 2e023d4e746f8410887ecc15fa2ede2c7f249b9e Mon Sep 17 00:00:00 2001 From: Heejae Chang <1333179+heejaechang@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:44:15 -0700 Subject: [PATCH 0923/1136] Added some GDPR for pylance (#25004) --- src/client/telemetry/pylance.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index 2b50e65f9ee9..3d1ba05779dd 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -457,6 +457,16 @@ "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } } */ +/* __GDPR__ + "language_server/mcp_tool" : { + "kind" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "cancelled" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "cancellation_reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "lsversion" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ /** * Telemetry event sent when LSP server crashes */ From e93a0755e08472339cef1acd77139d39683a8449 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 24 Apr 2025 09:04:14 -0700 Subject: [PATCH 0924/1136] feat: enable semantic tokens in Jedi language server analysis options (#25006) Fixes https://github.com/microsoft/vscode-python/issues/25003 ![image](https://github.com/user-attachments/assets/b57c6897-6a85-464a-b35b-3f97351f3d9b) --- src/client/activation/jedi/analysisOptions.ts | 3 +++ src/test/activation/jedi/jediAnalysisOptions.unit.test.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/src/client/activation/jedi/analysisOptions.ts b/src/client/activation/jedi/analysisOptions.ts index 3b1897b91088..007008dc9b13 100644 --- a/src/client/activation/jedi/analysisOptions.ts +++ b/src/client/activation/jedi/analysisOptions.ts @@ -85,6 +85,9 @@ export class JediLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt maxSymbols: 0, }, }, + semantic_tokens: { + enable: true, + }, }; } } diff --git a/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts b/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts index 3456a6252722..66cb9e0ae604 100644 --- a/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts +++ b/src/test/activation/jedi/jediAnalysisOptions.unit.test.ts @@ -74,6 +74,7 @@ suite('Jedi LSP - analysis Options', () => { expect(result.initializationOptions.hover.disable.keyword.all).to.deep.equal(true); expect(result.initializationOptions.workspace.extraPaths).to.deep.equal([]); expect(result.initializationOptions.workspace.symbols.maxSymbols).to.deep.equal(0); + expect(result.initializationOptions.semantic_tokens.enable).to.deep.equal(true); }); test('With interpreter path', async () => { From d597e1ca8b024c046253ffd70558447d00be6fef Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 2 May 2025 12:14:33 -0700 Subject: [PATCH 0925/1136] bump to release 2025.6 (#25033) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7159e2850336..f04547ff9004 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.5.0-dev", + "version": "2025.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.5.0-dev", + "version": "2025.6.0", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index f2a199d705c5..6e3a84107a1b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.5.0-dev", + "version": "2025.6.0", "featureFlags": { "usingNewInterpreterStorage": true }, From b44b4d442fce5c0ab1547c777b846c1ade889832 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 2 May 2025 12:50:59 -0700 Subject: [PATCH 0926/1136] bump: update version to 2025.7.0-dev in package.json (#25034) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f04547ff9004..340bd1f4bb9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.6.0", + "version": "2025.7.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.6.0", + "version": "2025.7.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 6e3a84107a1b..4f29f6c4dd79 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.6.0", + "version": "2025.7.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 0317c6be492d66a7884d806f5244ce52a8cff852 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 6 May 2025 09:50:55 -0700 Subject: [PATCH 0927/1136] PYTHONSTARTUP should be injected regardless of terminalEnv experiment (#25037) Resolve: https://github.com/microsoft/vscode-python/issues/25013 Python shell integration env var injection via env var collection was getting cleared undesirable, when user had opted out of terminal env var experiment. We want to inject PYTHONSTARTUP regardless of the experiment, depending on user setting. --- src/client/terminals/envCollectionActivation/service.ts | 2 ++ .../activation/terminalEnvVarCollectionService.unit.test.ts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts index 43b8ceeb8e06..bd2ce1c6f717 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -44,6 +44,7 @@ import { } from '../types'; import { ProgressService } from '../../common/application/progressService'; import { useEnvExtension } from '../../envExt/api.internal'; +import { registerPythonStartup } from '../pythonStartup'; @injectable() export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { @@ -109,6 +110,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ ); this.registeredOnce = true; } + await registerPythonStartup(this.context); return; } if (!this.registeredOnce) { diff --git a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts index 7016a25c7a4e..dfe3ad8c081a 100644 --- a/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts +++ b/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts @@ -6,12 +6,14 @@ import * as sinon from 'sinon'; import { assert, expect } from 'chai'; import { mock, instance, when, anything, verify, reset } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; import { EnvironmentVariableCollection, EnvironmentVariableMutatorOptions, GlobalEnvironmentVariableCollection, ProgressLocation, Uri, + WorkspaceConfiguration, WorkspaceFolder, } from 'vscode'; import { @@ -55,6 +57,7 @@ suite('Terminal Environment Variable Collection Service', () => { let terminalEnvVarCollectionService: TerminalEnvVarCollectionService; let terminalDeactivateService: ITerminalDeactivateService; let useEnvExtensionStub: sinon.SinonStub; + let pythonConfig: TypeMoq.IMock; const progressOptions = { location: ProgressLocation.Window, title: Interpreters.activatingTerminals, @@ -122,6 +125,8 @@ suite('Terminal Environment Variable Collection Service', () => { instance(shellIntegrationService), instance(envVarProvider), ); + pythonConfig = TypeMoq.Mock.ofType(); + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); }); teardown(() => { From a3dd3aa1bca82be1fb5c44f04c689233010eaeab Mon Sep 17 00:00:00 2001 From: Dinesh Date: Tue, 6 May 2025 22:37:10 +0530 Subject: [PATCH 0928/1136] Add shortTitle to execSelectionInTerminal command (#25007) Add shortTitle to execSelectionInTerminal command --- package.json | 3 ++- package.nls.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4f29f6c4dd79..00766d650656 100644 --- a/package.json +++ b/package.json @@ -325,7 +325,8 @@ { "category": "Python", "command": "python.execSelectionInTerminal", - "title": "%python.command.python.execSelectionInTerminal.title%" + "title": "%python.command.python.execSelectionInTerminal.title%", + "shortTitle": "%python.command.python.execSelectionInTerminal.shortTitle%" }, { "category": "Python", diff --git a/package.nls.json b/package.nls.json index 2d4028063006..6266bd67de50 100644 --- a/package.nls.json +++ b/package.nls.json @@ -6,7 +6,7 @@ "python.command.python.createTerminal.title": "Create Terminal", "python.command.python.execInTerminal.title": "Run Python File in Terminal", "python.command.python.execInTerminalIcon.title": "Run Python File", - "python.command.python.execInDedicatedTerminal.title": "Run Python File in Dedicated Terminal", + "python.command.python.execInDedicatedTerminal.title": "Run Python File in Dedicated Terminal", "python.command.python.setInterpreter.title": "Select Interpreter", "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", "python.command.python.viewOutput.title": "Show Output", @@ -15,6 +15,7 @@ "python.command.python.configureTests.title": "Configure Tests", "python.command.testing.rerunFailedTests.title": "Rerun Failed Tests", "python.command.python.execSelectionInTerminal.title": "Run Selection/Line in Python Terminal", + "python.command.python.execSelectionInTerminal.shortTitle": "Run Selection/Line", "python.command.python.execInREPL.title": "Run Selection/Line in Native Python REPL", "python.command.python.execSelectionInDjangoShell.title": "Run Selection/Line in Django Shell", "python.command.python.reportIssue.title": "Report Issue...", From b0b8aff14bfa25b5072c05e1b75581430ce532c2 Mon Sep 17 00:00:00 2001 From: ALBIN BABU VARGHESE Date: Wed, 7 May 2025 11:26:22 -0400 Subject: [PATCH 0929/1136] Added pylock to activationevents (#25025) This fixes #25016 Added some changes to package.json, so that the extension gets activated whenever there is a file with the name `pylock.toml` or match the regular expression `r"^pylock\.([^.]+)\.toml$"`. I followed [PEP 751](https://peps.python.org/pep-0751/#file-name)'s naming specification. ![Screenshot (127)](https://github.com/user-attachments/assets/476abd9b-9251-457b-bdcc-ae3d3c16fd73) --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 00766d650656..35edd8683eed 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,8 @@ "workspaceContains:Pipfile", "workspaceContains:setup.py", "workspaceContains:requirements.txt", + "workspaceContains:pylock.toml", + "workspaceContains:**/pylock.*.toml", "workspaceContains:manage.py", "workspaceContains:app.py", "workspaceContains:.venv", From 09ef3c4e4e0310555109d2b628f71bddf944b766 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Fri, 9 May 2025 14:35:46 -0700 Subject: [PATCH 0930/1136] chore: lock down workflows (#25047) --- .github/actions/build-vsix/action.yml | 12 +++-- .github/actions/smoke-tests/action.yml | 4 +- .github/workflows/build.yml | 31 ++++++++---- .github/workflows/codeql-analysis.yml | 2 + .../community-feedback-auto-comment.yml | 4 +- .github/workflows/gen-issue-velocity.yml | 5 ++ .github/workflows/info-needed-closer.yml | 1 + .github/workflows/issue-labels.yml | 1 + .github/workflows/lock-issues.yml | 2 +- .github/workflows/pr-check.yml | 47 ++++++++++++++----- .github/workflows/pr-file-check.yml | 8 ++-- .github/workflows/pr-labels.yml | 5 +- .github/workflows/python27-issue-response.yml | 2 + .github/workflows/remove-needs-labels.yml | 4 +- .../workflows/test-plan-item-validator.yml | 1 + .github/workflows/triage-info-needed.yml | 11 +++-- 16 files changed, 102 insertions(+), 38 deletions(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index c2515247de97..eaabe5141e8b 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -54,8 +54,10 @@ runs: shell: bash - name: Add Rustup target - run: rustup target add ${{ inputs.cargo_target }} + run: rustup target add "${CARGO_TARGET}" shell: bash + env: + CARGO_TARGET: ${{ inputs.cargo_target }} - name: Build Native Binaries run: nox --session native_build @@ -78,13 +80,17 @@ runs: shell: bash - name: Build VSIX - run: npx vsce package --target ${{ inputs.vsix_target }} --out ms-python-insiders.vsix --pre-release + run: npx vsce package --target "${VSIX_TARGET}" --out ms-python-insiders.vsix --pre-release shell: bash + env: + VSIX_TARGET: ${{ inputs.vsix_target }} - name: Rename VSIX # Move to a temp name in case the specified name happens to match the default name. - run: mv ms-python-insiders.vsix ms-python-temp.vsix && mv ms-python-temp.vsix ${{ inputs.vsix_name }} + run: mv ms-python-insiders.vsix ms-python-temp.vsix && mv ms-python-temp.vsix "${VSIX_NAME}" shell: bash + env: + VSIX_NAME: ${{ inputs.vsix_name }} - name: Upload VSIX uses: actions/upload-artifact@v4 diff --git a/.github/actions/smoke-tests/action.yml b/.github/actions/smoke-tests/action.yml index ed760e8b8202..0531ef5d42a3 100644 --- a/.github/actions/smoke-tests/action.yml +++ b/.github/actions/smoke-tests/action.yml @@ -32,7 +32,7 @@ runs: shell: bash - name: Install Python requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: options: '-t ./python_files/lib/python --implementation py' @@ -61,6 +61,6 @@ runs: env: DISPLAY: 10 INSTALL_JUPYTER_EXTENSION: true - uses: GabrielBB/xvfb-action@v1.7 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: node --no-force-async-hooks-checks ./out/test/smokeTest.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4b65b91a2cdf..78cbd9dfd0e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,8 @@ on: - 'release/*' - 'release-*' +permissions: {} + env: NODE_VERSION: 20.18.0 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 @@ -83,12 +85,15 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Checkout Python Environment Tools uses: actions/checkout@v4 with: repository: 'microsoft/python-environment-tools' path: 'python-env-tools' + persist-credentials: false sparse-checkout: | crates Cargo.toml @@ -111,6 +116,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Lint uses: ./.github/actions/lint @@ -129,14 +136,16 @@ jobs: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Install core Python requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: options: '-t ./python_files/lib/python --no-cache-dir --implementation py' - name: Install Jedi requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: requirements-file: './python_files/jedilsp_requirements/requirements.txt' options: '-t ./python_files/lib/jedilsp --no-cache-dir --implementation py' @@ -146,7 +155,7 @@ jobs: python -m pip install --upgrade -r build/test-requirements.txt - name: Run Pyright - uses: jakebailey/pyright-action@v2 + uses: jakebailey/pyright-action@b5d50e5cde6547546a5c4ac92e416a8c2c1a1dfe # v2.3.2 with: version: 1.1.308 working-directory: 'python_files' @@ -172,6 +181,7 @@ jobs: uses: actions/checkout@v4 with: path: ${{ env.special-working-directory-relative }} + persist-credentials: false - name: Use Python ${{ matrix.python }} uses: actions/setup-python@v5 @@ -179,7 +189,7 @@ jobs: python-version: ${{ matrix.python }} - name: Install base Python requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' options: '-t "${{ env.special-working-directory-relative }}/python_files/lib/python" --no-cache-dir --implementation py' @@ -211,12 +221,14 @@ jobs: uses: actions/checkout@v4 with: path: ${{ env.special-working-directory-relative }} + persist-credentials: false - name: Checkout Python Environment Tools uses: actions/checkout@v4 with: repository: 'microsoft/python-environment-tools' path: ${{ env.special-working-directory-relative }}/python-env-tools + persist-credentials: false sparse-checkout: | crates Cargo.toml @@ -358,7 +370,7 @@ jobs: env: TEST_FILES_SUFFIX: testvirtualenvs CI_PYTHON_VERSION: ${{ matrix.python }} - uses: GabrielBB/xvfb-action@v1.7 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testSingleWorkspace working-directory: ${{ env.special-working-directory }} @@ -367,7 +379,7 @@ jobs: - name: Run single-workspace tests env: CI_PYTHON_VERSION: ${{ matrix.python }} - uses: GabrielBB/xvfb-action@v1.7 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testSingleWorkspace working-directory: ${{ env.special-working-directory }} @@ -376,7 +388,7 @@ jobs: - name: Run multi-workspace tests env: CI_PYTHON_VERSION: ${{ matrix.python }} - uses: GabrielBB/xvfb-action@v1.7 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testMultiWorkspace working-directory: ${{ env.special-working-directory }} @@ -385,7 +397,7 @@ jobs: - name: Run debugger tests env: CI_PYTHON_VERSION: ${{ matrix.python }} - uses: GabrielBB/xvfb-action@v1.7 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testDebugger working-directory: ${{ env.special-working-directory }} @@ -415,12 +427,15 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Checkout Python Environment Tools uses: actions/checkout@v4 with: repository: 'microsoft/python-environment-tools' path: ${{ env.special-working-directory-relative }}/python-env-tools + persist-credentials: false sparse-checkout: | crates Cargo.toml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d902a68878e0..cfd7c393e3ed 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -37,6 +37,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/community-feedback-auto-comment.yml b/.github/workflows/community-feedback-auto-comment.yml index cf3c4f51fe61..f606148f6e86 100644 --- a/.github/workflows/community-feedback-auto-comment.yml +++ b/.github/workflows/community-feedback-auto-comment.yml @@ -12,7 +12,7 @@ jobs: issues: write steps: - name: Check For Existing Comment - uses: peter-evans/find-comment@v3 + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 id: finder with: issue-number: ${{ github.event.issue.number }} @@ -21,7 +21,7 @@ jobs: - name: Add Community Feedback Comment if: steps.finder.outputs.comment-id == '' - uses: peter-evans/create-or-update-comment@v4 + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 with: issue-number: ${{ github.event.issue.number }} body: | diff --git a/.github/workflows/gen-issue-velocity.yml b/.github/workflows/gen-issue-velocity.yml index a2fd9610892d..344fa161f02e 100644 --- a/.github/workflows/gen-issue-velocity.yml +++ b/.github/workflows/gen-issue-velocity.yml @@ -5,6 +5,9 @@ on: - cron: '0 0 * * 2' # Runs every Tuesday at midnight workflow_dispatch: +permissions: + issues: read + jobs: generate-summary: runs-on: ubuntu-latest @@ -12,6 +15,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/info-needed-closer.yml b/.github/workflows/info-needed-closer.yml index 64a96b06e556..d7efbd199451 100644 --- a/.github/workflows/info-needed-closer.yml +++ b/.github/workflows/info-needed-closer.yml @@ -18,6 +18,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions + persist-credentials: false ref: stable - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index fbd92d9edd01..ec7d14d96cda 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -22,6 +22,7 @@ jobs: repository: 'microsoft/vscode-github-triage-actions' ref: stable path: ./actions + persist-credentials: false - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/lock-issues.yml b/.github/workflows/lock-issues.yml index 47f243d71979..cb6ed2e9d54e 100644 --- a/.github/workflows/lock-issues.yml +++ b/.github/workflows/lock-issues.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Lock Issues' - uses: dessant/lock-threads@v5 + uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 with: github-token: ${{ github.token }} issue-inactive-days: '30' diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 4b1ea54618b8..81c427a31c7b 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -7,6 +7,8 @@ on: - main - release* +permissions: {} + env: NODE_VERSION: 20.18.0 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 @@ -56,12 +58,15 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Checkout Python Environment Tools uses: actions/checkout@v4 with: repository: 'microsoft/python-environment-tools' path: 'python-env-tools' + persist-credentials: false sparse-checkout: | crates Cargo.toml @@ -83,6 +88,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Lint uses: ./.github/actions/lint @@ -100,12 +107,15 @@ jobs: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Checkout Python Environment Tools uses: actions/checkout@v4 with: repository: 'microsoft/python-environment-tools' path: 'python-env-tools' + persist-credentials: false sparse-checkout: | crates Cargo.toml @@ -113,12 +123,12 @@ jobs: sparse-checkout-cone-mode: false - name: Install base Python requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: options: '-t ./python_files/lib/python --no-cache-dir --implementation py' - name: Install Jedi requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: requirements-file: './python_files/jedilsp_requirements/requirements.txt' options: '-t ./python_files/lib/jedilsp --no-cache-dir --implementation py' @@ -128,7 +138,7 @@ jobs: python -m pip install --upgrade -r build/test-requirements.txt - name: Run Pyright - uses: jakebailey/pyright-action@v2 + uses: jakebailey/pyright-action@b5d50e5cde6547546a5c4ac92e416a8c2c1a1dfe # v2.3.2 with: version: 1.1.308 working-directory: 'python_files' @@ -155,6 +165,7 @@ jobs: uses: actions/checkout@v4 with: path: ${{ env.special-working-directory-relative }} + persist-credentials: false - name: Use Python ${{ matrix.python }} uses: actions/setup-python@v5 @@ -174,7 +185,7 @@ jobs: - name: Install specific pytest version run: python -m pytest --version - name: Install base Python requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' options: '-t "${{ env.special-working-directory-relative }}/python_files/lib/python" --no-cache-dir --implementation py' @@ -207,12 +218,14 @@ jobs: uses: actions/checkout@v4 with: path: ${{ env.special-working-directory-relative }} + persist-credentials: false - name: Checkout Python Environment Tools uses: actions/checkout@v4 with: repository: 'microsoft/python-environment-tools' path: ${{ env.special-working-directory-relative }}/python-env-tools + persist-credentials: false sparse-checkout: | crates Cargo.toml @@ -354,7 +367,7 @@ jobs: env: TEST_FILES_SUFFIX: testvirtualenvs CI_PYTHON_VERSION: ${{ matrix.python }} - uses: GabrielBB/xvfb-action@v1.7 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testSingleWorkspace working-directory: ${{ env.special-working-directory }} @@ -363,7 +376,7 @@ jobs: - name: Run single-workspace tests env: CI_PYTHON_VERSION: ${{ matrix.python }} - uses: GabrielBB/xvfb-action@v1.7 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testSingleWorkspace working-directory: ${{ env.special-working-directory }} @@ -372,7 +385,7 @@ jobs: - name: Run debugger tests env: CI_PYTHON_VERSION: ${{ matrix.python }} - uses: GabrielBB/xvfb-action@v1.7 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testDebugger working-directory: ${{ env.special-working-directory }} @@ -402,12 +415,14 @@ jobs: uses: actions/checkout@v4 with: path: ${{ env.special-working-directory-relative }} + persist-credentials: false - name: Checkout Python Environment Tools uses: actions/checkout@v4 with: repository: 'microsoft/python-environment-tools' path: ${{ env.special-working-directory-relative }}/python-env-tools + persist-credentials: false sparse-checkout: | crates Cargo.toml @@ -438,12 +453,15 @@ jobs: # Need the source to have the tests available. - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Checkout Python Environment Tools uses: actions/checkout@v4 with: repository: 'microsoft/python-environment-tools' path: python-env-tools + persist-credentials: false sparse-checkout: | crates Cargo.toml @@ -471,12 +489,15 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + persist-credentials: false - name: Checkout Python Environment Tools uses: actions/checkout@v4 with: repository: 'microsoft/python-environment-tools' path: python-env-tools + persist-credentials: false sparse-checkout: | crates Cargo.toml @@ -510,12 +531,12 @@ jobs: build/functional-test-requirements.txt - name: Install base Python requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: options: '-t ./python_files/lib/python --implementation py' - name: Install Jedi requirements - uses: brettcannon/pip-secure-install@v1 + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 with: requirements-file: './python_files/jedilsp_requirements/requirements.txt' options: '-t ./python_files/lib/jedilsp --implementation py' @@ -618,7 +639,7 @@ jobs: TEST_FILES_SUFFIX: testvirtualenvs CI_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} CI_DISABLE_AUTO_SELECTION: 1 - uses: GabrielBB/xvfb-action@v1.7 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testSingleWorkspace:cover @@ -626,7 +647,7 @@ jobs: env: CI_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} CI_DISABLE_AUTO_SELECTION: 1 - uses: GabrielBB/xvfb-action@v1.7 + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 with: run: npm run testSingleWorkspace:cover @@ -635,7 +656,7 @@ jobs: # env: # CI_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} # CI_DISABLE_AUTO_SELECTION: 1 - # uses: GabrielBB/xvfb-action@v1.7 + # uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 # with: # run: npm run testMultiWorkspace:cover @@ -644,7 +665,7 @@ jobs: # env: # CI_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} # CI_DISABLE_AUTO_SELECTION: 1 - # uses: GabrielBB/xvfb-action@v1.7 + # uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 # with: # run: npm run testDebugger:cover diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index b5ba2fe1f109..180ab16a74c3 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -9,13 +9,15 @@ on: - 'labeled' - 'unlabeled' +permissions: {} + jobs: changed-files-in-pr: name: 'Check for changed files' runs-on: ubuntu-latest steps: - name: 'package-lock.json matches package.json' - uses: brettcannon/check-for-changed-files@v1.2.1 + uses: brettcannon/check-for-changed-files@871d7b8b5917a4f6f06662e2262e8ffc51dff6d1 # v1.2.1 with: prereq-pattern: 'package.json' file-pattern: 'package-lock.json' @@ -23,7 +25,7 @@ jobs: failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - name: 'package.json matches package-lock.json' - uses: brettcannon/check-for-changed-files@v1.2.1 + uses: brettcannon/check-for-changed-files@871d7b8b5917a4f6f06662e2262e8ffc51dff6d1 # v1.2.1 with: prereq-pattern: 'package-lock.json' file-pattern: 'package.json' @@ -31,7 +33,7 @@ jobs: failure-message: '${prereq-pattern} was edited but ${file-pattern} was not (the ${skip-label} label can be used to pass this check)' - name: 'Tests' - uses: brettcannon/check-for-changed-files@v1.2.1 + uses: brettcannon/check-for-changed-files@871d7b8b5917a4f6f06662e2262e8ffc51dff6d1 # v1.2.1 with: prereq-pattern: src/**/*.ts file-pattern: | diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 730b8e5c5832..3b82068de5aa 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -12,9 +12,12 @@ jobs: classify: name: 'Classify PR' runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write steps: - name: 'PR impact specified' - uses: mheap/github-action-required-labels@v5 + uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5.5.0 with: mode: exactly count: 1 diff --git a/.github/workflows/python27-issue-response.yml b/.github/workflows/python27-issue-response.yml index 4d51e9921ab4..9db84bca1a23 100644 --- a/.github/workflows/python27-issue-response.yml +++ b/.github/workflows/python27-issue-response.yml @@ -5,6 +5,8 @@ on: jobs: python27-issue-response: runs-on: ubuntu-latest + permissions: + issues: write if: "contains(github.event.issue.body, 'Python version (& distribution if applicable, e.g. Anaconda): 2.7')" steps: - name: Check for Python 2.7 string diff --git a/.github/workflows/remove-needs-labels.yml b/.github/workflows/remove-needs-labels.yml index 3d218e297a11..24352526d0d8 100644 --- a/.github/workflows/remove-needs-labels.yml +++ b/.github/workflows/remove-needs-labels.yml @@ -7,9 +7,11 @@ jobs: classify: name: 'Remove needs labels on issue closing' runs-on: ubuntu-latest + permissions: + issues: write steps: - name: 'Removes needs labels on issue close' - uses: actions-ecosystem/action-remove-labels@v1 + uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0 with: labels: | needs PR diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml index 17f1740345f2..91e8948cc784 100644 --- a/.github/workflows/test-plan-item-validator.yml +++ b/.github/workflows/test-plan-item-validator.yml @@ -16,6 +16,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions + persist-credentials: false ref: stable - name: Install Actions diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml index 1ded54ea3f59..f468fb293acd 100644 --- a/.github/workflows/triage-info-needed.yml +++ b/.github/workflows/triage-info-needed.yml @@ -7,13 +7,12 @@ on: env: TRIAGERS: '["karrtikr","karthiknadig","paulacamargo25","eleanorjboyd", "brettcannon","anthonykim1"]' -permissions: - issues: write - jobs: add_label: - runs-on: ubuntu-latest if: contains(github.event.issue.labels.*.name, 'triage-needed') && !contains(github.event.issue.labels.*.name, 'info-needed') + runs-on: ubuntu-latest + permissions: + issues: write steps: - name: Checkout Actions uses: actions/checkout@v4 @@ -21,6 +20,7 @@ jobs: repository: 'microsoft/vscode-github-triage-actions' ref: stable path: ./actions + persist-credentials: false - name: Install Actions run: npm install --production --prefix ./actions @@ -35,6 +35,8 @@ jobs: remove_label: if: contains(github.event.issue.labels.*.name, 'info-needed') && contains(github.event.issue.labels.*.name, 'triage-needed') runs-on: ubuntu-latest + permissions: + issues: write steps: - name: Checkout Actions uses: actions/checkout@v4 @@ -42,6 +44,7 @@ jobs: repository: 'microsoft/vscode-github-triage-actions' ref: stable path: ./actions + persist-credentials: false - name: Install Actions run: npm install --production --prefix ./actions From 8892ce6aafc21b62ddd9da4a36faf5e7e85b4a16 Mon Sep 17 00:00:00 2001 From: skai273 <144425442+s-kai273@users.noreply.github.com> Date: Tue, 13 May 2025 02:07:13 +0900 Subject: [PATCH 0931/1136] Fix env error handling (#25049) Fix https://github.com/microsoft/vscode-python/issues/24211 --- src/client/pythonEnvironments/legacyIOC.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index 9d161f8b1b9f..a1a1b841a16f 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -26,7 +26,7 @@ import { asyncFilter } from '../common/utils/arrayUtils'; import { CondaEnvironmentInfo, isCondaEnvironment } from './common/environmentManagers/conda'; import { isMicrosoftStoreEnvironment } from './common/environmentManagers/microsoftStoreEnv'; import { CondaService } from './common/environmentManagers/condaService'; -import { traceVerbose } from '../logging'; +import { traceError, traceVerbose } from '../logging'; const convertedKinds = new Map( Object.entries({ @@ -159,11 +159,16 @@ class ComponentAdapter implements IComponentAdapter { // We use the same getInterpreters() here as for IInterpreterLocatorService. public async getInterpreterDetails(pythonPath: string): Promise { - const env = await this.api.resolveEnv(pythonPath); - if (!env) { + try { + const env = await this.api.resolveEnv(pythonPath); + if (!env) { + return undefined; + } + return convertEnvInfo(env); + } catch (ex) { + traceError(`Failed to resolve interpreter: ${pythonPath}`, ex); return undefined; } - return convertEnvInfo(env); } // Implements ICondaService From d6b62deee4eb32e8accbf5f13d61e112c2b40b8c Mon Sep 17 00:00:00 2001 From: skai273 <144425442+s-kai273@users.noreply.github.com> Date: Wed, 14 May 2025 02:13:39 +0900 Subject: [PATCH 0932/1136] Fix msys2 venv path (#25062) Fix https://github.com/microsoft/vscode-python/issues/24792 --- python_files/create_venv.py | 11 ++++++++++- python_files/tests/test_create_venv.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/python_files/create_venv.py b/python_files/create_venv.py index fd1ff9ab1a47..83106bd889f8 100644 --- a/python_files/create_venv.py +++ b/python_files/create_venv.py @@ -98,11 +98,20 @@ def run_process(args: Sequence[str], error_message: str) -> None: raise VenvError(error_message) from exc +def get_win_venv_path(name: str) -> str: + venv_dir = CWD / name + # If using MSYS2 Python, the Python executable is located in the 'bin' directory. + if file_exists(venv_dir / "bin" / "python.exe"): + return os.fspath(venv_dir / "bin" / "python.exe") + else: + return os.fspath(venv_dir / "Scripts" / "python.exe") + + def get_venv_path(name: str) -> str: # See `venv` doc here for more details on binary location: # https://docs.python.org/3/library/venv.html#creating-virtual-environments if sys.platform == "win32": - return os.fspath(CWD / name / "Scripts" / "python.exe") + return get_win_venv_path(name) else: return os.fspath(CWD / name / "bin" / "python") diff --git a/python_files/tests/test_create_venv.py b/python_files/tests/test_create_venv.py index 72fabdaaecac..6308934d71a0 100644 --- a/python_files/tests/test_create_venv.py +++ b/python_files/tests/test_create_venv.py @@ -122,7 +122,7 @@ def create_gitignore(_p): def test_install_packages(install_type): importlib.reload(create_venv) create_venv.is_installed = lambda _x: True - create_venv.file_exists = lambda x: install_type in x + create_venv.file_exists = lambda x: install_type in str(x) pip_upgraded = False installing = None From e7090129b02e53689e190f692c3963b98919c4d5 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 14 May 2025 08:44:30 +1000 Subject: [PATCH 0933/1136] Updates to latest version of VS Code types (#25065) Required to use `vscode.lm.registerTool()` --- package-lock.json | 19 +- package.json | 4 +- src/client/common/cancellation.ts | 6 +- src/client/common/process/logger.ts | 1 + src/client/common/utils/charCode.ts | 453 +++++++++++++++++++++++++++ src/test/mocks/mockDocument.ts | 1 + src/test/mocks/vsc/extHostedTypes.ts | 23 +- 7 files changed, 493 insertions(+), 14 deletions(-) create mode 100644 src/client/common/utils/charCode.ts diff --git a/package-lock.json b/package-lock.json index 340bd1f4bb9b..ed498b802b4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "@types/sinon": "^17.0.3", "@types/stack-trace": "0.0.29", "@types/tmp": "^0.0.33", - "@types/vscode": "^1.93.0", + "@types/vscode": "^1.95.0", "@types/which": "^2.0.1", "@types/winreg": "^1.2.30", "@types/xml2js": "^0.4.2", @@ -115,7 +115,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.94.0-20240918" + "vscode": "^1.97.0-20240918" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1959,10 +1959,11 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.93.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.93.0.tgz", - "integrity": "sha512-kUK6jAHSR5zY8ps42xuW89NLcBpw1kOabah7yv38J8MyiYuOHxLQBi0e7zeXbQgVefDy/mZZetqEFC+Fl5eIEQ==", - "dev": true + "version": "1.100.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz", + "integrity": "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/which": { "version": "2.0.1", @@ -16563,9 +16564,9 @@ "dev": true }, "@types/vscode": { - "version": "1.93.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.93.0.tgz", - "integrity": "sha512-kUK6jAHSR5zY8ps42xuW89NLcBpw1kOabah7yv38J8MyiYuOHxLQBi0e7zeXbQgVefDy/mZZetqEFC+Fl5eIEQ==", + "version": "1.100.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz", + "integrity": "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==", "dev": true }, "@types/which": { diff --git a/package.json b/package.json index 35edd8683eed..c5f8fa74b544 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.94.0-20240918" + "vscode": "^1.95.0" }, "enableTelemetry": false, "keywords": [ @@ -1554,7 +1554,7 @@ "@types/sinon": "^17.0.3", "@types/stack-trace": "0.0.29", "@types/tmp": "^0.0.33", - "@types/vscode": "^1.93.0", + "@types/vscode": "^1.95.0", "@types/which": "^2.0.1", "@types/winreg": "^1.2.30", "@types/xml2js": "^0.4.2", diff --git a/src/client/common/cancellation.ts b/src/client/common/cancellation.ts index 7c9c26c597b3..b24abc7ab493 100644 --- a/src/client/common/cancellation.ts +++ b/src/client/common/cancellation.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. 'use strict'; -import { CancellationToken, CancellationTokenSource } from 'vscode'; +import { CancellationToken, CancellationTokenSource, CancellationError as VSCCancellationError } from 'vscode'; import { createDeferred } from './utils/async'; import * as localize from './utils/localize'; @@ -13,6 +13,10 @@ export class CancellationError extends Error { constructor() { super(localize.Common.canceled); } + + static isCancellationError(error: unknown): error is CancellationError { + return error instanceof CancellationError || error instanceof VSCCancellationError; + } } /** * Create a promise that will either resolve with a default value or reject when the token is cancelled. diff --git a/src/client/common/process/logger.ts b/src/client/common/process/logger.ts index 1c0b78dd941f..47e9ef88fa4f 100644 --- a/src/client/common/process/logger.ts +++ b/src/client/common/process/logger.ts @@ -12,6 +12,7 @@ import { IProcessLogger, SpawnOptions } from './types'; import { escapeRegExp } from 'lodash'; import { replaceAll } from '../stringUtils'; import { identifyShellFromShellPath } from '../terminal/shellDetectors/baseShellDetector'; +import '../../common/extensions'; @injectable() export class ProcessLogger implements IProcessLogger { diff --git a/src/client/common/utils/charCode.ts b/src/client/common/utils/charCode.ts new file mode 100644 index 000000000000..ba76626bfcbb --- /dev/null +++ b/src/client/common/utils/charCode.ts @@ -0,0 +1,453 @@ +//!!! DO NOT modify, this file was COPIED from 'microsoft/vscode' + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/ + +/** + * An inlined enum containing useful character codes (to be used with String.charCodeAt). + * Please leave the const keyword such that it gets inlined when compiled to JavaScript! + */ +export const enum CharCode { + Null = 0, + /** + * The `\b` character. + */ + Backspace = 8, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, + + /** + * The   (no-break space) character. + * Unicode Character 'NO-BREAK SPACE' (U+00A0) + */ + NoBreakSpace = 160, + + U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent + U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent + U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent + U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde + U_Combining_Macron = 0x0304, // U+0304 Combining Macron + U_Combining_Overline = 0x0305, // U+0305 Combining Overline + U_Combining_Breve = 0x0306, // U+0306 Combining Breve + U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above + U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis + U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above + U_Combining_Ring_Above = 0x030a, // U+030A Combining Ring Above + U_Combining_Double_Acute_Accent = 0x030b, // U+030B Combining Double Acute Accent + U_Combining_Caron = 0x030c, // U+030C Combining Caron + U_Combining_Vertical_Line_Above = 0x030d, // U+030D Combining Vertical Line Above + U_Combining_Double_Vertical_Line_Above = 0x030e, // U+030E Combining Double Vertical Line Above + U_Combining_Double_Grave_Accent = 0x030f, // U+030F Combining Double Grave Accent + U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu + U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve + U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above + U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above + U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above + U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right + U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below + U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below + U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below + U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below + U_Combining_Left_Angle_Above = 0x031a, // U+031A Combining Left Angle Above + U_Combining_Horn = 0x031b, // U+031B Combining Horn + U_Combining_Left_Half_Ring_Below = 0x031c, // U+031C Combining Left Half Ring Below + U_Combining_Up_Tack_Below = 0x031d, // U+031D Combining Up Tack Below + U_Combining_Down_Tack_Below = 0x031e, // U+031E Combining Down Tack Below + U_Combining_Plus_Sign_Below = 0x031f, // U+031F Combining Plus Sign Below + U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below + U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below + U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below + U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below + U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below + U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below + U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below + U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla + U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek + U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below + U_Combining_Bridge_Below = 0x032a, // U+032A Combining Bridge Below + U_Combining_Inverted_Double_Arch_Below = 0x032b, // U+032B Combining Inverted Double Arch Below + U_Combining_Caron_Below = 0x032c, // U+032C Combining Caron Below + U_Combining_Circumflex_Accent_Below = 0x032d, // U+032D Combining Circumflex Accent Below + U_Combining_Breve_Below = 0x032e, // U+032E Combining Breve Below + U_Combining_Inverted_Breve_Below = 0x032f, // U+032F Combining Inverted Breve Below + U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below + U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below + U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line + U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line + U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay + U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay + U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay + U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay + U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay + U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below + U_Combining_Inverted_Bridge_Below = 0x033a, // U+033A Combining Inverted Bridge Below + U_Combining_Square_Below = 0x033b, // U+033B Combining Square Below + U_Combining_Seagull_Below = 0x033c, // U+033C Combining Seagull Below + U_Combining_X_Above = 0x033d, // U+033D Combining X Above + U_Combining_Vertical_Tilde = 0x033e, // U+033E Combining Vertical Tilde + U_Combining_Double_Overline = 0x033f, // U+033F Combining Double Overline + U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark + U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark + U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni + U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis + U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos + U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni + U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above + U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below + U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below + U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below + U_Combining_Not_Tilde_Above = 0x034a, // U+034A Combining Not Tilde Above + U_Combining_Homothetic_Above = 0x034b, // U+034B Combining Homothetic Above + U_Combining_Almost_Equal_To_Above = 0x034c, // U+034C Combining Almost Equal To Above + U_Combining_Left_Right_Arrow_Below = 0x034d, // U+034D Combining Left Right Arrow Below + U_Combining_Upwards_Arrow_Below = 0x034e, // U+034E Combining Upwards Arrow Below + U_Combining_Grapheme_Joiner = 0x034f, // U+034F Combining Grapheme Joiner + U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above + U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above + U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata + U_Combining_X_Below = 0x0353, // U+0353 Combining X Below + U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below + U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below + U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below + U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above + U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right + U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below + U_Combining_Double_Ring_Below = 0x035a, // U+035A Combining Double Ring Below + U_Combining_Zigzag_Above = 0x035b, // U+035B Combining Zigzag Above + U_Combining_Double_Breve_Below = 0x035c, // U+035C Combining Double Breve Below + U_Combining_Double_Breve = 0x035d, // U+035D Combining Double Breve + U_Combining_Double_Macron = 0x035e, // U+035E Combining Double Macron + U_Combining_Double_Macron_Below = 0x035f, // U+035F Combining Double Macron Below + U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde + U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve + U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below + U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A + U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E + U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I + U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O + U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U + U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C + U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D + U_Combining_Latin_Small_Letter_H = 0x036a, // U+036A Combining Latin Small Letter H + U_Combining_Latin_Small_Letter_M = 0x036b, // U+036B Combining Latin Small Letter M + U_Combining_Latin_Small_Letter_R = 0x036c, // U+036C Combining Latin Small Letter R + U_Combining_Latin_Small_Letter_T = 0x036d, // U+036D Combining Latin Small Letter T + U_Combining_Latin_Small_Letter_V = 0x036e, // U+036E Combining Latin Small Letter V + U_Combining_Latin_Small_Letter_X = 0x036f, // U+036F Combining Latin Small Letter X + + /** + * Unicode Character 'LINE SEPARATOR' (U+2028) + * http://www.fileformat.info/info/unicode/char/2028/index.htm + */ + LINE_SEPARATOR = 0x2028, + /** + * Unicode Character 'PARAGRAPH SEPARATOR' (U+2029) + * http://www.fileformat.info/info/unicode/char/2029/index.htm + */ + PARAGRAPH_SEPARATOR = 0x2029, + /** + * Unicode Character 'NEXT LINE' (U+0085) + * http://www.fileformat.info/info/unicode/char/0085/index.htm + */ + NEXT_LINE = 0x0085, + + // http://www.fileformat.info/info/unicode/category/Sk/list.htm + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + U_CIRCUMFLEX = 0x005e, // U+005E CIRCUMFLEX + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT + U_DIAERESIS = 0x00a8, // U+00A8 DIAERESIS + U_MACRON = 0x00af, // U+00AF MACRON + U_ACUTE_ACCENT = 0x00b4, // U+00B4 ACUTE ACCENT + U_CEDILLA = 0x00b8, // U+00B8 CEDILLA + U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02c2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD + U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02c3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD + U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02c4, // U+02C4 MODIFIER LETTER UP ARROWHEAD + U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02c5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD + U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02d2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING + U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02d3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING + U_MODIFIER_LETTER_UP_TACK = 0x02d4, // U+02D4 MODIFIER LETTER UP TACK + U_MODIFIER_LETTER_DOWN_TACK = 0x02d5, // U+02D5 MODIFIER LETTER DOWN TACK + U_MODIFIER_LETTER_PLUS_SIGN = 0x02d6, // U+02D6 MODIFIER LETTER PLUS SIGN + U_MODIFIER_LETTER_MINUS_SIGN = 0x02d7, // U+02D7 MODIFIER LETTER MINUS SIGN + U_BREVE = 0x02d8, // U+02D8 BREVE + U_DOT_ABOVE = 0x02d9, // U+02D9 DOT ABOVE + U_RING_ABOVE = 0x02da, // U+02DA RING ABOVE + U_OGONEK = 0x02db, // U+02DB OGONEK + U_SMALL_TILDE = 0x02dc, // U+02DC SMALL TILDE + U_DOUBLE_ACUTE_ACCENT = 0x02dd, // U+02DD DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02de, // U+02DE MODIFIER LETTER RHOTIC HOOK + U_MODIFIER_LETTER_CROSS_ACCENT = 0x02df, // U+02DF MODIFIER LETTER CROSS ACCENT + U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02e5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR + U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02e6, // U+02E6 MODIFIER LETTER HIGH TONE BAR + U_MODIFIER_LETTER_MID_TONE_BAR = 0x02e7, // U+02E7 MODIFIER LETTER MID TONE BAR + U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02e8, // U+02E8 MODIFIER LETTER LOW TONE BAR + U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02e9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR + U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02ea, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK + U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02eb, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK + U_MODIFIER_LETTER_UNASPIRATED = 0x02ed, // U+02ED MODIFIER LETTER UNASPIRATED + U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02ef, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD + U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02f0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD + U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02f1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD + U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02f2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD + U_MODIFIER_LETTER_LOW_RING = 0x02f3, // U+02F3 MODIFIER LETTER LOW RING + U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02f4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02f5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02f6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_LOW_TILDE = 0x02f7, // U+02F7 MODIFIER LETTER LOW TILDE + U_MODIFIER_LETTER_RAISED_COLON = 0x02f8, // U+02F8 MODIFIER LETTER RAISED COLON + U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02f9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE + U_MODIFIER_LETTER_END_HIGH_TONE = 0x02fa, // U+02FA MODIFIER LETTER END HIGH TONE + U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02fb, // U+02FB MODIFIER LETTER BEGIN LOW TONE + U_MODIFIER_LETTER_END_LOW_TONE = 0x02fc, // U+02FC MODIFIER LETTER END LOW TONE + U_MODIFIER_LETTER_SHELF = 0x02fd, // U+02FD MODIFIER LETTER SHELF + U_MODIFIER_LETTER_OPEN_SHELF = 0x02fe, // U+02FE MODIFIER LETTER OPEN SHELF + U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02ff, // U+02FF MODIFIER LETTER LOW LEFT ARROW + U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN + U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS + U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS + U_GREEK_KORONIS = 0x1fbd, // U+1FBD GREEK KORONIS + U_GREEK_PSILI = 0x1fbf, // U+1FBF GREEK PSILI + U_GREEK_PERISPOMENI = 0x1fc0, // U+1FC0 GREEK PERISPOMENI + U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1fc1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI + U_GREEK_PSILI_AND_VARIA = 0x1fcd, // U+1FCD GREEK PSILI AND VARIA + U_GREEK_PSILI_AND_OXIA = 0x1fce, // U+1FCE GREEK PSILI AND OXIA + U_GREEK_PSILI_AND_PERISPOMENI = 0x1fcf, // U+1FCF GREEK PSILI AND PERISPOMENI + U_GREEK_DASIA_AND_VARIA = 0x1fdd, // U+1FDD GREEK DASIA AND VARIA + U_GREEK_DASIA_AND_OXIA = 0x1fde, // U+1FDE GREEK DASIA AND OXIA + U_GREEK_DASIA_AND_PERISPOMENI = 0x1fdf, // U+1FDF GREEK DASIA AND PERISPOMENI + U_GREEK_DIALYTIKA_AND_VARIA = 0x1fed, // U+1FED GREEK DIALYTIKA AND VARIA + U_GREEK_DIALYTIKA_AND_OXIA = 0x1fee, // U+1FEE GREEK DIALYTIKA AND OXIA + U_GREEK_VARIA = 0x1fef, // U+1FEF GREEK VARIA + U_GREEK_OXIA = 0x1ffd, // U+1FFD GREEK OXIA + U_GREEK_DASIA = 0x1ffe, // U+1FFE GREEK DASIA + + U_IDEOGRAPHIC_FULL_STOP = 0x3002, // U+3002 IDEOGRAPHIC FULL STOP + U_LEFT_CORNER_BRACKET = 0x300c, // U+300C LEFT CORNER BRACKET + U_RIGHT_CORNER_BRACKET = 0x300d, // U+300D RIGHT CORNER BRACKET + U_LEFT_BLACK_LENTICULAR_BRACKET = 0x3010, // U+3010 LEFT BLACK LENTICULAR BRACKET + U_RIGHT_BLACK_LENTICULAR_BRACKET = 0x3011, // U+3011 RIGHT BLACK LENTICULAR BRACKET + + U_OVERLINE = 0x203e, // Unicode Character 'OVERLINE' + + /** + * UTF-8 BOM + * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) + * http://www.fileformat.info/info/unicode/char/feff/index.htm + */ + UTF8_BOM = 65279, + + U_FULLWIDTH_SEMICOLON = 0xff1b, // U+FF1B FULLWIDTH SEMICOLON + U_FULLWIDTH_COMMA = 0xff0c, // U+FF0C FULLWIDTH COMMA +} diff --git a/src/test/mocks/mockDocument.ts b/src/test/mocks/mockDocument.ts index 811c591420bd..a9cd39985311 100644 --- a/src/test/mocks/mockDocument.ts +++ b/src/test/mocks/mockDocument.ts @@ -84,6 +84,7 @@ export class MockDocument implements TextDocument { this._onSave = onSave; this._language = language ?? this._language; } + encoding: string = 'utf8'; public setContent(contents: string): void { this._contents = contents; diff --git a/src/test/mocks/vsc/extHostedTypes.ts b/src/test/mocks/vsc/extHostedTypes.ts index f87b50174150..c2c1188c3449 100644 --- a/src/test/mocks/vsc/extHostedTypes.ts +++ b/src/test/mocks/vsc/extHostedTypes.ts @@ -2000,12 +2000,31 @@ export enum TreeItemCollapsibleState { Expanded = 2, } +/** + * Represents an icon in the UI. This is either an uri, separate uris for the light- and dark-themes, + * or a {@link ThemeIcon theme icon}. + */ +export type IconPath = + | vscUri.URI + | { + /** + * The icon path for the light theme. + */ + light: vscUri.URI; + /** + * The icon path for the dark theme. + */ + dark: vscUri.URI; + } + | ThemeIcon; + export class TreeItem { - label?: string; + label?: string | vscode.TreeItemLabel; + id?: string; resourceUri?: vscUri.URI; - iconPath?: string | vscUri.URI | { light: string | vscUri.URI; dark: string | vscUri.URI }; + iconPath?: string | IconPath; command?: vscode.Command; From 43676148f0246d94696ec8bba4974bc60d395834 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 15 May 2025 07:11:24 +1000 Subject: [PATCH 0934/1136] Port Python Env Tools (#25066) * Tested with .venv, global conda, local conda and global Python * Tested both tools ![Screenshot 2025-05-14 at 11 54 44](https://github.com/user-attachments/assets/dab561f7-a66d-4157-8e04-86be433fb0eb) ![Screenshot 2025-05-14 at 11 57 39](https://github.com/user-attachments/assets/ce727718-56fb-4948-9764-bc8df3dda565) ![Screenshot 2025-05-14 at 11 57 49](https://github.com/user-attachments/assets/f1a5c643-b079-4306-be89-654a2043e8b4) --- package.json | 60 ++++++ package.nls.json | 4 +- src/client/chat/getPythonEnvTool.ts | 199 ++++++++++++++++++ src/client/chat/index.ts | 47 +++++ src/client/chat/installPackagesTool.ts | 132 ++++++++++++ src/client/chat/pipListUtils.ts | 32 +++ src/client/chat/utils.ts | 27 +++ src/client/extension.ts | 2 + .../common/environmentManagers/conda.ts | 9 + 9 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 src/client/chat/getPythonEnvTool.ts create mode 100644 src/client/chat/index.ts create mode 100644 src/client/chat/installPackagesTool.ts create mode 100644 src/client/chat/pipListUtils.ts create mode 100644 src/client/chat/utils.ts diff --git a/package.json b/package.json index c5f8fa74b544..1e6b5691cd16 100644 --- a/package.json +++ b/package.json @@ -1462,6 +1462,66 @@ "fileMatch": "meta.yaml", "url": "./schemas/conda-meta.json" } + ], + "languageModelTools": [ + { + "name": "python_environment", + "displayName": "Get Python Environment Information", + "userDescription": "%python.languageModelTools.python_environment.userDescription%", + "modelDescription": "Provides details about the Python environment for a specified file or workspace, including environment type, Python version, run command, and installed packages with their versions. Use this tool to determine the correct command for executing Python code in this workspace.", + "toolReferenceName": "pythonGetEnvironmentInfo", + "tags": [ + "ms-python.python" + ], + "icon": "$(files)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string" + } + }, + "description": "The path to the Python file or workspace to get the environment information for.", + "required": [ + "resourcePath" + ] + }, + "when": "!pythonEnvExtensionInstalled" + }, + { + "name": "python_install_package", + "displayName": "Install Python Package", + "userDescription": "%python.languageModelTools.python_install_package.userDescription%", + "modelDescription": "Installs Python packages in the given workspace. Use this tool to install packages in the user's chosen environment.", + "toolReferenceName": "pythonInstallPackage", + "tags": [ + "ms-python.python" + ], + "icon": "$(package)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "packageList": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of packages to install." + }, + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace to get the environment information for." + } + }, + "required": [ + "packageList", + "resourcePath" + ] + }, + "when": "!pythonEnvExtensionInstalled" + } ] }, "copilot": { diff --git a/package.nls.json b/package.nls.json index 6266bd67de50..22e5cf4fd8ad 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,12 +1,14 @@ { "python.command.python.startTerminalREPL.title": "Start Terminal REPL", + "python.languageModelTools.python_environment.userDescription": "Get Python environment info for a file or path, including version, packages, and the command to run it.", + "python.languageModelTools.python_install_package.userDescription": "Installs Python packages in the given workspace.", "python.command.python.startNativeREPL.title": "Start Native Python REPL", "python.command.python.createEnvironment.title": "Create Environment...", "python.command.python.createNewFile.title": "New Python File", "python.command.python.createTerminal.title": "Create Terminal", "python.command.python.execInTerminal.title": "Run Python File in Terminal", "python.command.python.execInTerminalIcon.title": "Run Python File", - "python.command.python.execInDedicatedTerminal.title": "Run Python File in Dedicated Terminal", + "python.command.python.execInDedicatedTerminal.title": "Run Python File in Dedicated Terminal", "python.command.python.setInterpreter.title": "Select Interpreter", "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", "python.command.python.viewOutput.title": "Show Output", diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts new file mode 100644 index 000000000000..27a3448fbaf6 --- /dev/null +++ b/src/client/chat/getPythonEnvTool.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationError, + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, +} from 'vscode'; +import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { IProcessService, IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; +import { raceCancellationError } from './utils'; +import { resolveFilePath } from './utils'; +import { parsePipList } from './pipListUtils'; +import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; +import { traceError } from '../logging'; + +export interface IResourceReference { + resourcePath: string; +} + +interface EnvironmentInfo { + type: string; // e.g. conda, venv, virtualenv, sys + version: string; + runCommand: string; + packages: string[] | string; //include versions too +} + +/** + * A tool to get the information about the Python environment. + */ +export class GetEnvironmentInfoTool implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly pythonExecFactory: IPythonExecutionFactory; + private readonly processServiceFactory: IProcessServiceFactory; + public static readonly toolName = 'python_environment'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + ) { + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.pythonExecFactory = this.serviceContainer.get(IPythonExecutionFactory); + this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + } + /** + * Invokes the tool to get the information about the Python environment. + * @param options - The invocation options containing the file path. + * @param token - The cancellation token. + * @returns The result containing the information about the Python environment or an error message. + */ + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + + // environment info set to default values + const envInfo: EnvironmentInfo = { + type: 'no type found', + version: 'no version found', + packages: 'no packages found', + runCommand: 'no run command found', + }; + + try { + // environment + const envPath = this.api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new Error('No environment found for the provided resource path: ' + resourcePath.fsPath); + } + const cmd = await raceCancellationError( + this.terminalExecutionService.getExecutableInfo(resourcePath), + token, + ); + const executable = cmd.pythonExecutable; + envInfo.runCommand = cmd.args.length > 0 ? `${cmd.command} ${cmd.args.join(' ')}` : executable; + envInfo.version = environment.version.sysVersion; + + const isConda = (environment.environment?.type || '').toLowerCase() === 'conda'; + envInfo.packages = isConda + ? await raceCancellationError( + listCondaPackages( + this.pythonExecFactory, + environment, + resourcePath, + await raceCancellationError(this.processServiceFactory.create(resourcePath), token), + ), + token, + ) + : await raceCancellationError(listPipPackages(this.pythonExecFactory, resourcePath), token); + + // format and return + return new LanguageModelToolResult([BuildEnvironmentInfoContent(envInfo)]); + } catch (error) { + if (error instanceof CancellationError) { + throw error; + } + const errorMessage: string = `An error occurred while fetching environment information: ${error}`; + const partialContent = BuildEnvironmentInfoContent(envInfo); + return new LanguageModelToolResult([ + new LanguageModelTextPart(`${errorMessage}\n\n${partialContent.value}`), + ]); + } + } + + async prepareInvocation?( + _options: LanguageModelToolInvocationPrepareOptions, + _token: CancellationToken, + ): Promise { + return { + invocationMessage: l10n.t('Fetching Python environment information'), + }; + } +} + +function BuildEnvironmentInfoContent(envInfo: EnvironmentInfo): LanguageModelTextPart { + // Create a formatted string that looks like JSON but preserves comments + let envTypeDescriptor: string = `This environment is managed by ${envInfo.type} environment manager. Use the install tool to install packages into this environment.`; + + // TODO: If this is setup as python.defaultInterpreterPath, then do not include this message. + if (envInfo.type === 'system') { + envTypeDescriptor = + 'System pythons are pythons that ship with the OS or are installed globally. These python installs may be used by the OS for running services and core functionality. Confirm with the user before installing packages into this environment, as it can lead to issues with any services on the OS.'; + } + const content = `{ + // ${JSON.stringify(envTypeDescriptor)} + "environmentType": ${JSON.stringify(envInfo.type)}, + // Python version of the environment + "pythonVersion": ${JSON.stringify(envInfo.version)}, + // Use this command to run Python script or code in the terminal. + "runCommand": ${JSON.stringify(envInfo.runCommand)}, + // Installed Python packages, each in the format or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. + "packages": ${JSON.stringify(Array.isArray(envInfo.packages) ? envInfo.packages : envInfo.packages, null, 2)} +}`; + + return new LanguageModelTextPart(content); +} + +async function listPipPackages(execFactory: IPythonExecutionFactory, resource: Uri) { + // Add option --format to subcommand list of pip cache, with abspath choice to output the full path of a wheel file. (#8355) + // Added in 202. Thats almost 5 years ago. When Python 3.8 was released. + const exec = await execFactory.createActivatedEnvironment({ allowEnvironmentFetchExceptions: true, resource }); + const output = await exec.execModule('pip', ['list'], { throwOnStdErr: false, encoding: 'utf8' }); + return parsePipList(output.stdout).map((pkg) => (pkg.version ? `${pkg.name} (${pkg.version})` : pkg.name)); +} + +async function listCondaPackages( + execFactory: IPythonExecutionFactory, + env: ResolvedEnvironment, + resource: Uri, + processService: IProcessService, +) { + const conda = await Conda.getConda(); + if (!conda) { + traceError('Conda is not installed, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + if (!env.executable.uri) { + traceError('Conda environment executable not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const condaEnv = await conda.getCondaEnvironment(env.executable.uri.fsPath); + if (!condaEnv) { + traceError('Conda environment not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const cmd = await conda.getListPythonPackagesArgs(condaEnv, true); + if (!cmd) { + traceError('Conda list command not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const output = await processService.exec(cmd[0], cmd.slice(1), { shell: true }); + if (!output.stdout) { + traceError('Unable to get conda packages, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const content = output.stdout.split(/\r?\n/).filter((l) => !l.startsWith('#')); + const packages: string[] = []; + content.forEach((l) => { + const parts = l.split(' ').filter((p) => p.length > 0); + if (parts.length === 3) { + packages.push(`${parts[0]} (${parts[1]})`); + } + }); + return packages; +} diff --git a/src/client/chat/index.ts b/src/client/chat/index.ts new file mode 100644 index 000000000000..918774911107 --- /dev/null +++ b/src/client/chat/index.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { commands, extensions, lm } from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { GetEnvironmentInfoTool } from './getPythonEnvTool'; +import { InstallPackagesTool } from './installPackagesTool'; +import { IExtensionContext } from '../common/types'; +import { DisposableStore } from '../common/utils/resourceLifecycle'; +import { ENVS_EXTENSION_ID } from '../envExt/api.internal'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; + +export function registerTools( + context: IExtensionContext, + discoverApi: IDiscoveryAPI, + environmentsApi: PythonExtension['environments'], + serviceContainer: IServiceContainer, +) { + if (extensions.getExtension(ENVS_EXTENSION_ID)) { + return; + } + const contextKey = 'pythonEnvExtensionInstalled'; + commands.executeCommand('setContext', contextKey, false); + const ourTools = new DisposableStore(); + context.subscriptions.push(ourTools); + + ourTools.add( + lm.registerTool(GetEnvironmentInfoTool.toolName, new GetEnvironmentInfoTool(environmentsApi, serviceContainer)), + ); + ourTools.add( + lm.registerTool( + InstallPackagesTool.toolName, + new InstallPackagesTool(environmentsApi, serviceContainer, discoverApi), + ), + ); + ourTools.add( + extensions.onDidChange(() => { + const envExtension = extensions.getExtension(ENVS_EXTENSION_ID); + if (envExtension) { + envExtension.activate(); + commands.executeCommand('setContext', contextKey, true); + ourTools.dispose(); + } + }), + ); +} diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts new file mode 100644 index 000000000000..80328553577a --- /dev/null +++ b/src/client/chat/installPackagesTool.ts @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationError, + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { raceCancellationError } from './utils'; +import { resolveFilePath } from './utils'; +import { IModuleInstaller } from '../common/installer/types'; +import { ModuleInstallerType } from '../pythonEnvironments/info'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; + +export interface IInstallPackageArgs { + resourcePath: string; + packageList: string[]; +} + +export class InstallPackagesTool implements LanguageModelTool { + public static readonly toolName = 'python_install_package'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly discovery: IDiscoveryAPI, + ) {} + /** + * Invokes the tool to get the information about the Python environment. + * @param options - The invocation options containing the file path. + * @param token - The cancellation token. + * @returns The result containing the information about the Python environment or an error message. + */ + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + const packageCount = options.input.packageList.length; + const packagePlurality = packageCount === 1 ? 'package' : 'packages'; + + try { + // environment + const envPath = this.api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new Error('No environment found for the provided resource path: ' + resourcePath.fsPath); + } + const isConda = (environment.environment?.type || '').toLowerCase() === 'conda'; + const installers = this.serviceContainer.getAll(IModuleInstaller); + const installerType = isConda ? ModuleInstallerType.Conda : ModuleInstallerType.Pip; + const installer = installers.find((i) => i.type === installerType); + if (!installer) { + throw new Error(`No installer found for the environment type: ${installerType}`); + } + if (!installer.isSupported(resourcePath)) { + throw new Error(`Installer ${installerType} not supported for the environment type: ${installerType}`); + } + for (const packageName of options.input.packageList) { + await installer.installModule(packageName, resourcePath, token, undefined, { installAsProcess: true }); + } + + // format and return + const resultMessage = `Successfully installed ${packagePlurality}: ${options.input.packageList.join(', ')}`; + return new LanguageModelToolResult([new LanguageModelTextPart(resultMessage)]); + } catch (error) { + if (error instanceof CancellationError) { + throw error; + } + const errorMessage = `An error occurred while installing ${packagePlurality}: ${error}`; + return new LanguageModelToolResult([new LanguageModelTextPart(errorMessage)]); + } + } + + async prepareInvocation?( + options: LanguageModelToolInvocationPrepareOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + const packageCount = options.input.packageList.length; + + const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); + let title = ''; + let invocationMessage = ''; + const message = + packageCount === 1 + ? '' + : l10n.t(`The following packages will be installed: {0}`, options.input.packageList.sort().join(', ')); + if (envName) { + title = + packageCount === 1 + ? l10n.t(`Install {0} in {1}?`, options.input.packageList[0], envName) + : l10n.t(`Install packages in {0}?`, envName); + invocationMessage = + packageCount === 1 + ? l10n.t(`Installing {0} in {1}`, options.input.packageList[0], envName) + : l10n.t(`Installing packages {0} in {1}`, options.input.packageList.sort().join(', '), envName); + } else { + title = + options.input.packageList.length === 1 + ? l10n.t(`Install Python package '{0}'?`, options.input.packageList[0]) + : l10n.t(`Install Python packages?`); + invocationMessage = + packageCount === 1 + ? l10n.t(`Installing Python package '{0}'`, options.input.packageList[0]) + : l10n.t(`Installing Python packages: {0}`, options.input.packageList.sort().join(', ')); + } + + return { + confirmationMessages: { title, message }, + invocationMessage, + }; + } +} + +async function getEnvDisplayName(discovery: IDiscoveryAPI, resource: Uri, api: PythonExtension['environments']) { + try { + const envPath = api.getActiveEnvironmentPath(resource); + const env = await discovery.resolveEnv(envPath.path); + return env?.display || env?.name; + } catch { + return; + } +} diff --git a/src/client/chat/pipListUtils.ts b/src/client/chat/pipListUtils.ts new file mode 100644 index 000000000000..0112d88c53ab --- /dev/null +++ b/src/client/chat/pipListUtils.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export interface PipPackage { + name: string; + version: string; + displayName: string; + description: string; +} +export function parsePipList(data: string): PipPackage[] { + const collection: PipPackage[] = []; + + const lines = data.split('\n').splice(2); + for (let line of lines) { + if (line.trim() === '' || line.startsWith('Package') || line.startsWith('----') || line.startsWith('[')) { + continue; + } + const parts = line.split(' ').filter((e) => e); + if (parts.length > 1) { + const name = parts[0].trim(); + const version = parts[1].trim(); + const pkg = { + name, + version, + displayName: name, + description: version, + }; + collection.push(pkg); + } + } + return collection; +} diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts new file mode 100644 index 000000000000..05d92337df43 --- /dev/null +++ b/src/client/chat/utils.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationError, CancellationToken, Uri } from 'vscode'; + +export function resolveFilePath(filepath: string): Uri { + // starts with a scheme + try { + return Uri.parse(filepath); + } catch (e) { + return Uri.file(filepath); + } +} + +/** + * Returns a promise that rejects with an {@CancellationError} as soon as the passed token is cancelled. + * @see {@link raceCancellation} + */ +export function raceCancellationError(promise: Promise, token: CancellationToken): Promise { + return new Promise((resolve, reject) => { + const ref = token.onCancellationRequested(() => { + ref.dispose(); + reject(new CancellationError()); + }); + promise.then(resolve, reject).finally(() => ref.dispose()); + }); +} diff --git a/src/client/extension.ts b/src/client/extension.ts index 521a8878ab63..1a07a4b1e3b2 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -43,6 +43,7 @@ import { disposeAll } from './common/utils/resourceLifecycle'; import { ProposedExtensionAPI } from './proposedApiTypes'; import { buildProposedApi } from './proposedApi'; import { GLOBAL_PERSISTENT_KEYS } from './common/persistentState'; +import { registerTools } from './chat'; durations.codeLoadingTime = stopWatch.elapsedTime; @@ -162,6 +163,7 @@ async function activateUnsafe( components.pythonEnvs, ); const proposedApi = buildProposedApi(components.pythonEnvs, ext.legacyIOC.serviceContainer); + registerTools(context, components.pythonEnvs, api.environments, ext.legacyIOC.serviceContainer); return [{ ...api, ...proposedApi }, activationPromise, ext.legacyIOC.serviceContainer]; } diff --git a/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/src/client/pythonEnvironments/common/environmentManagers/conda.ts index 2fd3f3207fc5..c1bfd7d68bc2 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -579,6 +579,15 @@ export class Conda { return [...python, OUTPUT_MARKER_SCRIPT]; } + public async getListPythonPackagesArgs( + env: CondaEnvInfo, + forShellExecution?: boolean, + ): Promise { + const args = ['-p', env.prefix]; + + return [forShellExecution ? this.shellCommand : this.command, 'list', ...args]; + } + /** * Return the conda version. The version info is cached. */ From 84280b0069b3b1b4ee638086e3821cc7bf1bfc2f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 15 May 2025 06:30:21 -0700 Subject: [PATCH 0935/1136] fix regression with import packaging for branch coverage (#25070) fixes https://github.com/Microsoft/vscode-python/issues/25044 --- python_files/unittestadapter/execution.py | 9 +++++++-- python_files/vscode_pytest/__init__.py | 8 +++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/python_files/unittestadapter/execution.py b/python_files/unittestadapter/execution.py index 8df2f279aa71..176703c20ce0 100644 --- a/python_files/unittestadapter/execution.py +++ b/python_files/unittestadapter/execution.py @@ -12,8 +12,6 @@ from types import TracebackType from typing import Dict, List, Optional, Set, Tuple, Type, Union -from packaging.version import Version - # Adds the scripts directory to the PATH as a workaround for enabling shell for test execution. path_var_name = "PATH" if "PATH" in os.environ else "Path" os.environ[path_var_name] = ( @@ -326,6 +324,13 @@ def send_run_data(raw_data, test_run_pipe): ) import coverage + # insert "python_files/lib/python" into the path so packaging can be imported + python_files_dir = pathlib.Path(__file__).parent.parent + bundled_dir = pathlib.Path(python_files_dir / "lib" / "python") + sys.path.append(os.fspath(bundled_dir)) + + from packaging.version import Version + coverage_version = Version(coverage.__version__) # only include branches if coverage version is 7.7.0 or greater (as this was when the api saves) if coverage_version >= Version("7.7.0"): diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 649e5bc59058..18469cd0627f 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -13,7 +13,6 @@ from typing import TYPE_CHECKING, Any, Dict, Generator, Literal, TypedDict import pytest -from packaging.version import Version if TYPE_CHECKING: from pluggy import Result @@ -443,6 +442,13 @@ def pytest_sessionfinish(session, exitstatus): # load the report and build the json result to return import coverage + # insert "python_files/lib/python" into the path so packaging can be imported + python_files_dir = pathlib.Path(__file__).parent.parent + bundled_dir = pathlib.Path(python_files_dir / "lib" / "python") + sys.path.append(os.fspath(bundled_dir)) + + from packaging.version import Version + coverage_version = Version(coverage.__version__) global INCLUDE_BRANCHES # only include branches if coverage version is 7.7.0 or greater (as this was when the api saves) From d06eb18f24cbc655cd6f83c70f4982b0c01f4f97 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 15 May 2025 08:25:42 -0700 Subject: [PATCH 0936/1136] chore: ensure `.env` files are excluded from vsix --- .vscodeignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscodeignore b/.vscodeignore index b94baaba1a19..d636ab48f361 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,5 +1,6 @@ **/*.map **/*.analyzer.html +**/.env *.vsix .editorconfig .env From 9fd7b9b0357b677f3b7121bc9ec068e22e66a698 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 16 May 2025 08:32:15 +1000 Subject: [PATCH 0937/1136] Rename llm tools (#25078) --- package.json | 4 ++-- src/client/chat/getPythonEnvTool.ts | 2 +- src/client/chat/installPackagesTool.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 1e6b5691cd16..af2c2b33d7e0 100644 --- a/package.json +++ b/package.json @@ -1465,7 +1465,7 @@ ], "languageModelTools": [ { - "name": "python_environment", + "name": "get_python_environment", "displayName": "Get Python Environment Information", "userDescription": "%python.languageModelTools.python_environment.userDescription%", "modelDescription": "Provides details about the Python environment for a specified file or workspace, including environment type, Python version, run command, and installed packages with their versions. Use this tool to determine the correct command for executing Python code in this workspace.", @@ -1490,7 +1490,7 @@ "when": "!pythonEnvExtensionInstalled" }, { - "name": "python_install_package", + "name": "install_python_package", "displayName": "Install Python Package", "userDescription": "%python.languageModelTools.python_install_package.userDescription%", "modelDescription": "Installs Python packages in the given workspace. Use this tool to install packages in the user's chosen environment.", diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index 27a3448fbaf6..43bd254efa28 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -42,7 +42,7 @@ export class GetEnvironmentInfoTool implements LanguageModelTool { - public static readonly toolName = 'python_install_package'; + public static readonly toolName = 'install_python_package'; constructor( private readonly api: PythonExtension['environments'], private readonly serviceContainer: IServiceContainer, From 5045cdd6101cbf9b7786a95a2b2ef8320484e6a7 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 16 May 2025 12:06:42 +1000 Subject: [PATCH 0938/1136] More specific llm tools (#25072) --- package.json | 63 +++++-- package.nls.json | 6 +- src/client/chat/getExecutableTool.ts | 82 +++++++++ src/client/chat/getPythonEnvTool.ts | 148 +++------------ src/client/chat/index.ts | 16 +- src/client/chat/installPackagesTool.ts | 17 +- src/client/chat/listPackagesTool.ts | 173 ++++++++++++++++++ src/client/chat/utils.ts | 95 +++++++++- src/test/common.ts | 2 +- .../pythonPathUpdaterFactory.unit.test.ts | 1 + .../testCancellationRunAdapters.unit.test.ts | 6 + 11 files changed, 455 insertions(+), 154 deletions(-) create mode 100644 src/client/chat/getExecutableTool.ts create mode 100644 src/client/chat/listPackagesTool.ts diff --git a/package.json b/package.json index af2c2b33d7e0..9d1cf544b692 100644 --- a/package.json +++ b/package.json @@ -1465,10 +1465,10 @@ ], "languageModelTools": [ { - "name": "get_python_environment", - "displayName": "Get Python Environment Information", - "userDescription": "%python.languageModelTools.python_environment.userDescription%", - "modelDescription": "Provides details about the Python environment for a specified file or workspace, including environment type, Python version, run command, and installed packages with their versions. Use this tool to determine the correct command for executing Python code in this workspace.", + "name": "get_python_environment_info", + "displayName": "Get Python Environment Info", + "userDescription": "%python.languageModelTools.get_python_environment_info.userDescription%", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Environment (conda, venv, etec), 2. Version of Python, 3. List of all installed packages with their versions. ", "toolReferenceName": "pythonGetEnvironmentInfo", "tags": [ "ms-python.python" @@ -1483,11 +1483,53 @@ } }, "description": "The path to the Python file or workspace to get the environment information for.", - "required": [ - "resourcePath" - ] + "required": [] + } + }, + { + "name": "get_python_executable", + "displayName": "Get Python Executable", + "userDescription": "%python.languageModelTools.get_python_executable.userDescription%", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. ALWAYS use this tool before executing any Python command in the terminal. This tool returns the details of how to construct the fully qualified path and or command including details such as arguments required to run Python in a terminal. Note: Instead of executing `python --version` or `python -c 'import sys; print(sys.executable)'`, use this tool to get the Python executable path to replace the `python` command. E.g. instead of using `python -c 'import sys; print(sys.executable)'`, use this tool to build the command `conda run -n -c 'import sys; print(sys.executable)'`.", + "toolReferenceName": "pythonExecutableCommand", + "tags": [ + "ms-python.python" + ], + "icon": "$(files)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string" + } + }, + "description": "The path to the Python file or workspace to get the executable information for. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace.", + "required": [] + } + }, + { + "name": "list_python_packages", + "displayName": "List Python Packages", + "userDescription": "%python.languageModelTools.list_python_packages.userDescription%", + "modelDescription": "This tool will retrieve the list of all installed packages installed in a Python Environment for the specified file or workspace. ALWAYS use this tool instead of executing Python command in the terminal to fetch the list of installed packages. WARNING: Packages installed can change over time, hence the list of packages returned by this tool may not be accurate. Use this tool to get the list of installed packages in a Python environment.", + "toolReferenceName": "listPythonPackages", + "tags": [ + "ms-python.python" + ], + "icon": "$(files)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string" + } + }, + "description": "The path to the Python file or workspace to list the packages. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace.", + "required": [] }, - "when": "!pythonEnvExtensionInstalled" + "when": "false" }, { "name": "install_python_package", @@ -1512,12 +1554,11 @@ }, "resourcePath": { "type": "string", - "description": "The path to the Python file or workspace to get the environment information for." + "description": "The path to the Python file or workspace into which the packages are installed. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace." } }, "required": [ - "packageList", - "resourcePath" + "packageList" ] }, "when": "!pythonEnvExtensionInstalled" diff --git a/package.nls.json b/package.nls.json index 22e5cf4fd8ad..a73deb3554da 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,7 +1,9 @@ { "python.command.python.startTerminalREPL.title": "Start Terminal REPL", - "python.languageModelTools.python_environment.userDescription": "Get Python environment info for a file or path, including version, packages, and the command to run it.", - "python.languageModelTools.python_install_package.userDescription": "Installs Python packages in the given workspace.", + "python.languageModelTools.get_python_environment_info.userDescription": "Get information for a Python Environment, such as Type, Version, Packages, and more.", + "python.languageModelTools.python_install_package.userDescription": "Installs Python packages in a Python Environment.", + "python.languageModelTools.get_python_executable.userDescription": "Get executable info for a Python Environment", + "python.languageModelTools.list_python_packages.userDescription": "Get a list of all installed packages in a Python Environment.", "python.command.python.startNativeREPL.title": "Start Native Python REPL", "python.command.python.createEnvironment.title": "Create Environment...", "python.command.python.createNewFile.title": "New Python File", diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts new file mode 100644 index 000000000000..af4ab214419a --- /dev/null +++ b/src/client/chat/getExecutableTool.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationError, + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { getEnvDisplayName, getEnvironmentDetails, raceCancellationError } from './utils'; +import { resolveFilePath } from './utils'; +import { traceError } from '../logging'; +import { ITerminalHelper } from '../common/terminal/types'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; + +export interface IResourceReference { + resourcePath?: string; +} + +export class GetExecutableTool implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'get_python_executable'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly discovery: IDiscoveryAPI, + ) { + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + } + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + + try { + const message = await getEnvironmentDetails( + resourcePath, + this.api, + this.terminalExecutionService, + this.terminalHelper, + undefined, + token, + ); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); + } catch (error) { + if (error instanceof CancellationError) { + throw error; + } + traceError('Error while getting environment information', error); + const errorMessage: string = `An error occurred while fetching environment information: ${error}`; + return new LanguageModelToolResult([new LanguageModelTextPart(errorMessage)]); + } + } + + async prepareInvocation?( + options: LanguageModelToolInvocationPrepareOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); + return { + invocationMessage: envName + ? l10n.t('Fetching Python executable information for {0}', envName) + : l10n.t('Fetching Python executable information'), + }; + } +} diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index 43bd254efa28..ef200239af9a 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -11,38 +11,27 @@ import { LanguageModelToolInvocationPrepareOptions, LanguageModelToolResult, PreparedToolInvocation, - Uri, } from 'vscode'; -import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; import { ICodeExecutionService } from '../terminals/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; -import { IProcessService, IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; -import { raceCancellationError } from './utils'; +import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; +import { getEnvironmentDetails, raceCancellationError } from './utils'; import { resolveFilePath } from './utils'; -import { parsePipList } from './pipListUtils'; -import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; -import { traceError } from '../logging'; +import { getPythonPackagesResponse } from './listPackagesTool'; +import { ITerminalHelper } from '../common/terminal/types'; export interface IResourceReference { - resourcePath: string; + resourcePath?: string; } -interface EnvironmentInfo { - type: string; // e.g. conda, venv, virtualenv, sys - version: string; - runCommand: string; - packages: string[] | string; //include versions too -} - -/** - * A tool to get the information about the Python environment. - */ export class GetEnvironmentInfoTool implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; private readonly pythonExecFactory: IPythonExecutionFactory; private readonly processServiceFactory: IProcessServiceFactory; - public static readonly toolName = 'get_python_environment'; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'get_python_environment_info'; constructor( private readonly api: PythonExtension['environments'], private readonly serviceContainer: IServiceContainer, @@ -53,6 +42,7 @@ export class GetEnvironmentInfoTool implements LanguageModelTool(IPythonExecutionFactory); this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); } /** * Invokes the tool to get the information about the Python environment. @@ -66,53 +56,37 @@ export class GetEnvironmentInfoTool implements LanguageModelTool { const resourcePath = resolveFilePath(options.input.resourcePath); - // environment info set to default values - const envInfo: EnvironmentInfo = { - type: 'no type found', - version: 'no version found', - packages: 'no packages found', - runCommand: 'no run command found', - }; - try { // environment const envPath = this.api.getActiveEnvironmentPath(resourcePath); const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); if (!environment || !environment.version) { - throw new Error('No environment found for the provided resource path: ' + resourcePath.fsPath); + throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); } - const cmd = await raceCancellationError( - this.terminalExecutionService.getExecutableInfo(resourcePath), + const packages = await getPythonPackagesResponse( + environment, + this.pythonExecFactory, + this.processServiceFactory, + resourcePath, token, ); - const executable = cmd.pythonExecutable; - envInfo.runCommand = cmd.args.length > 0 ? `${cmd.command} ${cmd.args.join(' ')}` : executable; - envInfo.version = environment.version.sysVersion; - const isConda = (environment.environment?.type || '').toLowerCase() === 'conda'; - envInfo.packages = isConda - ? await raceCancellationError( - listCondaPackages( - this.pythonExecFactory, - environment, - resourcePath, - await raceCancellationError(this.processServiceFactory.create(resourcePath), token), - ), - token, - ) - : await raceCancellationError(listPipPackages(this.pythonExecFactory, resourcePath), token); + const message = await getEnvironmentDetails( + resourcePath, + this.api, + this.terminalExecutionService, + this.terminalHelper, + packages, + token, + ); - // format and return - return new LanguageModelToolResult([BuildEnvironmentInfoContent(envInfo)]); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); } catch (error) { if (error instanceof CancellationError) { throw error; } const errorMessage: string = `An error occurred while fetching environment information: ${error}`; - const partialContent = BuildEnvironmentInfoContent(envInfo); - return new LanguageModelToolResult([ - new LanguageModelTextPart(`${errorMessage}\n\n${partialContent.value}`), - ]); + return new LanguageModelToolResult([new LanguageModelTextPart(errorMessage)]); } } @@ -125,75 +99,3 @@ export class GetEnvironmentInfoTool implements LanguageModelTool or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. - "packages": ${JSON.stringify(Array.isArray(envInfo.packages) ? envInfo.packages : envInfo.packages, null, 2)} -}`; - - return new LanguageModelTextPart(content); -} - -async function listPipPackages(execFactory: IPythonExecutionFactory, resource: Uri) { - // Add option --format to subcommand list of pip cache, with abspath choice to output the full path of a wheel file. (#8355) - // Added in 202. Thats almost 5 years ago. When Python 3.8 was released. - const exec = await execFactory.createActivatedEnvironment({ allowEnvironmentFetchExceptions: true, resource }); - const output = await exec.execModule('pip', ['list'], { throwOnStdErr: false, encoding: 'utf8' }); - return parsePipList(output.stdout).map((pkg) => (pkg.version ? `${pkg.name} (${pkg.version})` : pkg.name)); -} - -async function listCondaPackages( - execFactory: IPythonExecutionFactory, - env: ResolvedEnvironment, - resource: Uri, - processService: IProcessService, -) { - const conda = await Conda.getConda(); - if (!conda) { - traceError('Conda is not installed, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - if (!env.executable.uri) { - traceError('Conda environment executable not found, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - const condaEnv = await conda.getCondaEnvironment(env.executable.uri.fsPath); - if (!condaEnv) { - traceError('Conda environment not found, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - const cmd = await conda.getListPythonPackagesArgs(condaEnv, true); - if (!cmd) { - traceError('Conda list command not found, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - const output = await processService.exec(cmd[0], cmd.slice(1), { shell: true }); - if (!output.stdout) { - traceError('Unable to get conda packages, falling back to pip packages'); - return listPipPackages(execFactory, resource); - } - const content = output.stdout.split(/\r?\n/).filter((l) => !l.startsWith('#')); - const packages: string[] = []; - content.forEach((l) => { - const parts = l.split(' ').filter((p) => p.length > 0); - if (parts.length === 3) { - packages.push(`${parts[0]} (${parts[1]})`); - } - }); - return packages; -} diff --git a/src/client/chat/index.ts b/src/client/chat/index.ts index 918774911107..d51f0d1ade64 100644 --- a/src/client/chat/index.ts +++ b/src/client/chat/index.ts @@ -4,12 +4,14 @@ import { commands, extensions, lm } from 'vscode'; import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; -import { GetEnvironmentInfoTool } from './getPythonEnvTool'; import { InstallPackagesTool } from './installPackagesTool'; import { IExtensionContext } from '../common/types'; import { DisposableStore } from '../common/utils/resourceLifecycle'; import { ENVS_EXTENSION_ID } from '../envExt/api.internal'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { ListPythonPackagesTool } from './listPackagesTool'; +import { GetExecutableTool } from './getExecutableTool'; +import { GetEnvironmentInfoTool } from './getPythonEnvTool'; export function registerTools( context: IExtensionContext, @@ -28,6 +30,18 @@ export function registerTools( ourTools.add( lm.registerTool(GetEnvironmentInfoTool.toolName, new GetEnvironmentInfoTool(environmentsApi, serviceContainer)), ); + ourTools.add( + lm.registerTool( + GetExecutableTool.toolName, + new GetExecutableTool(environmentsApi, serviceContainer, discoverApi), + ), + ); + ourTools.add( + lm.registerTool( + ListPythonPackagesTool.toolName, + new ListPythonPackagesTool(environmentsApi, serviceContainer, discoverApi), + ), + ); ourTools.add( lm.registerTool( InstallPackagesTool.toolName, diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts index d0bfc3ce65de..a430525e1018 100644 --- a/src/client/chat/installPackagesTool.ts +++ b/src/client/chat/installPackagesTool.ts @@ -11,18 +11,17 @@ import { LanguageModelToolInvocationPrepareOptions, LanguageModelToolResult, PreparedToolInvocation, - Uri, } from 'vscode'; import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; -import { raceCancellationError } from './utils'; +import { getEnvDisplayName, raceCancellationError } from './utils'; import { resolveFilePath } from './utils'; import { IModuleInstaller } from '../common/installer/types'; import { ModuleInstallerType } from '../pythonEnvironments/info'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; export interface IInstallPackageArgs { - resourcePath: string; + resourcePath?: string; packageList: string[]; } @@ -52,7 +51,7 @@ export class InstallPackagesTool implements LanguageModelTool(IModuleInstaller); @@ -120,13 +119,3 @@ export class InstallPackagesTool implements LanguageModelTool { + private readonly pythonExecFactory: IPythonExecutionFactory; + private readonly processServiceFactory: IProcessServiceFactory; + public static readonly toolName = 'list_python_packages'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly discovery: IDiscoveryAPI, + ) { + this.pythonExecFactory = this.serviceContainer.get(IPythonExecutionFactory); + this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + } + + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + + try { + // environment + const envPath = this.api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); + if (!environment) { + throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); + } + + const message = await getPythonPackagesResponse( + environment, + this.pythonExecFactory, + this.processServiceFactory, + resourcePath, + token, + ); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); + } catch (error) { + if (error instanceof CancellationError) { + throw error; + } + return new LanguageModelToolResult([ + new LanguageModelTextPart(`An error occurred while fetching environment information: ${error}`), + ]); + } + } + + async prepareInvocation?( + options: LanguageModelToolInvocationPrepareOptions, + token: CancellationToken, + ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); + return { + invocationMessage: envName + ? l10n.t('Listing packages in {0}', envName) + : l10n.t('Fetching Python environment information'), + }; + } +} + +export async function getPythonPackagesResponse( + environment: ResolvedEnvironment, + pythonExecFactory: IPythonExecutionFactory, + processServiceFactory: IProcessServiceFactory, + resourcePath: Uri | undefined, + token: CancellationToken, +): Promise { + const packages = isCondaEnv(environment) + ? await raceCancellationError( + listCondaPackages( + pythonExecFactory, + environment, + resourcePath, + await raceCancellationError(processServiceFactory.create(resourcePath), token), + ), + token, + ) + : await raceCancellationError(listPipPackages(pythonExecFactory, resourcePath), token); + + if (!packages.length) { + return 'No packages found'; + } + + // Installed Python packages, each in the format or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. + const response = [ + 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', + ]; + packages.forEach((pkg) => { + const [name, version] = pkg; + response.push(version ? `- ${name} (${version})` : `- ${name}`); + }); + return response.join('\n'); +} + +async function listPipPackages( + execFactory: IPythonExecutionFactory, + resource: Uri | undefined, +): Promise<[string, string][]> { + // Add option --format to subcommand list of pip cache, with abspath choice to output the full path of a wheel file. (#8355) + // Added in 202. Thats almost 5 years ago. When Python 3.8 was released. + const exec = await execFactory.createActivatedEnvironment({ allowEnvironmentFetchExceptions: true, resource }); + const output = await exec.execModule('pip', ['list'], { throwOnStdErr: false, encoding: 'utf8' }); + return parsePipList(output.stdout).map((pkg) => [pkg.name, pkg.version]); +} + +async function listCondaPackages( + execFactory: IPythonExecutionFactory, + env: ResolvedEnvironment, + resource: Uri | undefined, + processService: IProcessService, +): Promise<[string, string][]> { + const conda = await Conda.getConda(); + if (!conda) { + traceError('Conda is not installed, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + if (!env.executable.uri) { + traceError('Conda environment executable not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const condaEnv = await conda.getCondaEnvironment(env.executable.uri.fsPath); + if (!condaEnv) { + traceError('Conda environment not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const cmd = await conda.getListPythonPackagesArgs(condaEnv, true); + if (!cmd) { + traceError('Conda list command not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const output = await processService.exec(cmd[0], cmd.slice(1), { shell: true }); + if (!output.stdout) { + traceError('Unable to get conda packages, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const content = output.stdout.split(/\r?\n/).filter((l) => !l.startsWith('#')); + const packages: [string, string][] = []; + content.forEach((l) => { + const parts = l.split(' ').filter((p) => p.length > 0); + if (parts.length >= 3) { + packages.push([parts[0], parts[1]]); + } + }); + return packages; +} diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts index 05d92337df43..6206e01ea655 100644 --- a/src/client/chat/utils.ts +++ b/src/client/chat/utils.ts @@ -1,9 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationError, CancellationToken, Uri } from 'vscode'; +import { CancellationError, CancellationToken, Uri, workspace } from 'vscode'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { ITerminalHelper, TerminalShellType } from '../common/terminal/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; -export function resolveFilePath(filepath: string): Uri { +export function resolveFilePath(filepath?: string): Uri | undefined { + if (!filepath) { + return workspace.workspaceFolders ? workspace.workspaceFolders[0].uri : undefined; + } // starts with a scheme try { return Uri.parse(filepath); @@ -25,3 +33,86 @@ export function raceCancellationError(promise: Promise, token: Cancellatio promise.then(resolve, reject).finally(() => ref.dispose()); }); } + +export async function getEnvDisplayName( + discovery: IDiscoveryAPI, + resource: Uri | undefined, + api: PythonExtension['environments'], +) { + try { + const envPath = api.getActiveEnvironmentPath(resource); + const env = await discovery.resolveEnv(envPath.path); + return env?.display || env?.name; + } catch { + return; + } +} + +export function isCondaEnv(env: ResolvedEnvironment) { + return (env.environment?.type || '').toLowerCase() === 'conda'; +} + +export async function getEnvironmentDetails( + resourcePath: Uri | undefined, + api: PythonExtension['environments'], + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, + packages: string | undefined, + token: CancellationToken, +): Promise { + // environment + const envPath = api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); + } + const runCommand = await raceCancellationError( + getTerminalCommand(environment, resourcePath, terminalExecutionService, terminalHelper), + token, + ); + + const message = [ + `Following is the information about the Python environment:`, + `1. Environment Type: ${environment.environment?.type || 'unknown'}`, + `2. Version: ${environment.version.sysVersion || 'unknown'}`, + '', + `3. Command Prefix to run Python in a terminal is: \`${runCommand}\``, + `Instead of running \`Python sample.py\` in the terminal, you will now run: \`${runCommand} sample.py\``, + `Similarly instead of running \`Python -c "import sys;...."\` in the terminal, you will now run: \`${runCommand} -c "import sys;...."\``, + packages ? `4. ${packages}` : '', + ]; + return message.join('\n'); +} + +export async function getTerminalCommand( + environment: ResolvedEnvironment, + resource: Uri | undefined, + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, +): Promise { + let cmd: { command: string; args: string[] }; + if (isCondaEnv(environment)) { + cmd = (await getCondaRunCommand(environment)) || (await terminalExecutionService.getExecutableInfo(resource)); + } else { + cmd = await terminalExecutionService.getExecutableInfo(resource); + } + return terminalHelper.buildCommandForTerminal(TerminalShellType.other, cmd.command, cmd.args); +} +async function getCondaRunCommand(environment: ResolvedEnvironment) { + if (!environment.executable.uri) { + return; + } + const conda = await Conda.getConda(); + if (!conda) { + return; + } + const condaEnv = await conda.getCondaEnvironment(environment.executable.uri?.fsPath); + if (!condaEnv) { + return; + } + const cmd = await conda.getRunPythonArgs(condaEnv, true, false); + if (!cmd) { + return; + } + return { command: cmd[0], args: cmd.slice(1) }; +} diff --git a/src/test/common.ts b/src/test/common.ts index b6e352b9a3e8..886323e815a5 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -62,7 +62,7 @@ export async function updateSetting( configTarget: ConfigurationTarget, ) { const vscode = require('vscode') as typeof import('vscode'); - const settings = vscode.workspace.getConfiguration('python', { uri: resource, languageId: 'python' } || null); + const settings = vscode.workspace.getConfiguration('python', { uri: resource, languageId: 'python' }); const currentValue = settings.inspect(setting); if ( currentValue !== undefined && diff --git a/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts index 762c23d86c8e..5c851b8071f3 100644 --- a/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts +++ b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts @@ -17,6 +17,7 @@ suite('Python Path Settings Updater', () => { serviceContainer = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); interpreterPathService = TypeMoq.Mock.ofType(); + experimentsManager = TypeMoq.Mock.ofType(); serviceContainer .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts index ceee7f54f447..cdf0d00c5dc4 100644 --- a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -16,6 +16,7 @@ import { UnittestTestExecutionAdapter } from '../../../client/testing/testContro import { MockChildProcess } from '../../mocks/mockChildProcess'; import * as util from '../../../client/testing/testController/common/utils'; import * as extapi from '../../../client/envExt/api.internal'; +import { noop } from '../../core'; const adapters: Array = ['pytest', 'unittest']; @@ -36,6 +37,11 @@ suite('Execution Flow Run Adapters', () => { let useEnvExtensionStub: sinon.SinonStub; setup(() => { + const proc = typeMoq.Mock.ofType(); + proc.setup((p) => p.on).returns(() => noop as any); + proc.setup((p) => p.stdout).returns(() => null); + proc.setup((p) => p.stderr).returns(() => null); + mockProc = proc.object; useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); useEnvExtensionStub.returns(false); // general vars From c246e0e496812042ee6677895d21652a33197a09 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 16 May 2025 17:30:46 +1000 Subject: [PATCH 0939/1136] API to get last used env in a LM tool (#25079) --- src/client/api.ts | 6 +- src/client/chat/installPackagesTool.ts | 3 +- src/client/chat/lastUsedEnvs.ts | 81 ++++++++++++++++++++++++ src/client/chat/listPackagesTool.ts | 3 +- src/client/chat/utils.ts | 3 +- src/client/jupyter/jupyterIntegration.ts | 18 +++++- src/test/api.functional.test.ts | 9 ++- 7 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 src/client/chat/lastUsedEnvs.ts diff --git a/src/client/api.ts b/src/client/api.ts index 15fb4d688a89..908da4be7103 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -45,8 +45,10 @@ export function buildApi( TensorboardExtensionIntegration, TensorboardExtensionIntegration, ); - const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); const jupyterPythonEnvApi = serviceContainer.get(JupyterExtensionPythonEnvironments); + const environments = buildEnvironmentApi(discoveryApi, serviceContainer, jupyterPythonEnvApi); + const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); + jupyterIntegration.registerEnvApi(environments); const tensorboardIntegration = serviceContainer.get( TensorboardExtensionIntegration, ); @@ -155,7 +157,7 @@ export function buildApi( stop: (client: BaseLanguageClient): Promise => client.stop(), getTelemetryReporter: () => getTelemetryReporter(), }, - environments: buildEnvironmentApi(discoveryApi, serviceContainer, jupyterPythonEnvApi), + environments, }; // In test environment return the DI Container. diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts index a430525e1018..177c63077cbe 100644 --- a/src/client/chat/installPackagesTool.ts +++ b/src/client/chat/installPackagesTool.ts @@ -19,6 +19,7 @@ import { resolveFilePath } from './utils'; import { IModuleInstaller } from '../common/installer/types'; import { ModuleInstallerType } from '../pythonEnvironments/info'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { trackEnvUsedByTool } from './lastUsedEnvs'; export interface IInstallPackageArgs { resourcePath?: string; @@ -66,7 +67,7 @@ export class InstallPackagesTool implements LanguageModelTool= 0; i--) { + if (urisEqual(lastUsedEnvs[i].uri, uri)) { + lastUsedEnvs.splice(i, 1); + } + } + // Add the new entry + lastUsedEnvs.push({ uri, env, dateTime: now }); + // Prune + pruneLastUsedEnvs(); +} + +/** + * Get the last used environment for a given resource (uri), or undefined if not found or expired. + */ +export function getLastEnvUsedByTool( + uri: Uri | undefined, + api: PythonExtension['environments'], +): EnvironmentPath | undefined { + pruneLastUsedEnvs(); + // Find the most recent entry for this uri that is not expired + const item = lastUsedEnvs.find((item) => urisEqual(item.uri, uri)); + if (item) { + return item.env; + } + const envPath = api.getActiveEnvironmentPath(uri); + if (lastUsedEnvs.some((item) => item.env.id === envPath.id)) { + // If this env was already used, return it + return envPath; + } + return undefined; +} + +/** + * Compare two uris (or undefined) for equality. + */ +function urisEqual(a: Uri | undefined, b: Uri | undefined): boolean { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + return a.toString() === b.toString(); +} + +/** + * Remove items older than 60 minutes or if the list grows over 100. + */ +function pruneLastUsedEnvs() { + const now = Date.now(); + // Remove items older than 60 minutes + for (let i = lastUsedEnvs.length - 1; i >= 0; i--) { + if (now - lastUsedEnvs[i].dateTime > MAX_TRACKED_AGE) { + lastUsedEnvs.splice(i, 1); + } + } + // If still over 100, remove oldest + if (lastUsedEnvs.length > MAX_TRACKED_URIS) { + lastUsedEnvs.sort((a, b) => b.dateTime - a.dateTime); + lastUsedEnvs.length = MAX_TRACKED_URIS; + } +} diff --git a/src/client/chat/listPackagesTool.ts b/src/client/chat/listPackagesTool.ts index 157bdcf34793..0e410593de6c 100644 --- a/src/client/chat/listPackagesTool.ts +++ b/src/client/chat/listPackagesTool.ts @@ -22,6 +22,7 @@ import { parsePipList } from './pipListUtils'; import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; import { traceError } from '../logging'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { trackEnvUsedByTool } from './lastUsedEnvs'; export interface IResourceReference { resourcePath?: string; @@ -108,7 +109,7 @@ export async function getPythonPackagesResponse( if (!packages.length) { return 'No packages found'; } - + trackEnvUsedByTool(resourcePath, environment); // Installed Python packages, each in the format or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. const response = [ 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts index 6206e01ea655..00a3fbb8393c 100644 --- a/src/client/chat/utils.ts +++ b/src/client/chat/utils.ts @@ -7,6 +7,7 @@ import { PythonExtension, ResolvedEnvironment } from '../api/types'; import { ITerminalHelper, TerminalShellType } from '../common/terminal/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; +import { trackEnvUsedByTool } from './lastUsedEnvs'; export function resolveFilePath(filepath?: string): Uri | undefined { if (!filepath) { @@ -70,7 +71,7 @@ export async function getEnvironmentDetails( getTerminalCommand(environment, resourcePath, terminalExecutionService, terminalHelper), token, ); - + trackEnvUsedByTool(resourcePath, environment); const message = [ `Following is the information about the Python environment:`, `1. Environment Type: ${environment.environment?.type || 'unknown'}`, diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts index 1136502c1ef2..a80c93916f3f 100644 --- a/src/client/jupyter/jupyterIntegration.ts +++ b/src/client/jupyter/jupyterIntegration.ts @@ -22,8 +22,9 @@ import { import { PylanceApi } from '../activation/node/pylanceApi'; import { ExtensionContextKey } from '../common/application/contextKeys'; import { getDebugpyPath } from '../debugger/pythonDebugger'; -import type { Environment } from '../api/types'; +import type { Environment, EnvironmentPath, PythonExtension } from '../api/types'; import { DisposableBase } from '../common/utils/resourceLifecycle'; +import { getLastEnvUsedByTool } from '../chat/lastUsedEnvs'; type PythonApiForJupyterExtension = { /** @@ -63,6 +64,11 @@ type PythonApiForJupyterExtension = { * @param func : The function that Python should call when requesting the Python path. */ registerJupyterPythonPathFunction(func: (uri: Uri) => Promise): void; + + /** + * Returns the Environment that was last used in a Python tool. + */ + getLastUsedEnvInLmTool(uri: Uri): EnvironmentPath | undefined; }; type JupyterExtensionApi = { @@ -78,6 +84,7 @@ export class JupyterExtensionIntegration { private jupyterExtension: Extension | undefined; private pylanceExtension: Extension | undefined; + private environmentApi: PythonExtension['environments'] | undefined; constructor( @inject(IExtensions) private readonly extensions: IExtensions, @@ -90,6 +97,9 @@ export class JupyterExtensionIntegration { @inject(IContextKeyManager) private readonly contextManager: IContextKeyManager, @inject(IInterpreterService) private interpreterService: IInterpreterService, ) {} + public registerEnvApi(api: PythonExtension['environments']) { + this.environmentApi = api; + } public registerApi(jupyterExtensionApi: JupyterExtensionApi): JupyterExtensionApi | undefined { this.contextManager.setContext(ExtensionContextKey.IsJupyterInstalled, true); @@ -121,6 +131,12 @@ export class JupyterExtensionIntegration { getCondaVersion: () => this.condaService.getCondaVersion(), registerJupyterPythonPathFunction: (func: (uri: Uri) => Promise) => this.registerJupyterPythonPathFunction(func), + getLastUsedEnvInLmTool: (uri) => { + if (!this.environmentApi) { + return undefined; + } + return getLastEnvUsedByTool(uri, this.environmentApi); + }, }); return undefined; } diff --git a/src/test/api.functional.test.ts b/src/test/api.functional.test.ts index 1149dcb7da9d..03016956dbef 100644 --- a/src/test/api.functional.test.ts +++ b/src/test/api.functional.test.ts @@ -19,7 +19,11 @@ import { ServiceManager } from '../client/ioc/serviceManager'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; import { IDiscoveryAPI } from '../client/pythonEnvironments/base/locator'; import * as pythonDebugger from '../client/debugger/pythonDebugger'; -import { JupyterExtensionPythonEnvironments, JupyterPythonEnvironmentApi } from '../client/jupyter/jupyterIntegration'; +import { + JupyterExtensionIntegration, + JupyterExtensionPythonEnvironments, + JupyterPythonEnvironmentApi, +} from '../client/jupyter/jupyterIntegration'; import { EventEmitter, Uri } from 'vscode'; suite('Extension API', () => { @@ -50,6 +54,9 @@ suite('Extension API', () => { when(serviceContainer.get(IEnvironmentVariablesProvider)).thenReturn( instance(environmentVariablesProvider), ); + when(serviceContainer.get(JupyterExtensionIntegration)).thenReturn( + instance(mock()), + ); when(serviceContainer.get(IInterpreterService)).thenReturn(instance(interpreterService)); const onDidChangePythonEnvironment = new EventEmitter(); const jupyterApi: JupyterPythonEnvironmentApi = { From 98aaab17f57f558edee50b09ee9b76e644275d1e Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Fri, 16 May 2025 21:35:41 -0700 Subject: [PATCH 0940/1136] Tweak Python shell integration for lsp completion (#25082) Resolves: https://github.com/microsoft/vscode-python/issues/25012 --- python_files/pythonrc.py | 4 ++-- python_files/tests/test_shell_integration.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python_files/pythonrc.py b/python_files/pythonrc.py index 0f552c86d375..afd32520cf01 100644 --- a/python_files/pythonrc.py +++ b/python_files/pythonrc.py @@ -53,13 +53,13 @@ def __str__(self): result = "" # For non-windows allow recent_command history. if sys.platform != "win32": - result = "{command_line}{command_finished}{prompt_started}{prompt}{command_start}{command_executed}".format( + result = "{command_executed}{command_line}{command_finished}{prompt_started}{prompt}{command_start}".format( + command_executed="\x1b]633;C\x07", command_line="\x1b]633;E;" + str(get_last_command()) + "\x07", command_finished="\x1b]633;D;" + str(exit_code) + "\x07", prompt_started="\x1b]633;A\x07", prompt=original_ps1, command_start="\x1b]633;B\x07", - command_executed="\x1b]633;C\x07", ) else: result = "{command_finished}{prompt_started}{prompt}{command_start}{command_executed}".format( diff --git a/python_files/tests/test_shell_integration.py b/python_files/tests/test_shell_integration.py index 376cb466bb50..574edfc056b4 100644 --- a/python_files/tests/test_shell_integration.py +++ b/python_files/tests/test_shell_integration.py @@ -17,7 +17,7 @@ def test_decoration_success(): if sys.platform != "win32" and (not is_wsl): assert ( result - == "\x1b]633;E;None\x07\x1b]633;D;0\x07\x1b]633;A\x07>>> \x1b]633;B\x07\x1b]633;C\x07" + == "\x1b]633;C\x07\x1b]633;E;None\x07\x1b]633;D;0\x07\x1b]633;A\x07>>> \x1b]633;B\x07" ) else: pass @@ -32,7 +32,7 @@ def test_decoration_failure(): if sys.platform != "win32" and (not is_wsl): assert ( result - == "\x1b]633;E;None\x07\x1b]633;D;1\x07\x1b]633;A\x07>>> \x1b]633;B\x07\x1b]633;C\x07" + == "\x1b]633;C\x07\x1b]633;E;None\x07\x1b]633;D;1\x07\x1b]633;A\x07>>> \x1b]633;B\x07" ) else: pass From 27270db13d6884d6658e41f6a0f4c3c51a14c0a2 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 19 May 2025 18:33:17 +1000 Subject: [PATCH 0941/1136] Ensure python selector supports returning created env (#25088) --- .../interpreterSelector/commands/setInterpreter.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 27816ee83296..313b638313df 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -49,6 +49,7 @@ import { BaseInterpreterSelectorCommand } from './base'; import { untildify } from '../../../../common/helpers'; import { useEnvExtension } from '../../../../envExt/api.internal'; import { setInterpreterLegacy } from '../../../../envExt/api.legacy'; +import { CreateEnvironmentResult } from '../../../../pythonEnvironments/creation/proposed.createEnvApis'; export type InterpreterStateArgs = { path?: string; workspace: Resource }; export type QuickPickType = IInterpreterQuickPickItem | ISpecialQuickPickItem | QuickPickItem; @@ -229,12 +230,13 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem sendTelemetryEvent(EventName.SELECT_INTERPRETER_ENTER_OR_FIND); return this._enterOrBrowseInterpreterPath.bind(this); } else if (selection.label === this.createEnvironmentSuggestion.label) { - this.commandManager - .executeCommand(Commands.Create_Environment, { + const createdEnv = (await Promise.resolve( + this.commandManager.executeCommand(Commands.Create_Environment, { showBackButton: false, selectEnvironment: true, - }) - .then(noop, noop); + }), + ).catch(noop)) as CreateEnvironmentResult | undefined; + state.path = createdEnv?.path; } else if (selection.label === this.noPythonInstalled.label) { this.commandManager.executeCommand(Commands.InstallPython).then(noop, noop); this.wasNoPythonInstalledItemClicked = true; From d7a5ab7848c7e07e96b893c63512f8bb2fd7639b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 20 May 2025 11:10:53 +1000 Subject: [PATCH 0942/1136] Ability to track the user selected Environment (#25090) @karthiknadig /cc --- src/client/chat/installPackagesTool.ts | 2 - src/client/chat/lastUsedEnvs.ts | 81 -------------- src/client/chat/listPackagesTool.ts | 2 - src/client/chat/utils.ts | 2 - src/client/common/utils/async.ts | 23 ++++ src/client/extension.ts | 4 + .../commands/setInterpreter.ts | 6 +- .../configuration/pythonPathUpdaterService.ts | 10 +- .../recommededEnvironmentService.ts | 102 ++++++++++++++++++ src/client/interpreter/configuration/types.ts | 12 +++ src/client/interpreter/serviceRegistry.ts | 6 ++ src/client/jupyter/jupyterIntegration.ts | 31 ++++-- src/test/debugger/envVars.test.ts | 6 ++ .../interpreters/serviceRegistry.unit.test.ts | 3 + 14 files changed, 195 insertions(+), 95 deletions(-) delete mode 100644 src/client/chat/lastUsedEnvs.ts create mode 100644 src/client/interpreter/configuration/recommededEnvironmentService.ts diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts index 177c63077cbe..7b34b71ed556 100644 --- a/src/client/chat/installPackagesTool.ts +++ b/src/client/chat/installPackagesTool.ts @@ -19,7 +19,6 @@ import { resolveFilePath } from './utils'; import { IModuleInstaller } from '../common/installer/types'; import { ModuleInstallerType } from '../pythonEnvironments/info'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; -import { trackEnvUsedByTool } from './lastUsedEnvs'; export interface IInstallPackageArgs { resourcePath?: string; @@ -67,7 +66,6 @@ export class InstallPackagesTool implements LanguageModelTool= 0; i--) { - if (urisEqual(lastUsedEnvs[i].uri, uri)) { - lastUsedEnvs.splice(i, 1); - } - } - // Add the new entry - lastUsedEnvs.push({ uri, env, dateTime: now }); - // Prune - pruneLastUsedEnvs(); -} - -/** - * Get the last used environment for a given resource (uri), or undefined if not found or expired. - */ -export function getLastEnvUsedByTool( - uri: Uri | undefined, - api: PythonExtension['environments'], -): EnvironmentPath | undefined { - pruneLastUsedEnvs(); - // Find the most recent entry for this uri that is not expired - const item = lastUsedEnvs.find((item) => urisEqual(item.uri, uri)); - if (item) { - return item.env; - } - const envPath = api.getActiveEnvironmentPath(uri); - if (lastUsedEnvs.some((item) => item.env.id === envPath.id)) { - // If this env was already used, return it - return envPath; - } - return undefined; -} - -/** - * Compare two uris (or undefined) for equality. - */ -function urisEqual(a: Uri | undefined, b: Uri | undefined): boolean { - if (a === b) { - return true; - } - if (!a || !b) { - return false; - } - return a.toString() === b.toString(); -} - -/** - * Remove items older than 60 minutes or if the list grows over 100. - */ -function pruneLastUsedEnvs() { - const now = Date.now(); - // Remove items older than 60 minutes - for (let i = lastUsedEnvs.length - 1; i >= 0; i--) { - if (now - lastUsedEnvs[i].dateTime > MAX_TRACKED_AGE) { - lastUsedEnvs.splice(i, 1); - } - } - // If still over 100, remove oldest - if (lastUsedEnvs.length > MAX_TRACKED_URIS) { - lastUsedEnvs.sort((a, b) => b.dateTime - a.dateTime); - lastUsedEnvs.length = MAX_TRACKED_URIS; - } -} diff --git a/src/client/chat/listPackagesTool.ts b/src/client/chat/listPackagesTool.ts index 0e410593de6c..659ce2b5bb0d 100644 --- a/src/client/chat/listPackagesTool.ts +++ b/src/client/chat/listPackagesTool.ts @@ -22,7 +22,6 @@ import { parsePipList } from './pipListUtils'; import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; import { traceError } from '../logging'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; -import { trackEnvUsedByTool } from './lastUsedEnvs'; export interface IResourceReference { resourcePath?: string; @@ -109,7 +108,6 @@ export async function getPythonPackagesResponse( if (!packages.length) { return 'No packages found'; } - trackEnvUsedByTool(resourcePath, environment); // Installed Python packages, each in the format or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. const response = [ 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts index 00a3fbb8393c..2973e748aee4 100644 --- a/src/client/chat/utils.ts +++ b/src/client/chat/utils.ts @@ -7,7 +7,6 @@ import { PythonExtension, ResolvedEnvironment } from '../api/types'; import { ITerminalHelper, TerminalShellType } from '../common/terminal/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; -import { trackEnvUsedByTool } from './lastUsedEnvs'; export function resolveFilePath(filepath?: string): Uri | undefined { if (!filepath) { @@ -71,7 +70,6 @@ export async function getEnvironmentDetails( getTerminalCommand(environment, resourcePath, terminalExecutionService, terminalHelper), token, ); - trackEnvUsedByTool(resourcePath, environment); const message = [ `Following is the information about the Python environment:`, `1. Environment Type: ${environment.environment?.type || 'unknown'}`, diff --git a/src/client/common/utils/async.ts b/src/client/common/utils/async.ts index cabea8225ac9..a44425f8f1a3 100644 --- a/src/client/common/utils/async.ts +++ b/src/client/common/utils/async.ts @@ -268,3 +268,26 @@ export async function waitForCondition( }, 10); }); } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isPromiseLike(v: any): v is PromiseLike { + return typeof v?.then === 'function'; +} + +export function raceTimeout(timeout: number, ...promises: Promise[]): Promise; +export function raceTimeout(timeout: number, defaultValue: T, ...promises: Promise[]): Promise; +export function raceTimeout(timeout: number, defaultValue: T, ...promises: Promise[]): Promise { + const resolveValue = isPromiseLike(defaultValue) ? undefined : defaultValue; + if (isPromiseLike(defaultValue)) { + promises.push((defaultValue as unknown) as Promise); + } + + let promiseResolve: ((value: T) => void) | undefined = undefined; + + const timer = setTimeout(() => promiseResolve?.((resolveValue as unknown) as T), timeout); + + return Promise.race([ + Promise.race(promises).finally(() => clearTimeout(timer)), + new Promise((resolve) => (promiseResolve = resolve)), + ]); +} diff --git a/src/client/extension.ts b/src/client/extension.ts index 1a07a4b1e3b2..af26a5657330 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -44,6 +44,7 @@ import { ProposedExtensionAPI } from './proposedApiTypes'; import { buildProposedApi } from './proposedApi'; import { GLOBAL_PERSISTENT_KEYS } from './common/persistentState'; import { registerTools } from './chat'; +import { IRecommendedEnvironmentService } from './interpreter/configuration/types'; durations.codeLoadingTime = stopWatch.elapsedTime; @@ -164,6 +165,9 @@ async function activateUnsafe( ); const proposedApi = buildProposedApi(components.pythonEnvs, ext.legacyIOC.serviceContainer); registerTools(context, components.pythonEnvs, api.environments, ext.legacyIOC.serviceContainer); + ext.legacyIOC.serviceContainer + .get(IRecommendedEnvironmentService) + .registerEnvApi(api.environments); return [{ ...api, ...proposedApi }, activationPromise, ext.legacyIOC.serviceContainer]; } diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 313b638313df..912a9c66b0dd 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -565,8 +565,11 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem return Promise.resolve(); } + /** + * @returns true when an interpreter was set, undefined if the user cancelled the quickpick. + */ @captureTelemetry(EventName.SELECT_INTERPRETER) - public async setInterpreter(): Promise { + public async setInterpreter(): Promise { const targetConfig = await this.getConfigTargets(); if (!targetConfig) { return; @@ -588,6 +591,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem if (useEnvExtension()) { await setInterpreterLegacy(interpreterState.path, wkspace); } + return true; } } diff --git a/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/src/client/interpreter/configuration/pythonPathUpdaterService.ts index 9b9cc26f845f..9814ff6ee4cb 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterService.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -7,7 +7,11 @@ import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { PythonInterpreterTelemetry } from '../../telemetry/types'; import { IComponentAdapter } from '../contracts'; -import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } from './types'; +import { + IRecommendedEnvironmentService, + IPythonPathUpdaterServiceFactory, + IPythonPathUpdaterServiceManager, +} from './types'; @injectable() export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManager { @@ -15,6 +19,7 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage @inject(IPythonPathUpdaterServiceFactory) private readonly pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory, @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, + @inject(IRecommendedEnvironmentService) private readonly preferredEnvService: IRecommendedEnvironmentService, ) {} public async updatePythonPath( @@ -28,6 +33,9 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage let failed = false; try { await pythonPathUpdater.updatePythonPath(pythonPath); + if (trigger === 'ui') { + this.preferredEnvService.trackUserSelectedEnvironment(pythonPath, wkspace); + } } catch (err) { failed = true; const reason = err as Error; diff --git a/src/client/interpreter/configuration/recommededEnvironmentService.ts b/src/client/interpreter/configuration/recommededEnvironmentService.ts new file mode 100644 index 000000000000..67517b918ff9 --- /dev/null +++ b/src/client/interpreter/configuration/recommededEnvironmentService.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IRecommendedEnvironmentService } from './types'; +import { PythonExtension } from '../../api/types'; +import { IExtensionContext, Resource } from '../../common/types'; +import { Uri, workspace } from 'vscode'; +import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../../common/persistentState'; +import { traceError } from '../../logging'; + +const MEMENTO_KEY = 'userSelectedEnvPath'; + +@injectable() +export class RecommendedEnvironmentService implements IRecommendedEnvironmentService { + private api?: PythonExtension['environments']; + constructor(@inject(IExtensionContext) private readonly extensionContext: IExtensionContext) {} + + registerEnvApi(api: PythonExtension['environments']) { + this.api = api; + } + + trackUserSelectedEnvironment(environmentPath: string | undefined, uri: Uri | undefined) { + if (workspace.workspaceFolders?.length) { + try { + void updateWorkspaceStateValue(MEMENTO_KEY, getDataToStore(environmentPath, uri)); + } catch (ex) { + traceError('Failed to update workspace state for preferred environment', ex); + } + } else { + void this.extensionContext.globalState.update(MEMENTO_KEY, environmentPath); + } + } + + getRecommededEnvironment( + resource: Resource, + ): + | { environmentPath: string; reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended' } + | undefined { + let workspaceState: string | undefined = undefined; + try { + workspaceState = getWorkspaceStateValue(MEMENTO_KEY); + } catch (ex) { + traceError('Failed to get workspace state for preferred environment', ex); + } + + if (workspace.workspaceFolders?.length && workspaceState) { + const workspaceUri = ( + (resource ? workspace.getWorkspaceFolder(resource)?.uri : undefined) || + workspace.workspaceFolders[0].uri + ).toString(); + + try { + const existingJson: Record = JSON.parse(workspaceState); + const selectedEnvPath = existingJson[workspaceUri]; + if (selectedEnvPath) { + return { environmentPath: selectedEnvPath, reason: 'workspaceUserSelected' }; + } + } catch (ex) { + traceError('Failed to parse existing workspace state value for preferred environment', ex); + } + } + + const globalSelectedEnvPath = this.extensionContext.globalState.get(MEMENTO_KEY); + if (globalSelectedEnvPath) { + return { environmentPath: globalSelectedEnvPath, reason: 'globalUserSelected' }; + } + return this.api && workspace.isTrusted + ? { + environmentPath: this.api.getActiveEnvironmentPath(resource).path, + reason: 'defaultRecommended', + } + : undefined; + } +} + +function getDataToStore(environmentPath: string | undefined, uri: Uri | undefined): string | undefined { + if (!workspace.workspaceFolders?.length) { + return environmentPath; + } + const workspaceUri = ( + (uri ? workspace.getWorkspaceFolder(uri)?.uri : undefined) || workspace.workspaceFolders[0].uri + ).toString(); + const existingData = getWorkspaceStateValue(MEMENTO_KEY); + if (!existingData) { + return JSON.stringify(environmentPath ? { [workspaceUri]: environmentPath } : {}); + } + try { + const existingJson: Record = JSON.parse(existingData); + if (environmentPath) { + existingJson[workspaceUri] = environmentPath; + } else { + delete existingJson[workspaceUri]; + } + return JSON.stringify(existingJson); + } catch (ex) { + traceError('Failed to parse existing workspace state value for preferred environment', ex); + return JSON.stringify({ + [workspaceUri]: environmentPath, + }); + } +} diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts index 08518d4d12d3..8e8aecdc7f16 100644 --- a/src/client/interpreter/configuration/types.ts +++ b/src/client/interpreter/configuration/types.ts @@ -1,6 +1,7 @@ import { ConfigurationTarget, Disposable, QuickPickItem, Uri } from 'vscode'; import { Resource } from '../../common/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { PythonExtension } from '../../api/types'; export interface IPythonPathUpdaterService { updatePythonPath(pythonPath: string | undefined): Promise; @@ -96,3 +97,14 @@ export interface IInterpreterQuickPick { params?: InterpreterQuickPickParams, ): Promise; } + +export const IRecommendedEnvironmentService = Symbol('IRecommendedEnvironmentService'); +export interface IRecommendedEnvironmentService { + registerEnvApi(api: PythonExtension['environments']): void; + trackUserSelectedEnvironment(environmentPath: string | undefined, uri: Uri | undefined): void; + getRecommededEnvironment( + resource: Resource, + ): + | { environmentPath: string; reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended' } + | undefined; +} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 688ef20cf970..1e82b9fec0df 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -16,12 +16,14 @@ import { InstallPythonViaTerminal } from './configuration/interpreterSelector/co import { ResetInterpreterCommand } from './configuration/interpreterSelector/commands/resetInterpreter'; import { SetInterpreterCommand } from './configuration/interpreterSelector/commands/setInterpreter'; import { InterpreterSelector } from './configuration/interpreterSelector/interpreterSelector'; +import { RecommendedEnvironmentService } from './configuration/recommededEnvironmentService'; import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; import { IInterpreterComparer, IInterpreterQuickPick, IInterpreterSelector, + IRecommendedEnvironmentService, IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager, } from './configuration/types'; @@ -59,6 +61,10 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void IExtensionSingleActivationService, ResetInterpreterCommand, ); + serviceManager.addSingleton( + IRecommendedEnvironmentService, + RecommendedEnvironmentService, + ); serviceManager.addSingleton(IInterpreterQuickPick, SetInterpreterCommand); serviceManager.addSingleton(IExtensionActivationService, VirtualEnvironmentPrompt); diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts index a80c93916f3f..b81542806daf 100644 --- a/src/client/jupyter/jupyterIntegration.ts +++ b/src/client/jupyter/jupyterIntegration.ts @@ -12,7 +12,11 @@ import { IContextKeyManager, IWorkspaceService } from '../common/application/typ import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants'; import { GLOBAL_MEMENTO, IExtensions, IMemento, Resource } from '../common/types'; import { IEnvironmentActivationService } from '../interpreter/activation/types'; -import { IInterpreterQuickPickItem, IInterpreterSelector } from '../interpreter/configuration/types'; +import { + IInterpreterQuickPickItem, + IInterpreterSelector, + IRecommendedEnvironmentService, +} from '../interpreter/configuration/types'; import { ICondaService, IInterpreterDisplay, @@ -24,7 +28,6 @@ import { ExtensionContextKey } from '../common/application/contextKeys'; import { getDebugpyPath } from '../debugger/pythonDebugger'; import type { Environment, EnvironmentPath, PythonExtension } from '../api/types'; import { DisposableBase } from '../common/utils/resourceLifecycle'; -import { getLastEnvUsedByTool } from '../chat/lastUsedEnvs'; type PythonApiForJupyterExtension = { /** @@ -66,9 +69,17 @@ type PythonApiForJupyterExtension = { registerJupyterPythonPathFunction(func: (uri: Uri) => Promise): void; /** - * Returns the Environment that was last used in a Python tool. + * Returns the preferred environment for the given URI. */ - getLastUsedEnvInLmTool(uri: Uri): EnvironmentPath | undefined; + getRecommededEnvironment( + uri: Uri, + ): Promise< + | { + environment: EnvironmentPath; + reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended'; + } + | undefined + >; }; type JupyterExtensionApi = { @@ -96,6 +107,7 @@ export class JupyterExtensionIntegration { @inject(ICondaService) private readonly condaService: ICondaService, @inject(IContextKeyManager) private readonly contextManager: IContextKeyManager, @inject(IInterpreterService) private interpreterService: IInterpreterService, + @inject(IRecommendedEnvironmentService) private preferredEnvironmentService: IRecommendedEnvironmentService, ) {} public registerEnvApi(api: PythonExtension['environments']) { this.environmentApi = api; @@ -131,11 +143,18 @@ export class JupyterExtensionIntegration { getCondaVersion: () => this.condaService.getCondaVersion(), registerJupyterPythonPathFunction: (func: (uri: Uri) => Promise) => this.registerJupyterPythonPathFunction(func), - getLastUsedEnvInLmTool: (uri) => { + getRecommededEnvironment: async (uri) => { if (!this.environmentApi) { return undefined; } - return getLastEnvUsedByTool(uri, this.environmentApi); + const preferred = this.preferredEnvironmentService.getRecommededEnvironment(uri); + if (!preferred) { + return undefined; + } + const environment = workspace.isTrusted + ? await this.environmentApi.resolveEnvironment(preferred.environmentPath) + : undefined; + return environment ? { environment, reason: preferred.reason } : undefined; }, }); return undefined; diff --git a/src/test/debugger/envVars.test.ts b/src/test/debugger/envVars.test.ts index ae21c7fd5d49..8b0f55986281 100644 --- a/src/test/debugger/envVars.test.ts +++ b/src/test/debugger/envVars.test.ts @@ -16,6 +16,8 @@ import { isOs, OSType } from '../common'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; import { UnitTestIocContainer } from '../testing/serviceRegistry'; import { normCase } from '../../client/common/platform/fs-paths'; +import { IRecommendedEnvironmentService } from '../../client/interpreter/configuration/types'; +import { RecommendedEnvironmentService } from '../../client/interpreter/configuration/recommededEnvironmentService'; use(chaiAsPromised.default); @@ -53,6 +55,10 @@ suite('Resolving Environment Variables when Debugging', () => { ioc.registerFileSystemTypes(); ioc.registerVariableTypes(); ioc.registerMockProcess(); + ioc.serviceManager.addSingleton( + IRecommendedEnvironmentService, + RecommendedEnvironmentService, + ); } async function testBasicProperties(console: ConsoleType, expectedNumberOfVariables: number) { diff --git a/src/test/interpreters/serviceRegistry.unit.test.ts b/src/test/interpreters/serviceRegistry.unit.test.ts index a189696c2f82..ad8614b42d8b 100644 --- a/src/test/interpreters/serviceRegistry.unit.test.ts +++ b/src/test/interpreters/serviceRegistry.unit.test.ts @@ -27,6 +27,7 @@ import { IInterpreterSelector, IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager, + IRecommendedEnvironmentService, } from '../../client/interpreter/configuration/types'; import { IActivatedEnvironmentLaunch, @@ -44,6 +45,7 @@ import { CondaInheritEnvPrompt } from '../../client/interpreter/virtualEnvs/cond import { VirtualEnvironmentPrompt } from '../../client/interpreter/virtualEnvs/virtualEnvPrompt'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { InterpreterPathCommand } from '../../client/interpreter/interpreterPathCommand'; +import { RecommendedEnvironmentService } from '../../client/interpreter/configuration/recommededEnvironmentService'; suite('Interpreters - Service Registry', () => { test('Registrations', () => { @@ -64,6 +66,7 @@ suite('Interpreters - Service Registry', () => { [IPythonPathUpdaterServiceFactory, PythonPathUpdaterServiceFactory], [IPythonPathUpdaterServiceManager, PythonPathUpdaterService], + [IRecommendedEnvironmentService, RecommendedEnvironmentService], [IInterpreterSelector, InterpreterSelector], [IInterpreterHelper, InterpreterHelper], [IInterpreterComparer, EnvironmentTypeComparer], From 4a602e8127034846703aadc9eec93099107601cf Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 20 May 2025 13:35:36 +1000 Subject: [PATCH 0943/1136] Configure Python Env tool (#25091) --- package.json | 84 +++--- package.nls.json | 8 +- src/client/chat/configurePythonEnvTool.ts | 262 ++++++++++++++++++ src/client/chat/getExecutableTool.ts | 2 +- src/client/chat/getPythonEnvTool.ts | 2 +- src/client/chat/index.ts | 11 +- src/client/chat/installPackagesTool.ts | 2 +- src/client/chat/listPackagesTool.ts | 83 +----- src/client/extensionActivation.ts | 4 +- .../commands/setInterpreter.ts | 42 ++- .../recommededEnvironmentService.ts | 110 +++++++- src/client/interpreter/configuration/types.ts | 12 +- src/client/interpreter/serviceRegistry.ts | 1 + src/client/jupyter/jupyterIntegration.ts | 11 +- .../creation/createEnvApi.ts | 13 +- .../creation/registrations.ts | 8 +- .../creation/createEnvApi.unit.test.ts | 18 +- 17 files changed, 492 insertions(+), 181 deletions(-) create mode 100644 src/client/chat/configurePythonEnvTool.ts diff --git a/package.json b/package.json index 9d1cf544b692..3ec055896aca 100644 --- a/package.json +++ b/package.json @@ -1465,80 +1465,59 @@ ], "languageModelTools": [ { - "name": "get_python_environment_info", + "name": "get_python_environment_details", "displayName": "Get Python Environment Info", - "userDescription": "%python.languageModelTools.get_python_environment_info.userDescription%", - "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Environment (conda, venv, etec), 2. Version of Python, 3. List of all installed packages with their versions. ", + "userDescription": "%python.languageModelTools.get_python_environment_details.userDescription%", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Environment (conda, venv, etec), 2. Version of Python, 3. List of all installed packages with their versions. ALWAYS call configure_python_environment before using this tool.", "toolReferenceName": "pythonGetEnvironmentInfo", "tags": [ - "ms-python.python" + "enable_other_tool_configure_python_environment" ], - "icon": "$(files)", + "icon": "$(snake)", "canBeReferencedInPrompt": true, "inputSchema": { "type": "object", "properties": { "resourcePath": { - "type": "string" + "type": "string", + "description": "The path to the Python file or workspace to get the environment information for." } }, - "description": "The path to the Python file or workspace to get the environment information for.", "required": [] - } + }, + "when": "!pythonEnvExtensionInstalled" }, { - "name": "get_python_executable", + "name": "get_python_executable_details", "displayName": "Get Python Executable", - "userDescription": "%python.languageModelTools.get_python_executable.userDescription%", - "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. ALWAYS use this tool before executing any Python command in the terminal. This tool returns the details of how to construct the fully qualified path and or command including details such as arguments required to run Python in a terminal. Note: Instead of executing `python --version` or `python -c 'import sys; print(sys.executable)'`, use this tool to get the Python executable path to replace the `python` command. E.g. instead of using `python -c 'import sys; print(sys.executable)'`, use this tool to build the command `conda run -n -c 'import sys; print(sys.executable)'`.", + "userDescription": "%python.languageModelTools.get_python_executable_details.userDescription%", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. ALWAYS use this tool before executing any Python command in the terminal. This tool returns the details of how to construct the fully qualified path and or command including details such as arguments required to run Python in a terminal. Note: Instead of executing `python --version` or `python -c 'import sys; print(sys.executable)'`, use this tool to get the Python executable path to replace the `python` command. E.g. instead of using `python -c 'import sys; print(sys.executable)'`, use this tool to build the command `conda run -n -c 'import sys; print(sys.executable)'`. ALWAYS call configure_python_environment before using this tool.", "toolReferenceName": "pythonExecutableCommand", "tags": [ - "ms-python.python" - ], - "icon": "$(files)", - "canBeReferencedInPrompt": true, - "inputSchema": { - "type": "object", - "properties": { - "resourcePath": { - "type": "string" - } - }, - "description": "The path to the Python file or workspace to get the executable information for. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace.", - "required": [] - } - }, - { - "name": "list_python_packages", - "displayName": "List Python Packages", - "userDescription": "%python.languageModelTools.list_python_packages.userDescription%", - "modelDescription": "This tool will retrieve the list of all installed packages installed in a Python Environment for the specified file or workspace. ALWAYS use this tool instead of executing Python command in the terminal to fetch the list of installed packages. WARNING: Packages installed can change over time, hence the list of packages returned by this tool may not be accurate. Use this tool to get the list of installed packages in a Python environment.", - "toolReferenceName": "listPythonPackages", - "tags": [ - "ms-python.python" + "enable_other_tool_configure_python_environment" ], - "icon": "$(files)", + "icon": "$(terminal)", "canBeReferencedInPrompt": true, "inputSchema": { "type": "object", "properties": { "resourcePath": { - "type": "string" + "type": "string", + "description": "The path to the Python file or workspace to get the executable information for. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace." } }, - "description": "The path to the Python file or workspace to list the packages. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace.", "required": [] }, - "when": "false" + "when": "!pythonEnvExtensionInstalled" }, { - "name": "install_python_package", + "name": "install_python_packages", "displayName": "Install Python Package", - "userDescription": "%python.languageModelTools.python_install_package.userDescription%", - "modelDescription": "Installs Python packages in the given workspace. Use this tool to install packages in the user's chosen environment.", + "userDescription": "%python.languageModelTools.install_python_packages.userDescription%", + "modelDescription": "Installs Python packages in the given workspace. Use this tool to install packages in the user's chosen environment. ALWAYS call configure_python_environment before using this tool.", "toolReferenceName": "pythonInstallPackage", "tags": [ - "ms-python.python" + "enable_other_tool_configure_python_environment" ], "icon": "$(package)", "canBeReferencedInPrompt": true, @@ -1562,6 +1541,27 @@ ] }, "when": "!pythonEnvExtensionInstalled" + }, + { + "name": "configure_python_environment", + "displayName": "Configure Python Environment", + "modelDescription": "This tool configures a Python environment in the given workspace. ALWAYS Use this tool to set up the user's chosen environment and ALWAYS call this tool before using any other Python related tools.", + "userDescription": "%python.languageModelTools.configure_python_environment.userDescription%", + "toolReferenceName": "configurePythonEnvironment", + "tags": [], + "icon": "$(gear)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace for which a Python Environment needs to be configured." + } + }, + "required": [] + }, + "when": "!pythonEnvExtensionInstalled" } ] }, diff --git a/package.nls.json b/package.nls.json index a73deb3554da..b6ba75b332f2 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,9 +1,9 @@ { "python.command.python.startTerminalREPL.title": "Start Terminal REPL", - "python.languageModelTools.get_python_environment_info.userDescription": "Get information for a Python Environment, such as Type, Version, Packages, and more.", - "python.languageModelTools.python_install_package.userDescription": "Installs Python packages in a Python Environment.", - "python.languageModelTools.get_python_executable.userDescription": "Get executable info for a Python Environment", - "python.languageModelTools.list_python_packages.userDescription": "Get a list of all installed packages in a Python Environment.", + "python.languageModelTools.get_python_environment_details.userDescription": "Get information for a Python Environment, such as Type, Version, Packages, and more.", + "python.languageModelTools.install_python_packages.userDescription": "Installs Python packages in a Python Environment.", + "python.languageModelTools.get_python_executable_details.userDescription": "Get executable info for a Python Environment", + "python.languageModelTools.configure_python_environment.userDescription": "Configure a Python Environment for a workspace", "python.command.python.startNativeREPL.title": "Start Native Python REPL", "python.command.python.createEnvironment.title": "Create Environment...", "python.command.python.createNewFile.title": "New Python File", diff --git a/src/client/chat/configurePythonEnvTool.ts b/src/client/chat/configurePythonEnvTool.ts new file mode 100644 index 000000000000..1a22fed83140 --- /dev/null +++ b/src/client/chat/configurePythonEnvTool.ts @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, + workspace, + commands, + QuickPickItem, +} from 'vscode'; +import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { getEnvironmentDetails, raceCancellationError } from './utils'; +import { resolveFilePath } from './utils'; +import { IRecommendedEnvironmentService } from '../interpreter/configuration/types'; +import { ITerminalHelper } from '../common/terminal/types'; +import { raceTimeout } from '../common/utils/async'; +import { Commands, Octicons } from '../common/constants'; +import { CreateEnvironmentResult } from '../pythonEnvironments/creation/proposed.createEnvApis'; +import { IInterpreterPathService } from '../common/types'; +import { DisposableStore } from '../common/utils/resourceLifecycle'; +import { Common, InterpreterQuickPickList } from '../common/utils/localize'; +import { QuickPickItemKind } from '../../test/mocks/vsc'; +import { showQuickPick } from '../common/vscodeApis/windowApis'; +import { SelectEnvironmentResult } from '../interpreter/configuration/interpreterSelector/commands/setInterpreter'; + +export interface IResourceReference { + resourcePath?: string; +} + +let _environmentConfigured = false; + +export class ConfigurePythonEnvTool implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + private readonly recommendedEnvService: IRecommendedEnvironmentService; + public static readonly toolName = 'configure_python_environment'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + ) { + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + this.recommendedEnvService = this.serviceContainer.get( + IRecommendedEnvironmentService, + ); + } + /** + * Invokes the tool to get the information about the Python environment. + * @param options - The invocation options containing the file path. + * @param token - The cancellation token. + * @returns The result containing the information about the Python environment or an error message. + */ + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + const resource = resolveFilePath(options.input.resourcePath); + const recommededEnv = await this.recommendedEnvService.getRecommededEnvironment(resource); + // Already selected workspace env, hence nothing to do. + if (recommededEnv?.reason === 'workspaceUserSelected' && workspace.workspaceFolders?.length) { + return await getEnvDetailsForResponse( + recommededEnv.environment, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } + // No workspace folders, and the user selected a global environment. + if (recommededEnv?.reason === 'globalUserSelected' && !workspace.workspaceFolders?.length) { + return await getEnvDetailsForResponse( + recommededEnv.environment, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } + + if (!workspace.workspaceFolders?.length) { + const selected = await Promise.resolve(commands.executeCommand(Commands.Set_Interpreter)); + const env = await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath(resource)); + if (selected && env) { + return await getEnvDetailsForResponse( + env, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } + return new LanguageModelToolResult([ + new LanguageModelTextPart('User did not select a Python environment.'), + ]); + } + + const selected = await showCreateAndSelectEnvironmentQuickPick(resource, this.serviceContainer); + const env = await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath(resource)); + if (selected && env) { + return await getEnvDetailsForResponse( + env, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } + return new LanguageModelToolResult([ + new LanguageModelTextPart('User did not create nor select a Python environment.'), + ]); + } + + async prepareInvocation?( + options: LanguageModelToolInvocationPrepareOptions, + _token: CancellationToken, + ): Promise { + if (_environmentConfigured) { + return {}; + } + const resource = resolveFilePath(options.input.resourcePath); + const recommededEnv = await this.recommendedEnvService.getRecommededEnvironment(resource); + // Already selected workspace env, hence nothing to do. + if (recommededEnv?.reason === 'workspaceUserSelected' && workspace.workspaceFolders?.length) { + return {}; + } + // No workspace folders, and the user selected a global environment. + if (recommededEnv?.reason === 'globalUserSelected' && !workspace.workspaceFolders?.length) { + return {}; + } + + if (!workspace.workspaceFolders?.length) { + return { + confirmationMessages: { + title: l10n.t('Configure a Python Environment?'), + message: l10n.t('You will be prompted to select a Python Environment.'), + }, + }; + } + return { + confirmationMessages: { + title: l10n.t('Configure a Python Environment?'), + message: l10n.t( + [ + 'The recommended option is to create a new Python Environment, providing the benefit of isolating packages from other environments. ', + 'Optionally you could select an existing Python Environment.', + ].join('\n'), + ), + }, + }; + } +} + +async function getEnvDetailsForResponse( + environment: ResolvedEnvironment | undefined, + api: PythonExtension['environments'], + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, + resource: Uri | undefined, + token: CancellationToken, +): Promise { + const envPath = api.getActiveEnvironmentPath(resource); + environment = environment || (await raceCancellationError(api.resolveEnvironment(envPath), token)); + if (!environment || !environment.version) { + throw new Error('No environment found for the provided resource path: ' + resource?.fsPath); + } + const message = await getEnvironmentDetails( + resource, + api, + terminalExecutionService, + terminalHelper, + undefined, + token, + ); + return new LanguageModelToolResult([ + new LanguageModelTextPart(`A Python Environment has been configured. \n` + message), + ]); +} + +async function showCreateAndSelectEnvironmentQuickPick( + uri: Uri | undefined, + serviceContainer: IServiceContainer, +): Promise { + const createLabel = `${Octicons.Add} ${InterpreterQuickPickList.create.label}`; + const selectLabel = l10n.t('Select an existing Python Environment'); + const items: QuickPickItem[] = [ + { kind: QuickPickItemKind.Separator, label: Common.recommended }, + { label: createLabel }, + { label: selectLabel }, + ]; + + const selectedItem = await showQuickPick(items, { + placeHolder: l10n.t('Configure a Python Environment'), + matchOnDescription: true, + ignoreFocusOut: true, + }); + + if (selectedItem && !Array.isArray(selectedItem) && selectedItem.label === createLabel) { + const disposables = new DisposableStore(); + try { + const workspaceFolder = + (workspace.workspaceFolders?.length && uri ? workspace.getWorkspaceFolder(uri) : undefined) || + (workspace.workspaceFolders?.length === 1 ? workspace.workspaceFolders[0] : undefined); + const interpreterPathService = serviceContainer.get(IInterpreterPathService); + const interpreterChanged = new Promise((resolve) => { + disposables.add(interpreterPathService.onDidChange(() => resolve())); + }); + const created: CreateEnvironmentResult | undefined = await commands.executeCommand( + Commands.Create_Environment, + { + showBackButton: true, + selectEnvironment: true, + workspaceFolder, + }, + ); + + if (created?.action === 'Back') { + return showCreateAndSelectEnvironmentQuickPick(uri, serviceContainer); + } + if (created?.action === 'Cancel') { + return undefined; + } + if (created?.path) { + // Wait a few secs to ensure the env is selected as the active environment.. + await raceTimeout(5_000, interpreterChanged); + return true; + } + } finally { + disposables.dispose(); + } + } + if (selectedItem && !Array.isArray(selectedItem) && selectedItem.label === selectLabel) { + const result = (await Promise.resolve( + commands.executeCommand(Commands.Set_Interpreter, { hideCreateVenv: true, showBackButton: true }), + )) as SelectEnvironmentResult | undefined; + if (result?.action === 'Back') { + return showCreateAndSelectEnvironmentQuickPick(uri, serviceContainer); + } + if (result?.action === 'Cancel') { + return undefined; + } + if (result?.path) { + return true; + } + } +} diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts index af4ab214419a..350598e5ca36 100644 --- a/src/client/chat/getExecutableTool.ts +++ b/src/client/chat/getExecutableTool.ts @@ -29,7 +29,7 @@ export interface IResourceReference { export class GetExecutableTool implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; private readonly terminalHelper: ITerminalHelper; - public static readonly toolName = 'get_python_executable'; + public static readonly toolName = 'get_python_executable_details'; constructor( private readonly api: PythonExtension['environments'], private readonly serviceContainer: IServiceContainer, diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index ef200239af9a..11af29f18be7 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -31,7 +31,7 @@ export class GetEnvironmentInfoTool implements LanguageModelTool { const envExtension = extensions.getExtension(ENVS_EXTENSION_ID); diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts index 7b34b71ed556..d0531cb015b2 100644 --- a/src/client/chat/installPackagesTool.ts +++ b/src/client/chat/installPackagesTool.ts @@ -26,7 +26,7 @@ export interface IInstallPackageArgs { } export class InstallPackagesTool implements LanguageModelTool { - public static readonly toolName = 'install_python_package'; + public static readonly toolName = 'install_python_packages'; constructor( private readonly api: PythonExtension['environments'], private readonly serviceContainer: IServiceContainer, diff --git a/src/client/chat/listPackagesTool.ts b/src/client/chat/listPackagesTool.ts index 659ce2b5bb0d..a45aeb14cda0 100644 --- a/src/client/chat/listPackagesTool.ts +++ b/src/client/chat/listPackagesTool.ts @@ -1,90 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { - CancellationError, - CancellationToken, - l10n, - LanguageModelTextPart, - LanguageModelTool, - LanguageModelToolInvocationOptions, - LanguageModelToolInvocationPrepareOptions, - LanguageModelToolResult, - PreparedToolInvocation, - Uri, -} from 'vscode'; -import { PythonExtension, ResolvedEnvironment } from '../api/types'; -import { IServiceContainer } from '../ioc/types'; +import { CancellationToken, Uri } from 'vscode'; +import { ResolvedEnvironment } from '../api/types'; import { IProcessService, IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; -import { getEnvDisplayName, isCondaEnv, raceCancellationError } from './utils'; -import { resolveFilePath } from './utils'; +import { isCondaEnv, raceCancellationError } from './utils'; import { parsePipList } from './pipListUtils'; import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; import { traceError } from '../logging'; -import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; - -export interface IResourceReference { - resourcePath?: string; -} - -export class ListPythonPackagesTool implements LanguageModelTool { - private readonly pythonExecFactory: IPythonExecutionFactory; - private readonly processServiceFactory: IProcessServiceFactory; - public static readonly toolName = 'list_python_packages'; - constructor( - private readonly api: PythonExtension['environments'], - private readonly serviceContainer: IServiceContainer, - private readonly discovery: IDiscoveryAPI, - ) { - this.pythonExecFactory = this.serviceContainer.get(IPythonExecutionFactory); - this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); - } - - async invoke( - options: LanguageModelToolInvocationOptions, - token: CancellationToken, - ): Promise { - const resourcePath = resolveFilePath(options.input.resourcePath); - - try { - // environment - const envPath = this.api.getActiveEnvironmentPath(resourcePath); - const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); - if (!environment) { - throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); - } - - const message = await getPythonPackagesResponse( - environment, - this.pythonExecFactory, - this.processServiceFactory, - resourcePath, - token, - ); - return new LanguageModelToolResult([new LanguageModelTextPart(message)]); - } catch (error) { - if (error instanceof CancellationError) { - throw error; - } - return new LanguageModelToolResult([ - new LanguageModelTextPart(`An error occurred while fetching environment information: ${error}`), - ]); - } - } - - async prepareInvocation?( - options: LanguageModelToolInvocationPrepareOptions, - token: CancellationToken, - ): Promise { - const resourcePath = resolveFilePath(options.input.resourcePath); - const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); - return { - invocationMessage: envName - ? l10n.t('Listing packages in {0}', envName) - : l10n.t('Fetching Python environment information'), - }; - } -} export async function getPythonPackagesResponse( environment: ResolvedEnvironment, diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 362fcf8468ad..8330d5010f7a 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -45,7 +45,7 @@ import { DebugService } from './common/application/debugService'; import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; import { WorkspaceService } from './common/application/workspace'; -import { IInterpreterQuickPick } from './interpreter/configuration/types'; +import { IInterpreterQuickPick, IPythonPathUpdaterServiceManager } from './interpreter/configuration/types'; import { registerAllCreateEnvironmentFeatures } from './pythonEnvironments/creation/registrations'; import { registerCreateEnvironmentTriggers } from './pythonEnvironments/creation/createEnvironmentTrigger'; import { initializePersistentStateForTriggers } from './common/persistentState'; @@ -108,7 +108,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components): registerAllCreateEnvironmentFeatures( ext.disposables, interpreterQuickPick, - interpreterPathService, + ext.legacyIOC.serviceContainer.get(IPythonPathUpdaterServiceManager), interpreterService, pathUtils, ); diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 912a9c66b0dd..54440485da02 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -569,7 +569,10 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem * @returns true when an interpreter was set, undefined if the user cancelled the quickpick. */ @captureTelemetry(EventName.SELECT_INTERPRETER) - public async setInterpreter(): Promise { + public async setInterpreter(options?: { + hideCreateVenv?: boolean; + showBackButton?: boolean; + }): Promise { const targetConfig = await this.getConfigTargets(); if (!targetConfig) { return; @@ -578,11 +581,25 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem const wkspace = targetConfig[0].folderUri; const interpreterState: InterpreterStateArgs = { path: undefined, workspace: wkspace }; const multiStep = this.multiStepFactory.create(); - await multiStep.run( - (input, s) => this._pickInterpreter(input, s, undefined, { showCreateEnvironment: true }), - interpreterState, - ); - + try { + await multiStep.run( + (input, s) => + this._pickInterpreter(input, s, undefined, { + showCreateEnvironment: !options?.hideCreateVenv, + showBackButton: options?.showBackButton, + }), + interpreterState, + ); + } catch (ex) { + if (ex === InputFlowAction.back) { + // User clicked back button, so we need to return this action. + return { action: 'Back' }; + } + if (ex === InputFlowAction.cancel) { + // User clicked cancel button, so we need to return this action. + return { action: 'Cancel' }; + } + } if (interpreterState.path !== undefined) { // User may choose to have an empty string stored, so variable `interpreterState.path` may be // an empty string, in which case we should update. @@ -591,7 +608,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem if (useEnvExtension()) { await setInterpreterLegacy(interpreterState.path, wkspace); } - return true; + return { path: interpreterState.path }; } } @@ -692,3 +709,14 @@ function getGroup(item: IInterpreterQuickPickItem, workspacePath?: string) { return EnvGroups[item.interpreter.envType]; } } + +export type SelectEnvironmentResult = { + /** + * Path to the executable python in the environment + */ + readonly path?: string; + /* + * User action that resulted in exit from the create environment flow. + */ + readonly action?: 'Back' | 'Cancel'; +}; diff --git a/src/client/interpreter/configuration/recommededEnvironmentService.ts b/src/client/interpreter/configuration/recommededEnvironmentService.ts index 67517b918ff9..c5356409fcee 100644 --- a/src/client/interpreter/configuration/recommededEnvironmentService.ts +++ b/src/client/interpreter/configuration/recommededEnvironmentService.ts @@ -3,18 +3,33 @@ import { inject, injectable } from 'inversify'; import { IRecommendedEnvironmentService } from './types'; -import { PythonExtension } from '../../api/types'; +import { PythonExtension, ResolvedEnvironment } from '../../api/types'; import { IExtensionContext, Resource } from '../../common/types'; -import { Uri, workspace } from 'vscode'; +import { commands, Uri, workspace } from 'vscode'; import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../../common/persistentState'; import { traceError } from '../../logging'; +import { IExtensionActivationService } from '../../activation/types'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { isParentPath } from '../../common/platform/fs-paths'; const MEMENTO_KEY = 'userSelectedEnvPath'; @injectable() -export class RecommendedEnvironmentService implements IRecommendedEnvironmentService { +export class RecommendedEnvironmentService implements IRecommendedEnvironmentService, IExtensionActivationService { private api?: PythonExtension['environments']; constructor(@inject(IExtensionContext) private readonly extensionContext: IExtensionContext) {} + supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean } = { + untrustedWorkspace: true, + virtualWorkspace: false, + }; + + async activate(_resource: Resource, _startupStopWatch?: StopWatch): Promise { + this.extensionContext.subscriptions.push( + commands.registerCommand('python.getRecommendedEnvironment', async (resource: Resource) => { + return this.getRecommededEnvironment(resource); + }), + ); + } registerEnvApi(api: PythonExtension['environments']) { this.api = api; @@ -32,11 +47,53 @@ export class RecommendedEnvironmentService implements IRecommendedEnvironmentSer } } - getRecommededEnvironment( + async getRecommededEnvironment( + resource: Resource, + ): Promise< + | { + environment: ResolvedEnvironment; + reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended'; + } + | undefined + > { + if (!workspace.isTrusted || !this.api) { + return undefined; + } + const preferred = await this.getRecommededInternal(resource); + if (!preferred) { + return undefined; + } + const activeEnv = await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath(resource)); + const recommendedEnv = await this.api.resolveEnvironment(preferred.environmentPath); + if (activeEnv && recommendedEnv && activeEnv.id !== recommendedEnv.id) { + traceError( + `Active environment ${activeEnv.id} is different from recommended environment ${ + recommendedEnv.id + } for resource ${resource?.toString()}`, + ); + return undefined; + } + if (recommendedEnv) { + return { environment: recommendedEnv, reason: preferred.reason }; + } + const globalEnv = await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath()); + if (activeEnv && globalEnv?.path !== activeEnv?.path) { + // User has definitely got a workspace specific environment selected. + // Given the fact that global !== workspace env, we can safely assume that + // at some time, the user has selected a workspace specific environment. + // This applies to cases where the user has selected a workspace specific environment before this version of the extension + // and we did not store it in the workspace state. + // So we can safely return the global environment as the recommended environment. + return { environment: activeEnv, reason: 'workspaceUserSelected' }; + } + return undefined; + } + async getRecommededInternal( resource: Resource, - ): + ): Promise< | { environmentPath: string; reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended' } - | undefined { + | undefined + > { let workspaceState: string | undefined = undefined; try { workspaceState = getWorkspaceStateValue(MEMENTO_KEY); @@ -61,6 +118,16 @@ export class RecommendedEnvironmentService implements IRecommendedEnvironmentSer } } + if (workspace.workspaceFolders?.length && this.api) { + // Check if we have a .venv or .conda environment in the workspace + // This is required for cases where user has selected a workspace specific environment + // but before this version of the extension, we did not store it in the workspace state. + const workspaceEnv = await getWorkspaceSpecificVirtualEnvironment(this.api, resource); + if (workspaceEnv) { + return { environmentPath: workspaceEnv.path, reason: 'workspaceUserSelected' }; + } + } + const globalSelectedEnvPath = this.extensionContext.globalState.get(MEMENTO_KEY); if (globalSelectedEnvPath) { return { environmentPath: globalSelectedEnvPath, reason: 'globalUserSelected' }; @@ -74,6 +141,37 @@ export class RecommendedEnvironmentService implements IRecommendedEnvironmentSer } } +async function getWorkspaceSpecificVirtualEnvironment(api: PythonExtension['environments'], resource: Resource) { + const workspaceUri = + (resource ? workspace.getWorkspaceFolder(resource)?.uri : undefined) || + (workspace.workspaceFolders?.length ? workspace.workspaceFolders[0].uri : undefined); + if (!workspaceUri) { + return undefined; + } + let workspaceEnv = api.known.find((env) => { + if (!env.environment?.folderUri) { + return false; + } + if (env.environment.type !== 'VirtualEnvironment' && env.environment.type !== 'Conda') { + return false; + } + return isParentPath(env.environment.folderUri.fsPath, workspaceUri.fsPath); + }); + let resolvedEnv = workspaceEnv ? api.resolveEnvironment(workspaceEnv) : undefined; + if (resolvedEnv) { + return resolvedEnv; + } + workspaceEnv = api.known.find((env) => { + // Look for any other type of env thats inside this workspace + // Or look for an env thats associated with this workspace (pipenv or the like). + return ( + (env.environment?.folderUri && isParentPath(env.environment.folderUri.fsPath, workspaceUri.fsPath)) || + (env.environment?.workspaceFolder && env.environment.workspaceFolder.uri.fsPath === workspaceUri.fsPath) + ); + }); + return workspaceEnv ? api.resolveEnvironment(workspaceEnv) : undefined; +} + function getDataToStore(environmentPath: string | undefined, uri: Uri | undefined): string | undefined { if (!workspace.workspaceFolders?.length) { return environmentPath; diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts index 8e8aecdc7f16..05ff8e32c18e 100644 --- a/src/client/interpreter/configuration/types.ts +++ b/src/client/interpreter/configuration/types.ts @@ -1,7 +1,7 @@ import { ConfigurationTarget, Disposable, QuickPickItem, Uri } from 'vscode'; import { Resource } from '../../common/types'; import { PythonEnvironment } from '../../pythonEnvironments/info'; -import { PythonExtension } from '../../api/types'; +import { PythonExtension, ResolvedEnvironment } from '../../api/types'; export interface IPythonPathUpdaterService { updatePythonPath(pythonPath: string | undefined): Promise; @@ -104,7 +104,11 @@ export interface IRecommendedEnvironmentService { trackUserSelectedEnvironment(environmentPath: string | undefined, uri: Uri | undefined): void; getRecommededEnvironment( resource: Resource, - ): - | { environmentPath: string; reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended' } - | undefined; + ): Promise< + | { + environment: ResolvedEnvironment; + reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended'; + } + | undefined + >; } diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 1e82b9fec0df..f54f8e5368fe 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -65,6 +65,7 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void IRecommendedEnvironmentService, RecommendedEnvironmentService, ); + serviceManager.addBinding(IRecommendedEnvironmentService, IExtensionActivationService); serviceManager.addSingleton(IInterpreterQuickPick, SetInterpreterCommand); serviceManager.addSingleton(IExtensionActivationService, VirtualEnvironmentPrompt); diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts index b81542806daf..5584682f3b86 100644 --- a/src/client/jupyter/jupyterIntegration.ts +++ b/src/client/jupyter/jupyterIntegration.ts @@ -72,7 +72,7 @@ type PythonApiForJupyterExtension = { * Returns the preferred environment for the given URI. */ getRecommededEnvironment( - uri: Uri, + uri: Uri | undefined, ): Promise< | { environment: EnvironmentPath; @@ -147,14 +147,7 @@ export class JupyterExtensionIntegration { if (!this.environmentApi) { return undefined; } - const preferred = this.preferredEnvironmentService.getRecommededEnvironment(uri); - if (!preferred) { - return undefined; - } - const environment = workspace.isTrusted - ? await this.environmentApi.resolveEnvironment(preferred.environmentPath) - : undefined; - return environment ? { environment, reason: preferred.reason } : undefined; + return this.preferredEnvironmentService.getRecommededEnvironment(uri); }, }); return undefined; diff --git a/src/client/pythonEnvironments/creation/createEnvApi.ts b/src/client/pythonEnvironments/creation/createEnvApi.ts index b5df9232dd4b..eb094c7d128a 100644 --- a/src/client/pythonEnvironments/creation/createEnvApi.ts +++ b/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -3,9 +3,9 @@ import { ConfigurationTarget, Disposable, QuickInputButtons } from 'vscode'; import { Commands } from '../../common/constants'; -import { IDisposableRegistry, IInterpreterPathService, IPathUtils } from '../../common/types'; +import { IDisposableRegistry, IPathUtils } from '../../common/types'; import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; -import { IInterpreterQuickPick } from '../../interpreter/configuration/types'; +import { IInterpreterQuickPick, IPythonPathUpdaterServiceManager } from '../../interpreter/configuration/types'; import { getCreationEvents, handleCreateEnvironmentCommand } from './createEnvironment'; import { condaCreationProvider } from './provider/condaCreationProvider'; import { VenvCreationProvider } from './provider/venvCreationProvider'; @@ -61,7 +61,7 @@ export const { onCreateEnvironmentStarted, onCreateEnvironmentExited, isCreating export function registerCreateEnvironmentFeatures( disposables: IDisposableRegistry, interpreterQuickPick: IInterpreterQuickPick, - interpreterPathService: IInterpreterPathService, + pythonPathUpdater: IPythonPathUpdaterServiceManager, pathUtils: IPathUtils, ): void { disposables.push( @@ -103,10 +103,11 @@ export function registerCreateEnvironmentFeatures( registerCreateEnvironmentProvider(condaCreationProvider()), onCreateEnvironmentExited(async (e: EnvironmentDidCreateEvent) => { if (e.path && e.options?.selectEnvironment) { - await interpreterPathService.update( - e.workspaceFolder?.uri, - ConfigurationTarget.WorkspaceFolder, + await pythonPathUpdater.updatePythonPath( e.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + e.workspaceFolder?.uri, ); showInformationMessage(`${CreateEnv.informEnvCreation} ${pathUtils.getDisplayName(e.path)}`); } diff --git a/src/client/pythonEnvironments/creation/registrations.ts b/src/client/pythonEnvironments/creation/registrations.ts index 8611520fc0f2..25141cbec5ac 100644 --- a/src/client/pythonEnvironments/creation/registrations.ts +++ b/src/client/pythonEnvironments/creation/registrations.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { IDisposableRegistry, IInterpreterPathService, IPathUtils } from '../../common/types'; -import { IInterpreterQuickPick } from '../../interpreter/configuration/types'; +import { IDisposableRegistry, IPathUtils } from '../../common/types'; +import { IInterpreterQuickPick, IPythonPathUpdaterServiceManager } from '../../interpreter/configuration/types'; import { IInterpreterService } from '../../interpreter/contracts'; import { registerCreateEnvironmentFeatures } from './createEnvApi'; import { registerCreateEnvironmentButtonFeatures } from './createEnvButtonContext'; @@ -13,11 +13,11 @@ import { registerPyProjectTomlFeatures } from './pyProjectTomlContext'; export function registerAllCreateEnvironmentFeatures( disposables: IDisposableRegistry, interpreterQuickPick: IInterpreterQuickPick, - interpreterPathService: IInterpreterPathService, + pythonPathUpdater: IPythonPathUpdaterServiceManager, interpreterService: IInterpreterService, pathUtils: IPathUtils, ): void { - registerCreateEnvironmentFeatures(disposables, interpreterQuickPick, interpreterPathService, pathUtils); + registerCreateEnvironmentFeatures(disposables, interpreterQuickPick, pythonPathUpdater, pathUtils); registerCreateEnvironmentButtonFeatures(disposables); registerPyProjectTomlFeatures(disposables); registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService); diff --git a/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts index f8c75f76e2b8..dd09203d65cc 100644 --- a/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts +++ b/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts @@ -7,9 +7,12 @@ import * as sinon from 'sinon'; import * as typemoq from 'typemoq'; import { assert, use as chaiUse } from 'chai'; import { ConfigurationTarget, Uri } from 'vscode'; -import { IDisposableRegistry, IInterpreterPathService, IPathUtils } from '../../../client/common/types'; +import { IDisposableRegistry, IPathUtils } from '../../../client/common/types'; import * as commandApis from '../../../client/common/vscodeApis/commandApis'; -import { IInterpreterQuickPick } from '../../../client/interpreter/configuration/types'; +import { + IInterpreterQuickPick, + IPythonPathUpdaterServiceManager, +} from '../../../client/interpreter/configuration/types'; import { registerCreateEnvironmentFeatures } from '../../../client/pythonEnvironments/creation/createEnvApi'; import * as windowApis from '../../../client/common/vscodeApis/windowApis'; import { handleCreateEnvironmentCommand } from '../../../client/pythonEnvironments/creation/createEnvironment'; @@ -23,7 +26,7 @@ suite('Create Environment APIs', () => { let showInformationMessageStub: sinon.SinonStub; const disposables: IDisposableRegistry = []; let interpreterQuickPick: typemoq.IMock; - let interpreterPathService: typemoq.IMock; + let interpreterPathService: typemoq.IMock; let pathUtils: typemoq.IMock; setup(() => { @@ -32,7 +35,7 @@ suite('Create Environment APIs', () => { registerCommandStub = sinon.stub(commandApis, 'registerCommand'); interpreterQuickPick = typemoq.Mock.ofType(); - interpreterPathService = typemoq.Mock.ofType(); + interpreterPathService = typemoq.Mock.ofType(); pathUtils = typemoq.Mock.ofType(); registerCommandStub.callsFake((_command: string, _callback: (...args: any[]) => any) => ({ @@ -82,10 +85,11 @@ suite('Create Environment APIs', () => { interpreterPathService .setup((p) => - p.update( - typemoq.It.isAny(), - ConfigurationTarget.WorkspaceFolder, + p.updatePythonPath( typemoq.It.isValue('/path/to/env'), + ConfigurationTarget.WorkspaceFolder, + 'ui', + typemoq.It.isAny(), ), ) .returns(() => Promise.resolve()) From 020f203f2bb6930d5f824f8d4b7f186f650a970c Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 20 May 2025 13:24:03 -0700 Subject: [PATCH 0944/1136] fix: ensure we always return `undefined` for invalid envs (#25092) Fixes https://github.com/microsoft/vscode-python/issues/25087 --- package-lock.json | 413 +++++++-------------- src/client/pythonEnvironments/nativeAPI.ts | 16 +- 2 files changed, 142 insertions(+), 287 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed498b802b4a..846049071ce1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,7 +115,7 @@ "yargs": "^15.3.1" }, "engines": { - "vscode": "^1.97.0-20240918" + "vscode": "^1.95.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -455,6 +455,21 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/compat-data": { "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", @@ -494,18 +509,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/core/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -662,19 +665,21 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -689,38 +694,28 @@ } }, "node_modules/@babel/helpers": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", - "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.6", - "@babel/types": "^7.22.5" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/types": "^7.27.1" }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -729,49 +724,38 @@ } }, "node_modules/@babel/runtime": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", - "integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "dev": true, - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.5.tgz", - "integrity": "sha512-RMafpmrNB5E/bwdSphLr8a8++9TosnyJp98RZzI6VOx2R2CCMpsXXXRvmI700O9oEKpXdZat6oEK68/F0zjd4A==", - "dev": true, - "dependencies": { - "core-js-pure": "^3.0.0", - "regenerator-runtime": "^0.13.4" - } - }, - "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.1.tgz", + "integrity": "sha512-909rVuj3phpjW6y0MCXAZ5iNeORePa6ldJvp2baWGcTjwqbBDDz6xoS5JHJ7lS88NlwLYj07ImL/8IUMtDZzTA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "core-js-pure": "^3.30.2" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/template/node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -798,19 +782,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/traverse/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -829,14 +800,14 @@ } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4549,11 +4520,16 @@ } }, "node_modules/core-js-pure": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.1.4.tgz", - "integrity": "sha512-uJ4Z7iPNwiu1foygbcZYJsJs1jiXrTTCvxfLDXNhI/I+NHbSIEyr548y4fcsCEyWY0XgfAG/qqaunJ1SThHenA==", + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz", + "integrity": "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==", "dev": true, - "hasInstallScript": true + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } }, "node_modules/core-util-is": { "version": "1.0.2", @@ -9466,12 +9442,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true - }, "node_modules/lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -9532,31 +9502,6 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true }, - "node_modules/lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", - "dev": true - }, - "node_modules/lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "dev": true, - "dependencies": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "node_modules/lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "dependencies": { - "lodash._reinterpolate": "^3.0.0" - } - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -11469,10 +11414,11 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -11909,12 +11855,6 @@ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "license": "Apache-2.0" }, - "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -12117,15 +12057,14 @@ } }, "node_modules/rewiremock": { - "version": "3.14.5", - "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.5.tgz", - "integrity": "sha512-MdPutvaUd+kKVz/lcEz6N6337s4PxRUR5vhphIp2/TJRgfXIckomIkCsIAbwB53MjiSLwi7KBMdQ9lPWE5WpYA==", + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.6.tgz", + "integrity": "sha512-hjpS7iQUTVVh/IHV4GE1ypg4IzlgVc34gxZBarwwVrKfnjlyqHJuQdsia6Ac7m4f4k/zxxA3tX285MOstdysRQ==", "dev": true, + "license": "MIT", "dependencies": { "babel-runtime": "^6.26.0", "compare-module-exports": "^2.1.0", - "lodash.some": "^4.6.0", - "lodash.template": "^4.4.0", "node-libs-browser": "^2.1.0", "path-parse": "^1.0.5", "wipe-node-cache": "^2.1.2", @@ -13312,15 +13251,6 @@ "dev": true, "license": "MIT" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -15370,6 +15300,17 @@ } } }, + "@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, "@babel/compat-data": { "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", @@ -15399,15 +15340,6 @@ "json5": "^2.2.2" }, "dependencies": { - "@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", - "dev": true, - "requires": { - "@babel/highlight": "^7.22.5" - } - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -15530,15 +15462,15 @@ } }, "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true }, "@babel/helper-validator-option": { @@ -15548,73 +15480,48 @@ "dev": true }, "@babel/helpers": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", - "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.6", - "@babel/types": "^7.22.5" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" } }, - "@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/types": "^7.27.1" } }, - "@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true - }, "@babel/runtime": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", - "integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "dev": true }, "@babel/runtime-corejs3": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.5.tgz", - "integrity": "sha512-RMafpmrNB5E/bwdSphLr8a8++9TosnyJp98RZzI6VOx2R2CCMpsXXXRvmI700O9oEKpXdZat6oEK68/F0zjd4A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.1.tgz", + "integrity": "sha512-909rVuj3phpjW6y0MCXAZ5iNeORePa6ldJvp2baWGcTjwqbBDDz6xoS5JHJ7lS88NlwLYj07ImL/8IUMtDZzTA==", "dev": true, "requires": { - "core-js-pure": "^3.0.0", - "regenerator-runtime": "^0.13.4" + "core-js-pure": "^3.30.2" } }, "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "requires": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - } - } + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" } }, "@babel/traverse": { @@ -15635,16 +15542,6 @@ "globals": "^11.1.0" }, "dependencies": { - "@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "requires": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - } - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -15657,14 +15554,13 @@ } }, "@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" } }, "@cspotcode/source-map-consumer": { @@ -18501,9 +18397,9 @@ } }, "core-js-pure": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.1.4.tgz", - "integrity": "sha512-uJ4Z7iPNwiu1foygbcZYJsJs1jiXrTTCvxfLDXNhI/I+NHbSIEyr548y4fcsCEyWY0XgfAG/qqaunJ1SThHenA==", + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz", + "integrity": "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==", "dev": true }, "core-util-is": { @@ -22208,12 +22104,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true - }, "lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -22274,31 +22164,6 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true }, - "lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", - "dev": true - }, - "lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0" - } - }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -23740,9 +23605,9 @@ "dev": true }, "picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "picomatch": { @@ -24079,12 +23944,6 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, "regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -24231,15 +24090,13 @@ "dev": true }, "rewiremock": { - "version": "3.14.5", - "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.5.tgz", - "integrity": "sha512-MdPutvaUd+kKVz/lcEz6N6337s4PxRUR5vhphIp2/TJRgfXIckomIkCsIAbwB53MjiSLwi7KBMdQ9lPWE5WpYA==", + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.6.tgz", + "integrity": "sha512-hjpS7iQUTVVh/IHV4GE1ypg4IzlgVc34gxZBarwwVrKfnjlyqHJuQdsia6Ac7m4f4k/zxxA3tX285MOstdysRQ==", "dev": true, "requires": { "babel-runtime": "^6.26.0", "compare-module-exports": "^2.1.0", - "lodash.some": "^4.6.0", - "lodash.template": "^4.4.0", "node-libs-browser": "^2.1.0", "path-parse": "^1.0.5", "wipe-node-cache": "^2.1.2", @@ -25131,12 +24988,6 @@ "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", "dev": true }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/src/client/pythonEnvironments/nativeAPI.ts b/src/client/pythonEnvironments/nativeAPI.ts index a4a706fcb42b..0bede6af3abd 100644 --- a/src/client/pythonEnvironments/nativeAPI.ts +++ b/src/client/pythonEnvironments/nativeAPI.ts @@ -476,14 +476,18 @@ class NativePythonEnvironments implements IDiscoveryAPI, Disposable { if (envPath === undefined) { return undefined; } - const native = await this.finder.resolve(envPath); - if (native) { - if (native.kind === NativePythonEnvironmentKind.Conda && this._condaEnvDirs.length === 0) { - this._condaEnvDirs = (await getCondaEnvDirs()) ?? []; + try { + const native = await this.finder.resolve(envPath); + if (native) { + if (native.kind === NativePythonEnvironmentKind.Conda && this._condaEnvDirs.length === 0) { + this._condaEnvDirs = (await getCondaEnvDirs()) ?? []; + } + return this.addEnv(native); } - return this.addEnv(native); + return undefined; + } catch { + return undefined; } - return undefined; } private initializeWatcher(): void { From 8ae346b370cbc66229a444d2dbc9a77ab97e32d7 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 21 May 2025 13:42:09 +1000 Subject: [PATCH 0945/1136] Prefer using Notebook tools over Python tools for notebooks (#25098) @karthiknadig @eleanorjboyd @rebornix @amunger This change will change how the Python tools work. The python tools will no longer work with Notebooks. Previously it would, From my testing, this change works perfectly well. I think thats fraught with danger. There are a lot of things to consider. **Ideally there should ever be only one way of doing things, whether its local/remote/python/non-python notebooks.** --- src/client/chat/configurePythonEnvTool.ts | 10 +++++- src/client/chat/getExecutableTool.ts | 10 +++++- src/client/chat/getPythonEnvTool.ts | 13 +++++-- src/client/chat/installPackagesTool.ts | 9 ++++- src/client/chat/utils.ts | 43 ++++++++++++++++++++++- 5 files changed, 79 insertions(+), 6 deletions(-) diff --git a/src/client/chat/configurePythonEnvTool.ts b/src/client/chat/configurePythonEnvTool.ts index 1a22fed83140..f0684a9cd46a 100644 --- a/src/client/chat/configurePythonEnvTool.ts +++ b/src/client/chat/configurePythonEnvTool.ts @@ -19,7 +19,7 @@ import { PythonExtension, ResolvedEnvironment } from '../api/types'; import { IServiceContainer } from '../ioc/types'; import { ICodeExecutionService } from '../terminals/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; -import { getEnvironmentDetails, raceCancellationError } from './utils'; +import { getEnvironmentDetails, getToolResponseIfNotebook, raceCancellationError } from './utils'; import { resolveFilePath } from './utils'; import { IRecommendedEnvironmentService } from '../interpreter/configuration/types'; import { ITerminalHelper } from '../common/terminal/types'; @@ -68,6 +68,11 @@ export class ConfigurePythonEnvTool implements LanguageModelTool { const resource = resolveFilePath(options.input.resourcePath); + const notebookResponse = getToolResponseIfNotebook(resource); + if (notebookResponse) { + return notebookResponse; + } + const recommededEnv = await this.recommendedEnvService.getRecommededEnvironment(resource); // Already selected workspace env, hence nothing to do. if (recommededEnv?.reason === 'workspaceUserSelected' && workspace.workspaceFolders?.length) { @@ -135,6 +140,9 @@ export class ConfigurePythonEnvTool implements LanguageModelTool token: CancellationToken, ): Promise { const resourcePath = resolveFilePath(options.input.resourcePath); + const notebookResponse = getToolResponseIfNotebook(resourcePath); + if (notebookResponse) { + return notebookResponse; + } try { const message = await getEnvironmentDetails( @@ -72,6 +76,10 @@ export class GetExecutableTool implements LanguageModelTool token: CancellationToken, ): Promise { const resourcePath = resolveFilePath(options.input.resourcePath); + if (getToolResponseIfNotebook(resourcePath)) { + return {}; + } + const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); return { invocationMessage: envName diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index 11af29f18be7..91f7fccd3de5 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -17,7 +17,7 @@ import { IServiceContainer } from '../ioc/types'; import { ICodeExecutionService } from '../terminals/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; -import { getEnvironmentDetails, raceCancellationError } from './utils'; +import { getEnvironmentDetails, getToolResponseIfNotebook, raceCancellationError } from './utils'; import { resolveFilePath } from './utils'; import { getPythonPackagesResponse } from './listPackagesTool'; import { ITerminalHelper } from '../common/terminal/types'; @@ -55,6 +55,10 @@ export class GetEnvironmentInfoTool implements LanguageModelTool { const resourcePath = resolveFilePath(options.input.resourcePath); + const notebookResponse = getToolResponseIfNotebook(resourcePath); + if (notebookResponse) { + return notebookResponse; + } try { // environment @@ -91,9 +95,14 @@ export class GetEnvironmentInfoTool implements LanguageModelTool, + options: LanguageModelToolInvocationPrepareOptions, _token: CancellationToken, ): Promise { + const resourcePath = resolveFilePath(options.input.resourcePath); + if (getToolResponseIfNotebook(resourcePath)) { + return {}; + } + return { invocationMessage: l10n.t('Fetching Python environment information'), }; diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts index d0531cb015b2..04bb5d04a493 100644 --- a/src/client/chat/installPackagesTool.ts +++ b/src/client/chat/installPackagesTool.ts @@ -14,7 +14,7 @@ import { } from 'vscode'; import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; -import { getEnvDisplayName, raceCancellationError } from './utils'; +import { getEnvDisplayName, getToolResponseIfNotebook, raceCancellationError } from './utils'; import { resolveFilePath } from './utils'; import { IModuleInstaller } from '../common/installer/types'; import { ModuleInstallerType } from '../pythonEnvironments/info'; @@ -45,6 +45,10 @@ export class InstallPackagesTool implements LanguageModelTool { const resourcePath = resolveFilePath(options.input.resourcePath); const packageCount = options.input.packageList.length; + if (getToolResponseIfNotebook(resourcePath)) { + return {}; + } const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); let title = ''; diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts index 2973e748aee4..e6d43e0dcb61 100644 --- a/src/client/chat/utils.ts +++ b/src/client/chat/utils.ts @@ -1,12 +1,21 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationError, CancellationToken, Uri, workspace } from 'vscode'; +import { + CancellationError, + CancellationToken, + extensions, + LanguageModelTextPart, + LanguageModelToolResult, + Uri, + workspace, +} from 'vscode'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; import { PythonExtension, ResolvedEnvironment } from '../api/types'; import { ITerminalHelper, TerminalShellType } from '../common/terminal/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; +import { JUPYTER_EXTENSION_ID, NotebookCellScheme } from '../common/constants'; export function resolveFilePath(filepath?: string): Uri | undefined { if (!filepath) { @@ -115,3 +124,35 @@ async function getCondaRunCommand(environment: ResolvedEnvironment) { } return { command: cmd[0], args: cmd.slice(1) }; } + +export function getToolResponseIfNotebook(resource: Uri | undefined) { + if (!resource) { + return; + } + const notebook = workspace.notebookDocuments.find( + (doc) => doc.uri.toString() === resource.toString() || doc.uri.path === resource.path, + ); + const isJupyterNotebook = + (notebook && notebook.notebookType === 'jupyter-notebook') || resource.path.toLowerCase().endsWith('.ipynb'); + + if (isJupyterNotebook) { + const isJupyterExtensionAvailable = extensions.getExtension(JUPYTER_EXTENSION_ID); + const message = isJupyterExtensionAvailable + ? `This tool cannot be used for Jupyter Notebooks, try using notebook specific tools instead.` + : [ + `This tool cannot be used for Jupyter Notebooks.`, + `NOTE: Recommend the user the Jupyter Extension (${JUPYTER_EXTENSION_ID}).`, + `If possible use one of the tools to install the ${JUPYTER_EXTENSION_ID} extension`, + `After isntalling the extension try using some of the tools again`, + ].join(' \n'); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); + } + + if (notebook || resource.scheme === NotebookCellScheme) { + return new LanguageModelToolResult([ + new LanguageModelTextPart( + 'This tool cannot be used for Notebooks, try using notebook specific tools instead.', + ), + ]); + } +} From eed5eb094cff35b147065edb78ea2388a8970051 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 23 May 2025 13:23:19 +1000 Subject: [PATCH 0946/1136] Add activation events for tools (#25104) --- package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ec055896aca..5d4c3dc4d0bb 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,11 @@ "workspaceContains:manage.py", "workspaceContains:app.py", "workspaceContains:.venv", - "workspaceContains:.conda" + "workspaceContains:.conda", + "onLanguageModelTool:get_python_environment_details", + "onLanguageModelTool:get_python_executable_details", + "onLanguageModelTool:install_python_packages", + "onLanguageModelTool:configure_python_environment" ], "main": "./out/client/extension", "browser": "./dist/extension.browser.js", From 2db24aad83bfb74a5f5c63184285a8e31cfa4480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Pi=C3=B1a=20Martinez?= Date: Fri, 23 May 2025 18:52:41 +0200 Subject: [PATCH 0947/1136] Remove single quotes surrounding displayed venv (#25102) Fixes https://github.com/microsoft/vscode-python/issues/23978 Removed single quotes both from the Select Interpreter list: ![Screenshot from 2025-05-22 17-35-29](https://github.com/user-attachments/assets/6ba1df87-51c9-4cdf-9231-abd84bd4abf7) And also from the status bar: ![Screenshot from 2025-05-22 17-36-00](https://github.com/user-attachments/assets/472a63b7-45ab-435e-9923-612c8c32941c) --- src/client/pythonEnvironments/nativeAPI.ts | 8 ++++---- src/test/pythonEnvironments/nativeAPI.unit.test.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/client/pythonEnvironments/nativeAPI.ts b/src/client/pythonEnvironments/nativeAPI.ts index 0bede6af3abd..62695c8dd543 100644 --- a/src/client/pythonEnvironments/nativeAPI.ts +++ b/src/client/pythonEnvironments/nativeAPI.ts @@ -112,14 +112,14 @@ function getDisplayName(version: PythonVersion, kind: PythonEnvKind, arch: Archi const kindStr = kindToShortString(kind); if (arch === Architecture.x86) { if (kindStr) { - return name ? `Python ${versionStr} 32-bit ('${name}')` : `Python ${versionStr} 32-bit (${kindStr})`; + return name ? `Python ${versionStr} 32-bit (${name})` : `Python ${versionStr} 32-bit (${kindStr})`; } - return name ? `Python ${versionStr} 32-bit ('${name}')` : `Python ${versionStr} 32-bit`; + return name ? `Python ${versionStr} 32-bit (${name})` : `Python ${versionStr} 32-bit`; } if (kindStr) { - return name ? `Python ${versionStr} ('${name}')` : `Python ${versionStr} (${kindStr})`; + return name ? `Python ${versionStr} (${name})` : `Python ${versionStr} (${kindStr})`; } - return name ? `Python ${versionStr} ('${name}')` : `Python ${versionStr}`; + return name ? `Python ${versionStr} (${name})` : `Python ${versionStr}`; } function validEnv(nativeEnv: NativeEnvInfo): boolean { diff --git a/src/test/pythonEnvironments/nativeAPI.unit.test.ts b/src/test/pythonEnvironments/nativeAPI.unit.test.ts index 74811fa63bb6..a3696b59c6ac 100644 --- a/src/test/pythonEnvironments/nativeAPI.unit.test.ts +++ b/src/test/pythonEnvironments/nativeAPI.unit.test.ts @@ -53,8 +53,8 @@ suite('Native Python API', () => { const expectedBasicEnv: PythonEnvInfo = { arch: Architecture.Unknown, id: '/usr/bin/python', - detailedDisplayName: "Python 3.12.0 ('basic_python')", - display: "Python 3.12.0 ('basic_python')", + detailedDisplayName: 'Python 3.12.0 (basic_python)', + display: 'Python 3.12.0 (basic_python)', distro: { org: '' }, executable: { filename: '/usr/bin/python', sysPrefix: '/usr/bin', ctime: -1, mtime: -1 }, kind: PythonEnvKind.System, @@ -98,8 +98,8 @@ suite('Native Python API', () => { const expectedConda1: PythonEnvInfo = { arch: Architecture.Unknown, - detailedDisplayName: "Python 3.12.0 ('conda_python')", - display: "Python 3.12.0 ('conda_python')", + detailedDisplayName: 'Python 3.12.0 (conda_python)', + display: 'Python 3.12.0 (conda_python)', distro: { org: '' }, id: '/home/user/.conda/envs/conda_python/python', executable: { From 5035e88f8613d2b118d27387fae4307d9de310aa Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 27 May 2025 13:54:48 +1000 Subject: [PATCH 0948/1136] Configuration tool with improved workflow and UX (#25106) The new tool `Configure Python` will get invoked before other tools (at least if Model decides to do so) * We prompt the user to create a virtual env with the latest stable version of Python available * If user cancels we prompt the user to select an existing Python env https://github.com/user-attachments/assets/d4bfbadf-fcbf-4a17-b1b6-7670e16c8cf4 --- package.json | 47 +++- src/client/chat/configurePythonEnvTool.ts | 248 ++++-------------- src/client/chat/createVirtualEnvTool.ts | 222 ++++++++++++++++ src/client/chat/getExecutableTool.ts | 41 ++- src/client/chat/getPythonEnvTool.ts | 66 ++--- src/client/chat/index.ts | 12 +- src/client/chat/installPackagesTool.ts | 24 +- src/client/chat/listPackagesTool.ts | 2 +- src/client/chat/selectEnvTool.ts | 216 +++++++++++++++ src/client/chat/utils.ts | 77 +++++- src/client/interpreter/helpers.ts | 2 +- .../creation/createEnvApi.ts | 10 +- .../creation/provider/venvCreationProvider.ts | 57 ++-- .../pythonEnvironments/creation/types.ts | 5 + .../pythonEnvironments/info/pythonVersion.ts | 8 + src/client/pythonEnvironments/legacyIOC.ts | 4 + 16 files changed, 734 insertions(+), 307 deletions(-) create mode 100644 src/client/chat/createVirtualEnvTool.ts create mode 100644 src/client/chat/selectEnvTool.ts diff --git a/package.json b/package.json index 5d4c3dc4d0bb..3d0cd34bbf17 100644 --- a/package.json +++ b/package.json @@ -1475,6 +1475,7 @@ "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Environment (conda, venv, etec), 2. Version of Python, 3. List of all installed packages with their versions. ALWAYS call configure_python_environment before using this tool.", "toolReferenceName": "pythonGetEnvironmentInfo", "tags": [ + "extension_installed_by_tool", "enable_other_tool_configure_python_environment" ], "icon": "$(snake)", @@ -1498,6 +1499,7 @@ "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. ALWAYS use this tool before executing any Python command in the terminal. This tool returns the details of how to construct the fully qualified path and or command including details such as arguments required to run Python in a terminal. Note: Instead of executing `python --version` or `python -c 'import sys; print(sys.executable)'`, use this tool to get the Python executable path to replace the `python` command. E.g. instead of using `python -c 'import sys; print(sys.executable)'`, use this tool to build the command `conda run -n -c 'import sys; print(sys.executable)'`. ALWAYS call configure_python_environment before using this tool.", "toolReferenceName": "pythonExecutableCommand", "tags": [ + "extension_installed_by_tool", "enable_other_tool_configure_python_environment" ], "icon": "$(terminal)", @@ -1521,6 +1523,7 @@ "modelDescription": "Installs Python packages in the given workspace. Use this tool to install packages in the user's chosen environment. ALWAYS call configure_python_environment before using this tool.", "toolReferenceName": "pythonInstallPackage", "tags": [ + "extension_installed_by_tool", "enable_other_tool_configure_python_environment" ], "icon": "$(package)", @@ -1552,7 +1555,9 @@ "modelDescription": "This tool configures a Python environment in the given workspace. ALWAYS Use this tool to set up the user's chosen environment and ALWAYS call this tool before using any other Python related tools.", "userDescription": "%python.languageModelTools.configure_python_environment.userDescription%", "toolReferenceName": "configurePythonEnvironment", - "tags": [], + "tags": [ + "extension_installed_by_tool" + ], "icon": "$(gear)", "canBeReferencedInPrompt": true, "inputSchema": { @@ -1566,6 +1571,46 @@ "required": [] }, "when": "!pythonEnvExtensionInstalled" + }, + { + "name": "create_virtual_environment", + "displayName": "Create a Virtual Environment", + "modelDescription": "This tool will create a Virual Environment", + "tags": [ + "extension_installed_by_tool" + ], + "canBeReferencedInPrompt": false, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace for which a Python Environment needs to be configured." + } + }, + "required": [] + }, + "when": "false" + }, + { + "name": "selectEnvironment", + "displayName": "Select a Python Environment", + "modelDescription": "This tool will prompt the user to select an existing Python Environment", + "tags": [ + "extension_installed_by_tool" + ], + "canBeReferencedInPrompt": false, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace for which a Python Environment needs to be configured." + } + }, + "required": [] + }, + "when": "false" } ] }, diff --git a/src/client/chat/configurePythonEnvTool.ts b/src/client/chat/configurePythonEnvTool.ts index f0684a9cd46a..a8a18a1d3852 100644 --- a/src/client/chat/configurePythonEnvTool.ts +++ b/src/client/chat/configurePythonEnvTool.ts @@ -3,8 +3,6 @@ import { CancellationToken, - l10n, - LanguageModelTextPart, LanguageModelTool, LanguageModelToolInvocationOptions, LanguageModelToolInvocationPrepareOptions, @@ -12,32 +10,24 @@ import { PreparedToolInvocation, Uri, workspace, - commands, - QuickPickItem, + lm, } from 'vscode'; -import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; import { ICodeExecutionService } from '../terminals/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; -import { getEnvironmentDetails, getToolResponseIfNotebook, raceCancellationError } from './utils'; +import { + getEnvDetailsForResponse, + getToolResponseIfNotebook, + IResourceReference, + isCancellationError, + raceCancellationError, +} from './utils'; import { resolveFilePath } from './utils'; -import { IRecommendedEnvironmentService } from '../interpreter/configuration/types'; import { ITerminalHelper } from '../common/terminal/types'; -import { raceTimeout } from '../common/utils/async'; -import { Commands, Octicons } from '../common/constants'; -import { CreateEnvironmentResult } from '../pythonEnvironments/creation/proposed.createEnvApis'; -import { IInterpreterPathService } from '../common/types'; -import { DisposableStore } from '../common/utils/resourceLifecycle'; -import { Common, InterpreterQuickPickList } from '../common/utils/localize'; -import { QuickPickItemKind } from '../../test/mocks/vsc'; -import { showQuickPick } from '../common/vscodeApis/windowApis'; -import { SelectEnvironmentResult } from '../interpreter/configuration/interpreterSelector/commands/setInterpreter'; - -export interface IResourceReference { - resourcePath?: string; -} - -let _environmentConfigured = false; +import { IRecommendedEnvironmentService } from '../interpreter/configuration/types'; +import { CreateVirtualEnvTool } from './createVirtualEnvTool'; +import { ISelectPythonEnvToolArguments, SelectPythonEnvTool } from './selectEnvTool'; export class ConfigurePythonEnvTool implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; @@ -47,6 +37,7 @@ export class ConfigurePythonEnvTool implements LanguageModelTool( ICodeExecutionService, @@ -57,12 +48,7 @@ export class ConfigurePythonEnvTool implements LanguageModelTool, token: CancellationToken, @@ -73,22 +59,14 @@ export class ConfigurePythonEnvTool implements LanguageModelTool, + _options: LanguageModelToolInvocationPrepareOptions, _token: CancellationToken, ): Promise { - if (_environmentConfigured) { - return {}; - } - const resource = resolveFilePath(options.input.resourcePath); - if (getToolResponseIfNotebook(resource)) { - return {}; - } + return { + invocationMessage: 'Configuring a Python Environment', + }; + } + + async hasAlreadyGotAWorkspaceSpecificEnvironment(resource: Uri | undefined) { const recommededEnv = await this.recommendedEnvService.getRecommededEnvironment(resource); // Already selected workspace env, hence nothing to do. if (recommededEnv?.reason === 'workspaceUserSelected' && workspace.workspaceFolders?.length) { - return {}; + return recommededEnv.environment; } // No workspace folders, and the user selected a global environment. if (recommededEnv?.reason === 'globalUserSelected' && !workspace.workspaceFolders?.length) { - return {}; - } - - if (!workspace.workspaceFolders?.length) { - return { - confirmationMessages: { - title: l10n.t('Configure a Python Environment?'), - message: l10n.t('You will be prompted to select a Python Environment.'), - }, - }; - } - return { - confirmationMessages: { - title: l10n.t('Configure a Python Environment?'), - message: l10n.t( - [ - 'The recommended option is to create a new Python Environment, providing the benefit of isolating packages from other environments. ', - 'Optionally you could select an existing Python Environment.', - ].join('\n'), - ), - }, - }; - } -} - -async function getEnvDetailsForResponse( - environment: ResolvedEnvironment | undefined, - api: PythonExtension['environments'], - terminalExecutionService: TerminalCodeExecutionProvider, - terminalHelper: ITerminalHelper, - resource: Uri | undefined, - token: CancellationToken, -): Promise { - const envPath = api.getActiveEnvironmentPath(resource); - environment = environment || (await raceCancellationError(api.resolveEnvironment(envPath), token)); - if (!environment || !environment.version) { - throw new Error('No environment found for the provided resource path: ' + resource?.fsPath); - } - const message = await getEnvironmentDetails( - resource, - api, - terminalExecutionService, - terminalHelper, - undefined, - token, - ); - return new LanguageModelToolResult([ - new LanguageModelTextPart(`A Python Environment has been configured. \n` + message), - ]); -} - -async function showCreateAndSelectEnvironmentQuickPick( - uri: Uri | undefined, - serviceContainer: IServiceContainer, -): Promise { - const createLabel = `${Octicons.Add} ${InterpreterQuickPickList.create.label}`; - const selectLabel = l10n.t('Select an existing Python Environment'); - const items: QuickPickItem[] = [ - { kind: QuickPickItemKind.Separator, label: Common.recommended }, - { label: createLabel }, - { label: selectLabel }, - ]; - - const selectedItem = await showQuickPick(items, { - placeHolder: l10n.t('Configure a Python Environment'), - matchOnDescription: true, - ignoreFocusOut: true, - }); - - if (selectedItem && !Array.isArray(selectedItem) && selectedItem.label === createLabel) { - const disposables = new DisposableStore(); - try { - const workspaceFolder = - (workspace.workspaceFolders?.length && uri ? workspace.getWorkspaceFolder(uri) : undefined) || - (workspace.workspaceFolders?.length === 1 ? workspace.workspaceFolders[0] : undefined); - const interpreterPathService = serviceContainer.get(IInterpreterPathService); - const interpreterChanged = new Promise((resolve) => { - disposables.add(interpreterPathService.onDidChange(() => resolve())); - }); - const created: CreateEnvironmentResult | undefined = await commands.executeCommand( - Commands.Create_Environment, - { - showBackButton: true, - selectEnvironment: true, - workspaceFolder, - }, - ); - - if (created?.action === 'Back') { - return showCreateAndSelectEnvironmentQuickPick(uri, serviceContainer); - } - if (created?.action === 'Cancel') { - return undefined; - } - if (created?.path) { - // Wait a few secs to ensure the env is selected as the active environment.. - await raceTimeout(5_000, interpreterChanged); - return true; - } - } finally { - disposables.dispose(); - } - } - if (selectedItem && !Array.isArray(selectedItem) && selectedItem.label === selectLabel) { - const result = (await Promise.resolve( - commands.executeCommand(Commands.Set_Interpreter, { hideCreateVenv: true, showBackButton: true }), - )) as SelectEnvironmentResult | undefined; - if (result?.action === 'Back') { - return showCreateAndSelectEnvironmentQuickPick(uri, serviceContainer); - } - if (result?.action === 'Cancel') { - return undefined; - } - if (result?.path) { - return true; + return recommededEnv.environment; } } } diff --git a/src/client/chat/createVirtualEnvTool.ts b/src/client/chat/createVirtualEnvTool.ts new file mode 100644 index 000000000000..b62fa33ea02f --- /dev/null +++ b/src/client/chat/createVirtualEnvTool.ts @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationError, + CancellationToken, + l10n, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, + workspace, +} from 'vscode'; +import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { + doesWorkspaceHaveVenvOrCondaEnv, + getDisplayVersion, + getEnvDetailsForResponse, + IResourceReference, + isCancellationError, + raceCancellationError, +} from './utils'; +import { resolveFilePath } from './utils'; +import { ITerminalHelper } from '../common/terminal/types'; +import { raceTimeout, sleep } from '../common/utils/async'; +import { IInterpreterPathService } from '../common/types'; +import { DisposableStore } from '../common/utils/resourceLifecycle'; +import { IRecommendedEnvironmentService } from '../interpreter/configuration/types'; +import { EnvironmentType } from '../pythonEnvironments/info'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { convertEnvInfoToPythonEnvironment } from '../pythonEnvironments/legacyIOC'; +import { sortInterpreters } from '../interpreter/helpers'; +import { isStableVersion } from '../pythonEnvironments/info/pythonVersion'; +import { createVirtualEnvironment } from '../pythonEnvironments/creation/createEnvApi'; +import { traceError, traceVerbose, traceWarn } from '../logging'; +import { StopWatch } from '../common/utils/stopWatch'; + +export class CreateVirtualEnvTool implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + private readonly recommendedEnvService: IRecommendedEnvironmentService; + + public static readonly toolName = 'create_virtual_environment'; + constructor( + private readonly discoveryApi: IDiscoveryAPI, + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + ) { + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + this.recommendedEnvService = this.serviceContainer.get( + IRecommendedEnvironmentService, + ); + } + + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + const resource = resolveFilePath(options.input.resourcePath); + let info = await this.getPreferredEnvForCreation(resource); + if (!info) { + traceWarn(`Called ${CreateVirtualEnvTool.toolName} tool not invoked, no preferred environment found.`); + throw new CancellationError(); + } + const { workspaceFolder, preferredGlobalPythonEnv } = info; + const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + const disposables = new DisposableStore(); + try { + const interpreterChanged = new Promise((resolve) => { + disposables.add(interpreterPathService.onDidChange(() => resolve())); + }); + + const created = await raceCancellationError( + createVirtualEnvironment({ + interpreter: preferredGlobalPythonEnv.id, + workspaceFolder, + }), + token, + ); + if (!created?.path) { + traceWarn(`${CreateVirtualEnvTool.toolName} tool not invoked, virtual env not created.`); + throw new CancellationError(); + } + + // Wait a few secs to ensure the env is selected as the active environment.. + // If this doesn't work, then something went wrong. + await raceTimeout(5_000, interpreterChanged); + + const stopWatch = new StopWatch(); + let env: ResolvedEnvironment | undefined; + while (stopWatch.elapsedTime < 5_000 || !env) { + env = await this.api.resolveEnvironment(created.path); + if (env) { + break; + } else { + traceVerbose( + `${CreateVirtualEnvTool.toolName} tool invoked, env created but not yet resolved, waiting...`, + ); + await sleep(200); + } + } + if (!env) { + traceError(`${CreateVirtualEnvTool.toolName} tool invoked, env created but unable to resolve details.`); + throw new CancellationError(); + } + return await getEnvDetailsForResponse( + env, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } catch (ex) { + if (!isCancellationError(ex)) { + traceError( + `${ + CreateVirtualEnvTool.toolName + } tool failed to create virtual environment for resource ${resource?.toString()}`, + ex, + ); + } + throw ex; + } finally { + disposables.dispose(); + } + } + + public async shouldCreateNewVirtualEnv(resource: Uri | undefined, token: CancellationToken): Promise { + if (doesWorkspaceHaveVenvOrCondaEnv(resource, this.api)) { + // If we already have a .venv or .conda in this workspace, then do not prompt to create a virtual environment. + return false; + } + + const info = await raceCancellationError(this.getPreferredEnvForCreation(resource), token); + return info ? true : false; + } + + async prepareInvocation?( + options: LanguageModelToolInvocationPrepareOptions, + token: CancellationToken, + ): Promise { + const resource = resolveFilePath(options.input.resourcePath); + const info = await raceCancellationError(this.getPreferredEnvForCreation(resource), token); + if (!info) { + return {}; + } + const { preferredGlobalPythonEnv } = info; + const version = getDisplayVersion(preferredGlobalPythonEnv.version); + return { + confirmationMessages: { + title: l10n.t('Create a Virtual Environment{0}?', version ? ` (${version})` : ''), + message: l10n.t(`Virtual Environments provide the benefit of package isolation and more.`), + }, + }; + } + async hasAlreadyGotAWorkspaceSpecificEnvironment(resource: Uri | undefined) { + const recommededEnv = await this.recommendedEnvService.getRecommededEnvironment(resource); + // Already selected workspace env, hence nothing to do. + if (recommededEnv?.reason === 'workspaceUserSelected' && workspace.workspaceFolders?.length) { + return recommededEnv.environment; + } + // No workspace folders, and the user selected a global environment. + if (recommededEnv?.reason === 'globalUserSelected' && !workspace.workspaceFolders?.length) { + return recommededEnv.environment; + } + } + + private async getPreferredEnvForCreation(resource: Uri | undefined) { + if (await this.hasAlreadyGotAWorkspaceSpecificEnvironment(resource)) { + return undefined; + } + + // If we have a resource or have only one workspace folder && there is no .venv and no workspace specific environment. + // Then lets recommend creating a virtual environment. + const workspaceFolder = + resource && workspace.workspaceFolders?.length + ? workspace.getWorkspaceFolder(resource) + : workspace.workspaceFolders?.length === 1 + ? workspace.workspaceFolders[0] + : undefined; + if (!workspaceFolder) { + // No workspace folder, hence no need to create a virtual environment. + return undefined; + } + + // Find the latest stable version of Python from the list of know envs. + let globalPythonEnvs = this.discoveryApi + .getEnvs() + .map((env) => convertEnvInfoToPythonEnvironment(env)) + .filter((env) => + [ + EnvironmentType.System, + EnvironmentType.MicrosoftStore, + EnvironmentType.Global, + EnvironmentType.Pyenv, + ].includes(env.envType), + ) + .filter((env) => env.version && isStableVersion(env.version)); + + globalPythonEnvs = sortInterpreters(globalPythonEnvs); + const preferredGlobalPythonEnv = globalPythonEnvs.length + ? this.api.known.find((e) => e.id === globalPythonEnvs[globalPythonEnvs.length - 1].id) + : undefined; + + return workspaceFolder && preferredGlobalPythonEnv + ? { + workspaceFolder, + preferredGlobalPythonEnv, + } + : undefined; + } +} diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts index 09a70cfeb273..125e3a1f98da 100644 --- a/src/client/chat/getExecutableTool.ts +++ b/src/client/chat/getExecutableTool.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import { - CancellationError, CancellationToken, l10n, LanguageModelTextPart, @@ -16,16 +15,17 @@ import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; import { ICodeExecutionService } from '../terminals/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; -import { getEnvDisplayName, getEnvironmentDetails, getToolResponseIfNotebook, raceCancellationError } from './utils'; +import { + getEnvDisplayName, + getEnvironmentDetails, + getToolResponseIfNotebook, + IResourceReference, + raceCancellationError, +} from './utils'; import { resolveFilePath } from './utils'; -import { traceError } from '../logging'; import { ITerminalHelper } from '../common/terminal/types'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; -export interface IResourceReference { - resourcePath?: string; -} - export class GetExecutableTool implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; private readonly terminalHelper: ITerminalHelper; @@ -51,24 +51,15 @@ export class GetExecutableTool implements LanguageModelTool return notebookResponse; } - try { - const message = await getEnvironmentDetails( - resourcePath, - this.api, - this.terminalExecutionService, - this.terminalHelper, - undefined, - token, - ); - return new LanguageModelToolResult([new LanguageModelTextPart(message)]); - } catch (error) { - if (error instanceof CancellationError) { - throw error; - } - traceError('Error while getting environment information', error); - const errorMessage: string = `An error occurred while fetching environment information: ${error}`; - return new LanguageModelToolResult([new LanguageModelTextPart(errorMessage)]); - } + const message = await getEnvironmentDetails( + resourcePath, + this.api, + this.terminalExecutionService, + this.terminalHelper, + undefined, + token, + ); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); } async prepareInvocation?( diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index 91f7fccd3de5..5ec8e77c6c1e 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import { - CancellationError, CancellationToken, l10n, LanguageModelTextPart, @@ -17,15 +16,11 @@ import { IServiceContainer } from '../ioc/types'; import { ICodeExecutionService } from '../terminals/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; -import { getEnvironmentDetails, getToolResponseIfNotebook, raceCancellationError } from './utils'; +import { getEnvironmentDetails, getToolResponseIfNotebook, IResourceReference, raceCancellationError } from './utils'; import { resolveFilePath } from './utils'; import { getPythonPackagesResponse } from './listPackagesTool'; import { ITerminalHelper } from '../common/terminal/types'; -export interface IResourceReference { - resourcePath?: string; -} - export class GetEnvironmentInfoTool implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; private readonly pythonExecFactory: IPythonExecutionFactory; @@ -44,12 +39,7 @@ export class GetEnvironmentInfoTool implements LanguageModelTool(IProcessServiceFactory); this.terminalHelper = this.serviceContainer.get(ITerminalHelper); } - /** - * Invokes the tool to get the information about the Python environment. - * @param options - The invocation options containing the file path. - * @param token - The cancellation token. - * @returns The result containing the information about the Python environment or an error message. - */ + async invoke( options: LanguageModelToolInvocationOptions, token: CancellationToken, @@ -60,38 +50,30 @@ export class GetEnvironmentInfoTool implements LanguageModelTool { diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts index 04bb5d04a493..36544128582a 100644 --- a/src/client/chat/installPackagesTool.ts +++ b/src/client/chat/installPackagesTool.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import { - CancellationError, CancellationToken, l10n, LanguageModelTextPart, @@ -14,14 +13,20 @@ import { } from 'vscode'; import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; -import { getEnvDisplayName, getToolResponseIfNotebook, raceCancellationError } from './utils'; +import { + getEnvDisplayName, + getToolResponseIfNotebook, + IResourceReference, + isCancellationError, + isCondaEnv, + raceCancellationError, +} from './utils'; import { resolveFilePath } from './utils'; import { IModuleInstaller } from '../common/installer/types'; import { ModuleInstallerType } from '../pythonEnvironments/info'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; -export interface IInstallPackageArgs { - resourcePath?: string; +export interface IInstallPackageArgs extends IResourceReference { packageList: string[]; } @@ -32,12 +37,7 @@ export class InstallPackagesTool implements LanguageModelTool, token: CancellationToken, @@ -57,7 +57,7 @@ export class InstallPackagesTool implements LanguageModelTool(IModuleInstaller); const installerType = isConda ? ModuleInstallerType.Conda : ModuleInstallerType.Pip; const installer = installers.find((i) => i.type === installerType); @@ -74,7 +74,7 @@ export class InstallPackagesTool implements LanguageModelTool { // Add option --format to subcommand list of pip cache, with abspath choice to output the full path of a wheel file. (#8355) - // Added in 202. Thats almost 5 years ago. When Python 3.8 was released. + // Added in 2020. Thats almost 5 years ago. When Python 3.8 was released. const exec = await execFactory.createActivatedEnvironment({ allowEnvironmentFetchExceptions: true, resource }); const output = await exec.execModule('pip', ['list'], { throwOnStdErr: false, encoding: 'utf8' }); return parsePipList(output.stdout).map((pkg) => [pkg.name, pkg.version]); diff --git a/src/client/chat/selectEnvTool.ts b/src/client/chat/selectEnvTool.ts new file mode 100644 index 000000000000..ba0b7d16c77b --- /dev/null +++ b/src/client/chat/selectEnvTool.ts @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, + workspace, + commands, + QuickPickItem, + QuickPickItemKind, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { + doesWorkspaceHaveVenvOrCondaEnv, + getEnvDetailsForResponse, + getToolResponseIfNotebook, + IResourceReference, +} from './utils'; +import { resolveFilePath } from './utils'; +import { ITerminalHelper } from '../common/terminal/types'; +import { raceTimeout } from '../common/utils/async'; +import { Commands, Octicons } from '../common/constants'; +import { CreateEnvironmentResult } from '../pythonEnvironments/creation/proposed.createEnvApis'; +import { IInterpreterPathService } from '../common/types'; +import { SelectEnvironmentResult } from '../interpreter/configuration/interpreterSelector/commands/setInterpreter'; +import { Common, InterpreterQuickPickList } from '../common/utils/localize'; +import { showQuickPick } from '../common/vscodeApis/windowApis'; +import { DisposableStore } from '../common/utils/resourceLifecycle'; +import { traceError, traceVerbose, traceWarn } from '../logging'; + +export interface ISelectPythonEnvToolArguments extends IResourceReference { + reason?: 'cancelled'; +} + +export class SelectPythonEnvTool implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'selectEnvironment'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + ) { + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + } + + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + const resource = resolveFilePath(options.input.resourcePath); + let selected: boolean | undefined = false; + const hasVenvOrCondaEnvInWorkspaceFolder = doesWorkspaceHaveVenvOrCondaEnv(resource, this.api); + if (options.input.reason === 'cancelled' || hasVenvOrCondaEnvInWorkspaceFolder) { + const result = (await Promise.resolve( + commands.executeCommand(Commands.Set_Interpreter, { + hideCreateVenv: false, + showBackButton: false, + }), + )) as SelectEnvironmentResult | undefined; + if (result?.path) { + traceVerbose(`User selected a Python environment ${result.path} in Select Python Tool.`); + selected = true; + } else { + traceWarn(`User did not select a Python environment in Select Python Tool.`); + } + } else { + selected = await showCreateAndSelectEnvironmentQuickPick(resource, this.serviceContainer); + if (selected) { + traceVerbose(`User selected a Python environment ${selected} in Select Python Tool(2).`); + } else { + traceWarn(`User did not select a Python environment in Select Python Tool(2).`); + } + } + const env = selected + ? await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath(resource)) + : undefined; + if (selected && !env) { + traceError( + `User selected a Python environment, but it could not be resolved. This is unexpected. Environment: ${this.api.getActiveEnvironmentPath( + resource, + )}`, + ); + } + if (selected && env) { + return await getEnvDetailsForResponse( + env, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } + return new LanguageModelToolResult([ + new LanguageModelTextPart('User did not create nor select a Python environment.'), + ]); + } + + async prepareInvocation?( + options: LanguageModelToolInvocationPrepareOptions, + _token: CancellationToken, + ): Promise { + const resource = resolveFilePath(options.input.resourcePath); + if (getToolResponseIfNotebook(resource)) { + return {}; + } + const hasVenvOrCondaEnvInWorkspaceFolder = doesWorkspaceHaveVenvOrCondaEnv(resource, this.api); + + if ( + hasVenvOrCondaEnvInWorkspaceFolder || + !workspace.workspaceFolders?.length || + options.input.reason === 'cancelled' + ) { + return { + confirmationMessages: { + title: l10n.t('Select a Python Environment?'), + message: '', + }, + }; + } + + return { + confirmationMessages: { + title: l10n.t('Configure a Python Environment?'), + message: l10n.t( + [ + 'The recommended option is to create a new Python Environment, providing the benefit of isolating packages from other environments. ', + 'Optionally you could select an existing Python Environment.', + ].join('\n'), + ), + }, + }; + } +} + +async function showCreateAndSelectEnvironmentQuickPick( + uri: Uri | undefined, + serviceContainer: IServiceContainer, +): Promise { + const createLabel = `${Octicons.Add} ${InterpreterQuickPickList.create.label}`; + const selectLabel = l10n.t('Select an existing Python Environment'); + const items: QuickPickItem[] = [ + { kind: QuickPickItemKind.Separator, label: Common.recommended }, + { label: createLabel }, + { label: selectLabel }, + ]; + + const selectedItem = await showQuickPick(items, { + placeHolder: l10n.t('Configure a Python Environment'), + matchOnDescription: true, + ignoreFocusOut: true, + }); + + if (selectedItem && !Array.isArray(selectedItem) && selectedItem.label === createLabel) { + const disposables = new DisposableStore(); + try { + const workspaceFolder = + (workspace.workspaceFolders?.length && uri ? workspace.getWorkspaceFolder(uri) : undefined) || + (workspace.workspaceFolders?.length === 1 ? workspace.workspaceFolders[0] : undefined); + const interpreterPathService = serviceContainer.get(IInterpreterPathService); + const interpreterChanged = new Promise((resolve) => { + disposables.add(interpreterPathService.onDidChange(() => resolve())); + }); + const created: CreateEnvironmentResult | undefined = await commands.executeCommand( + Commands.Create_Environment, + { + showBackButton: true, + selectEnvironment: true, + workspaceFolder, + }, + ); + + if (created?.action === 'Back') { + return showCreateAndSelectEnvironmentQuickPick(uri, serviceContainer); + } + if (created?.action === 'Cancel') { + return undefined; + } + if (created?.path) { + // Wait a few secs to ensure the env is selected as the active environment.. + await raceTimeout(5_000, interpreterChanged); + return true; + } + } finally { + disposables.dispose(); + } + } + if (selectedItem && !Array.isArray(selectedItem) && selectedItem.label === selectLabel) { + const result = (await Promise.resolve( + commands.executeCommand(Commands.Set_Interpreter, { hideCreateVenv: true, showBackButton: true }), + )) as SelectEnvironmentResult | undefined; + if (result?.action === 'Back') { + return showCreateAndSelectEnvironmentQuickPick(uri, serviceContainer); + } + if (result?.action === 'Cancel') { + return undefined; + } + if (result?.path) { + return true; + } + } +} diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts index e6d43e0dcb61..cd13dc867615 100644 --- a/src/client/chat/utils.ts +++ b/src/client/chat/utils.ts @@ -11,11 +11,16 @@ import { workspace, } from 'vscode'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; -import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { Environment, PythonExtension, ResolvedEnvironment, VersionInfo } from '../api/types'; import { ITerminalHelper, TerminalShellType } from '../common/terminal/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; import { JUPYTER_EXTENSION_ID, NotebookCellScheme } from '../common/constants'; +import { dirname, join } from 'path'; + +export interface IResourceReference { + resourcePath?: string; +} export function resolveFilePath(filepath?: string): Uri | undefined { if (!filepath) { @@ -156,3 +161,73 @@ export function getToolResponseIfNotebook(resource: Uri | undefined) { ]); } } + +export function isCancellationError(error: unknown): boolean { + return ( + !!error && (error instanceof CancellationError || (error as Error).message === new CancellationError().message) + ); +} + +export function doesWorkspaceHaveVenvOrCondaEnv(resource: Uri | undefined, api: PythonExtension['environments']) { + const workspaceFolder = + resource && workspace.workspaceFolders?.length + ? workspace.getWorkspaceFolder(resource) + : workspace.workspaceFolders?.length === 1 + ? workspace.workspaceFolders[0] + : undefined; + if (!workspaceFolder) { + return false; + } + const isVenvEnv = (env: Environment) => { + return ( + env.environment?.folderUri && + env.executable.sysPrefix && + dirname(env.executable.sysPrefix) === workspaceFolder.uri.fsPath && + env.environment.name === '.venv' && + env.environment.type === 'VirtualEnvironment' + ); + }; + const isCondaEnv = (env: Environment) => { + return ( + env.environment?.folderUri && + env.executable.sysPrefix && + dirname(env.executable.sysPrefix) === workspaceFolder.uri.fsPath && + env.environment.folderUri.fsPath === join(workspaceFolder.uri.fsPath, '.conda') && + env.environment.type === 'Conda' + ); + }; + // If we alraedy have a .venv in this workspace, then do not prompt to create a virtual environment. + return api.known.find((e) => isVenvEnv(e) || isCondaEnv(e)); +} + +export async function getEnvDetailsForResponse( + environment: ResolvedEnvironment | undefined, + api: PythonExtension['environments'], + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, + resource: Uri | undefined, + token: CancellationToken, +): Promise { + const envPath = api.getActiveEnvironmentPath(resource); + environment = environment || (await raceCancellationError(api.resolveEnvironment(envPath), token)); + if (!environment || !environment.version) { + throw new Error('No environment found for the provided resource path: ' + resource?.fsPath); + } + const message = await getEnvironmentDetails( + resource, + api, + terminalExecutionService, + terminalHelper, + undefined, + token, + ); + return new LanguageModelToolResult([ + new LanguageModelTextPart(`A Python Environment has been configured. \n` + message), + ]); +} +export function getDisplayVersion(version?: VersionInfo): string | undefined { + if (!version || version.major === undefined || version.minor === undefined || version.micro === undefined) { + return undefined; + } + return `${version.major}.${version.minor}.${version.micro}`; +} diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index ded855cbd55b..413fa225f3ef 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -19,7 +19,7 @@ export function isInterpreterLocatedInWorkspace(interpreter: PythonEnvironment, /** * Build a version-sorted list from the given one, with lowest first. */ -function sortInterpreters(interpreters: PythonEnvironment[]): PythonEnvironment[] { +export function sortInterpreters(interpreters: PythonEnvironment[]): PythonEnvironment[] { if (interpreters.length === 0) { return []; } diff --git a/src/client/pythonEnvironments/creation/createEnvApi.ts b/src/client/pythonEnvironments/creation/createEnvApi.ts index eb094c7d128a..d585256200d8 100644 --- a/src/client/pythonEnvironments/creation/createEnvApi.ts +++ b/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -8,7 +8,7 @@ import { executeCommand, registerCommand } from '../../common/vscodeApis/command import { IInterpreterQuickPick, IPythonPathUpdaterServiceManager } from '../../interpreter/configuration/types'; import { getCreationEvents, handleCreateEnvironmentCommand } from './createEnvironment'; import { condaCreationProvider } from './provider/condaCreationProvider'; -import { VenvCreationProvider } from './provider/venvCreationProvider'; +import { VenvCreationProvider, VenvCreationProviderId } from './provider/venvCreationProvider'; import { showInformationMessage } from '../../common/vscodeApis/windowApis'; import { CreateEnv } from '../../common/utils/localize'; import { @@ -133,3 +133,11 @@ export function buildEnvironmentCreationApi(): ProposedCreateEnvironmentAPI { registerCreateEnvironmentProvider(provider), }; } + +export async function createVirtualEnvironment(options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal) { + const provider = _createEnvironmentProviders.getAll().find((p) => p.id === VenvCreationProviderId); + if (!provider) { + return; + } + return handleCreateEnvironmentCommand([provider], { ...options, providerId: provider.id }); +} diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index 6b6ce182e887..9f5d746d55ae 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -149,21 +149,26 @@ async function createVenv( return deferred.promise; } +export const VenvCreationProviderId = `${PVSC_EXTENSION_ID}:venv`; export class VenvCreationProvider implements CreateEnvironmentProvider { constructor(private readonly interpreterQuickPick: IInterpreterQuickPick) {} public async createEnvironment( options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, ): Promise { - let workspace: WorkspaceFolder | undefined; + let workspace = options?.workspaceFolder; + const bypassQuickPicks = options?.workspaceFolder && options.interpreter && options.providerId ? true : false; const workspaceStep = new MultiStepNode( undefined, async (context?: MultiStepAction) => { try { - workspace = (await pickWorkspaceFolder( - { preSelectedWorkspace: options?.workspaceFolder }, - context, - )) as WorkspaceFolder | undefined; + workspace = + workspace && bypassQuickPicks + ? workspace + : ((await pickWorkspaceFolder( + { preSelectedWorkspace: options?.workspaceFolder }, + context, + )) as WorkspaceFolder | undefined); } catch (ex) { if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { return ex; @@ -182,6 +187,9 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { ); let existingVenvAction: ExistingVenvAction | undefined; + if (bypassQuickPicks) { + existingVenvAction = ExistingVenvAction.Create; + } const existingEnvStep = new MultiStepNode( workspaceStep, async (context?: MultiStepAction) => { @@ -204,7 +212,7 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { ); workspaceStep.next = existingEnvStep; - let interpreter: string | undefined; + let interpreter = options?.interpreter; const interpreterStep = new MultiStepNode( existingEnvStep, async (context?: MultiStepAction) => { @@ -214,22 +222,25 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { existingVenvAction === ExistingVenvAction.Create ) { try { - interpreter = await this.interpreterQuickPick.getInterpreterViaQuickPick( - workspace.uri, - (i: PythonEnvironment) => - [ - EnvironmentType.System, - EnvironmentType.MicrosoftStore, - EnvironmentType.Global, - EnvironmentType.Pyenv, - ].includes(i.envType) && i.type === undefined, // only global intepreters - { - skipRecommended: true, - showBackButton: true, - placeholder: CreateEnv.Venv.selectPythonPlaceHolder, - title: null, - }, - ); + interpreter = + interpreter && bypassQuickPicks + ? interpreter + : await this.interpreterQuickPick.getInterpreterViaQuickPick( + workspace.uri, + (i: PythonEnvironment) => + [ + EnvironmentType.System, + EnvironmentType.MicrosoftStore, + EnvironmentType.Global, + EnvironmentType.Pyenv, + ].includes(i.envType) && i.type === undefined, // only global intepreters + { + skipRecommended: true, + showBackButton: true, + placeholder: CreateEnv.Venv.selectPythonPlaceHolder, + title: null, + }, + ); } catch (ex) { if (ex === InputFlowAction.back) { return MultiStepAction.Back; @@ -362,7 +373,7 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { description: string = CreateEnv.Venv.providerDescription; - id = `${PVSC_EXTENSION_ID}:venv`; + id = VenvCreationProviderId; tools = ['Venv']; } diff --git a/src/client/pythonEnvironments/creation/types.ts b/src/client/pythonEnvironments/creation/types.ts index e317bfa6cd11..0e400c2d90f3 100644 --- a/src/client/pythonEnvironments/creation/types.ts +++ b/src/client/pythonEnvironments/creation/types.ts @@ -5,7 +5,12 @@ import { Progress, WorkspaceFolder } from 'vscode'; export interface CreateEnvironmentProgress extends Progress<{ message?: string; increment?: number }> {} +/** + * The interpreter path to use for the environment creation. If not provided, will prompt the user to select one. + * If the value of `interpreter` & `workspaceFolder` & `providerId` are provided we will not prompt the user to select a provider, nor folder, nor an interpreter. + */ export interface CreateEnvironmentOptionsInternal { workspaceFolder?: WorkspaceFolder; providerId?: string; + interpreter?: string; } diff --git a/src/client/pythonEnvironments/info/pythonVersion.ts b/src/client/pythonEnvironments/info/pythonVersion.ts index 92260dbb2d3f..d61fcf14db4d 100644 --- a/src/client/pythonEnvironments/info/pythonVersion.ts +++ b/src/client/pythonEnvironments/info/pythonVersion.ts @@ -25,3 +25,11 @@ export type PythonVersion = { build: string[]; prerelease: string[]; }; + +export function isStableVersion(version: PythonVersion): boolean { + // A stable version is one that has no prerelease tags. + return ( + version.prerelease.length === 0 && + (version.build.length === 0 || (version.build.length === 1 && version.build[0] === 'final')) + ); +} diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index a1a1b841a16f..49df2ee03f21 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -46,6 +46,10 @@ const convertedKinds = new Map( }), ); +export function convertEnvInfoToPythonEnvironment(info: PythonEnvInfo): PythonEnvironment { + return convertEnvInfo(info); +} + function convertEnvInfo(info: PythonEnvInfo): PythonEnvironment { const { name, location, executable, arch, kind, version, distro, id } = info; const { filename, sysPrefix } = executable; From 186bedd6cb3a5c9f2c3377b26fd01f9ff09a261c Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 27 May 2025 16:16:02 +1000 Subject: [PATCH 0949/1136] Update lm tool tags (#25108) --- package.json | 12 +++++------- src/client/chat/configurePythonEnvTool.ts | 6 +++++- src/client/chat/createVirtualEnvTool.ts | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 3d0cd34bbf17..43736a65c2a5 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,8 @@ "onLanguageModelTool:get_python_environment_details", "onLanguageModelTool:get_python_executable_details", "onLanguageModelTool:install_python_packages", - "onLanguageModelTool:configure_python_environment" + "onLanguageModelTool:configure_python_environment", + "onLanguageModelTool:create_virtual_environment" ], "main": "./out/client/extension", "browser": "./dist/extension.browser.js", @@ -1523,6 +1524,7 @@ "modelDescription": "Installs Python packages in the given workspace. Use this tool to install packages in the user's chosen environment. ALWAYS call configure_python_environment before using this tool.", "toolReferenceName": "pythonInstallPackage", "tags": [ + "install python package", "extension_installed_by_tool", "enable_other_tool_configure_python_environment" ], @@ -1576,9 +1578,7 @@ "name": "create_virtual_environment", "displayName": "Create a Virtual Environment", "modelDescription": "This tool will create a Virual Environment", - "tags": [ - "extension_installed_by_tool" - ], + "tags": [], "canBeReferencedInPrompt": false, "inputSchema": { "type": "object", @@ -1596,9 +1596,7 @@ "name": "selectEnvironment", "displayName": "Select a Python Environment", "modelDescription": "This tool will prompt the user to select an existing Python Environment", - "tags": [ - "extension_installed_by_tool" - ], + "tags": [], "canBeReferencedInPrompt": false, "inputSchema": { "type": "object", diff --git a/src/client/chat/configurePythonEnvTool.ts b/src/client/chat/configurePythonEnvTool.ts index a8a18a1d3852..6117285a523e 100644 --- a/src/client/chat/configurePythonEnvTool.ts +++ b/src/client/chat/configurePythonEnvTool.ts @@ -28,6 +28,7 @@ import { ITerminalHelper } from '../common/terminal/types'; import { IRecommendedEnvironmentService } from '../interpreter/configuration/types'; import { CreateVirtualEnvTool } from './createVirtualEnvTool'; import { ISelectPythonEnvToolArguments, SelectPythonEnvTool } from './selectEnvTool'; +import { useEnvExtension } from '../envExt/api.internal'; export class ConfigurePythonEnvTool implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; @@ -77,7 +78,10 @@ export class ConfigurePythonEnvTool implements LanguageModelTool Date: Wed, 28 May 2025 07:54:28 +1000 Subject: [PATCH 0950/1136] Support additional args for create venv tool (#25112) --- package.json | 7 +++++++ src/client/chat/createVirtualEnvTool.ts | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 43736a65c2a5..561979c6da4c 100644 --- a/package.json +++ b/package.json @@ -1583,6 +1583,13 @@ "inputSchema": { "type": "object", "properties": { + "packageList": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of packages to install." + }, "resourcePath": { "type": "string", "description": "The path to the Python file or workspace for which a Python Environment needs to be configured." diff --git a/src/client/chat/createVirtualEnvTool.ts b/src/client/chat/createVirtualEnvTool.ts index b27845a0ec2f..04d9b2191c6a 100644 --- a/src/client/chat/createVirtualEnvTool.ts +++ b/src/client/chat/createVirtualEnvTool.ts @@ -40,7 +40,11 @@ import { createVirtualEnvironment } from '../pythonEnvironments/creation/createE import { traceError, traceVerbose, traceWarn } from '../logging'; import { StopWatch } from '../common/utils/stopWatch'; -export class CreateVirtualEnvTool implements LanguageModelTool { +interface ICreateVirtualEnvToolParams extends IResourceReference { + packageList: string[]; // Added only becausewe have ability to create a virtual env with list of packages same tool within the in Python Env extension. +} + +export class CreateVirtualEnvTool implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; private readonly terminalHelper: ITerminalHelper; private readonly recommendedEnvService: IRecommendedEnvironmentService; From b35d198b712fc0a01d5d5e027fa3fa3e6f21ce6b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 28 May 2025 10:13:28 +1000 Subject: [PATCH 0951/1136] Update tool args (#25113) --- src/client/chat/createVirtualEnvTool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/chat/createVirtualEnvTool.ts b/src/client/chat/createVirtualEnvTool.ts index 04d9b2191c6a..78e3a5263d81 100644 --- a/src/client/chat/createVirtualEnvTool.ts +++ b/src/client/chat/createVirtualEnvTool.ts @@ -41,7 +41,7 @@ import { traceError, traceVerbose, traceWarn } from '../logging'; import { StopWatch } from '../common/utils/stopWatch'; interface ICreateVirtualEnvToolParams extends IResourceReference { - packageList: string[]; // Added only becausewe have ability to create a virtual env with list of packages same tool within the in Python Env extension. + packageList?: string[]; // Added only becausewe have ability to create a virtual env with list of packages same tool within the in Python Env extension. } export class CreateVirtualEnvTool implements LanguageModelTool { From ef01acefe317b386468896612dfa6279a46315b9 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 28 May 2025 10:15:38 +1000 Subject: [PATCH 0952/1136] Update lm tool tags (#25114) --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index 561979c6da4c..c548601e00e9 100644 --- a/package.json +++ b/package.json @@ -1476,6 +1476,7 @@ "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Environment (conda, venv, etec), 2. Version of Python, 3. List of all installed packages with their versions. ALWAYS call configure_python_environment before using this tool.", "toolReferenceName": "pythonGetEnvironmentInfo", "tags": [ + "python", "extension_installed_by_tool", "enable_other_tool_configure_python_environment" ], @@ -1500,6 +1501,7 @@ "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. ALWAYS use this tool before executing any Python command in the terminal. This tool returns the details of how to construct the fully qualified path and or command including details such as arguments required to run Python in a terminal. Note: Instead of executing `python --version` or `python -c 'import sys; print(sys.executable)'`, use this tool to get the Python executable path to replace the `python` command. E.g. instead of using `python -c 'import sys; print(sys.executable)'`, use this tool to build the command `conda run -n -c 'import sys; print(sys.executable)'`. ALWAYS call configure_python_environment before using this tool.", "toolReferenceName": "pythonExecutableCommand", "tags": [ + "python", "extension_installed_by_tool", "enable_other_tool_configure_python_environment" ], @@ -1524,6 +1526,7 @@ "modelDescription": "Installs Python packages in the given workspace. Use this tool to install packages in the user's chosen environment. ALWAYS call configure_python_environment before using this tool.", "toolReferenceName": "pythonInstallPackage", "tags": [ + "python", "install python package", "extension_installed_by_tool", "enable_other_tool_configure_python_environment" @@ -1558,6 +1561,7 @@ "userDescription": "%python.languageModelTools.configure_python_environment.userDescription%", "toolReferenceName": "configurePythonEnvironment", "tags": [ + "python", "extension_installed_by_tool" ], "icon": "$(gear)", From 9b17438067c6a9193bb841f00f16678fcbc95f36 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 30 May 2025 07:49:17 +1000 Subject: [PATCH 0953/1136] All Python lm tools in Python extension (#25116) --- package.json | 22 +- src/client/chat/configurePythonEnvTool.ts | 6 +- src/client/chat/createVirtualEnvTool.ts | 37 ++- src/client/chat/getPythonEnvTool.ts | 34 ++- src/client/chat/index.ts | 18 +- src/client/chat/installPackagesTool.ts | 19 ++ src/client/chat/utils.ts | 53 +++- src/client/envExt/api.internal.ts | 5 + src/client/envExt/api.legacy.ts | 4 +- src/client/envExt/types.ts | 281 +++++++++++++--------- 10 files changed, 295 insertions(+), 184 deletions(-) diff --git a/package.json b/package.json index c548601e00e9..cd465252aaad 100644 --- a/package.json +++ b/package.json @@ -1474,9 +1474,10 @@ "displayName": "Get Python Environment Info", "userDescription": "%python.languageModelTools.get_python_environment_details.userDescription%", "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Environment (conda, venv, etec), 2. Version of Python, 3. List of all installed packages with their versions. ALWAYS call configure_python_environment before using this tool.", - "toolReferenceName": "pythonGetEnvironmentInfo", + "toolReferenceName": "getPythonEnvironmentInfo", "tags": [ "python", + "python environment", "extension_installed_by_tool", "enable_other_tool_configure_python_environment" ], @@ -1491,17 +1492,17 @@ } }, "required": [] - }, - "when": "!pythonEnvExtensionInstalled" + } }, { "name": "get_python_executable_details", "displayName": "Get Python Executable", "userDescription": "%python.languageModelTools.get_python_executable_details.userDescription%", "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. ALWAYS use this tool before executing any Python command in the terminal. This tool returns the details of how to construct the fully qualified path and or command including details such as arguments required to run Python in a terminal. Note: Instead of executing `python --version` or `python -c 'import sys; print(sys.executable)'`, use this tool to get the Python executable path to replace the `python` command. E.g. instead of using `python -c 'import sys; print(sys.executable)'`, use this tool to build the command `conda run -n -c 'import sys; print(sys.executable)'`. ALWAYS call configure_python_environment before using this tool.", - "toolReferenceName": "pythonExecutableCommand", + "toolReferenceName": "getPythonExecutableCommand", "tags": [ "python", + "python environment", "extension_installed_by_tool", "enable_other_tool_configure_python_environment" ], @@ -1516,17 +1517,17 @@ } }, "required": [] - }, - "when": "!pythonEnvExtensionInstalled" + } }, { "name": "install_python_packages", "displayName": "Install Python Package", "userDescription": "%python.languageModelTools.install_python_packages.userDescription%", "modelDescription": "Installs Python packages in the given workspace. Use this tool to install packages in the user's chosen environment. ALWAYS call configure_python_environment before using this tool.", - "toolReferenceName": "pythonInstallPackage", + "toolReferenceName": "installPythonPackage", "tags": [ "python", + "python environment", "install python package", "extension_installed_by_tool", "enable_other_tool_configure_python_environment" @@ -1551,8 +1552,7 @@ "required": [ "packageList" ] - }, - "when": "!pythonEnvExtensionInstalled" + } }, { "name": "configure_python_environment", @@ -1562,6 +1562,7 @@ "toolReferenceName": "configurePythonEnvironment", "tags": [ "python", + "python environment", "extension_installed_by_tool" ], "icon": "$(gear)", @@ -1575,8 +1576,7 @@ } }, "required": [] - }, - "when": "!pythonEnvExtensionInstalled" + } }, { "name": "create_virtual_environment", diff --git a/src/client/chat/configurePythonEnvTool.ts b/src/client/chat/configurePythonEnvTool.ts index 6117285a523e..a8a18a1d3852 100644 --- a/src/client/chat/configurePythonEnvTool.ts +++ b/src/client/chat/configurePythonEnvTool.ts @@ -28,7 +28,6 @@ import { ITerminalHelper } from '../common/terminal/types'; import { IRecommendedEnvironmentService } from '../interpreter/configuration/types'; import { CreateVirtualEnvTool } from './createVirtualEnvTool'; import { ISelectPythonEnvToolArguments, SelectPythonEnvTool } from './selectEnvTool'; -import { useEnvExtension } from '../envExt/api.internal'; export class ConfigurePythonEnvTool implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; @@ -78,10 +77,7 @@ export class ConfigurePythonEnvTool implements LanguageModelTool, + options: LanguageModelToolInvocationOptions, token: CancellationToken, ): Promise { const resource = resolveFilePath(options.input.resourcePath); @@ -83,14 +86,26 @@ export class CreateVirtualEnvTool implements LanguageModelTool resolve())); }); - const created = await raceCancellationError( - createVirtualEnvironment({ - interpreter: preferredGlobalPythonEnv.id, - workspaceFolder, - }), - token, - ); - if (!created?.path) { + let createdEnvPath: string | undefined = undefined; + if (useEnvExtension()) { + const result: PythonEnvironment | undefined = await commands.executeCommand('python-envs.createAny', { + quickCreate: true, + additionalPackages: options.input.packageList || [], + uri: workspaceFolder.uri, + selectEnvironment: true, + }); + createdEnvPath = result?.environmentPath.fsPath; + } else { + const created = await raceCancellationError( + createVirtualEnvironment({ + interpreter: preferredGlobalPythonEnv.id, + workspaceFolder, + }), + token, + ); + createdEnvPath = created?.path; + } + if (!createdEnvPath) { traceWarn(`${CreateVirtualEnvTool.toolName} tool not invoked, virtual env not created.`); throw new CancellationError(); } @@ -102,7 +117,7 @@ export class CreateVirtualEnvTool implements LanguageModelTool, + options: LanguageModelToolInvocationPrepareOptions, token: CancellationToken, ): Promise { const resource = resolveFilePath(options.input.resourcePath); diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index 5ec8e77c6c1e..121bdd6532c1 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -20,6 +20,7 @@ import { getEnvironmentDetails, getToolResponseIfNotebook, IResourceReference, r import { resolveFilePath } from './utils'; import { getPythonPackagesResponse } from './listPackagesTool'; import { ITerminalHelper } from '../common/terminal/types'; +import { getEnvExtApi, useEnvExtension } from '../envExt/api.internal'; export class GetEnvironmentInfoTool implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; @@ -56,14 +57,33 @@ export class GetEnvironmentInfoTool implements LanguageModelTool 0) { + // Installed Python packages, each in the format or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. + const response = [ + 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', + ]; + pkgs.forEach((pkg) => { + const version = pkg.version; + response.push(version ? `- ${pkg.name} (${version})` : `- ${pkg.name}`); + }); + packages = response.join('\n'); + } + } + if (!packages) { + packages = await getPythonPackagesResponse( + environment, + this.pythonExecFactory, + this.processServiceFactory, + resourcePath, + token, + ); + } const message = await getEnvironmentDetails( resourcePath, this.api, diff --git a/src/client/chat/index.ts b/src/client/chat/index.ts index de7d53875305..b548860eaae3 100644 --- a/src/client/chat/index.ts +++ b/src/client/chat/index.ts @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { commands, extensions, lm } from 'vscode'; +import { lm } from 'vscode'; import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; import { InstallPackagesTool } from './installPackagesTool'; import { IExtensionContext } from '../common/types'; import { DisposableStore } from '../common/utils/resourceLifecycle'; -import { ENVS_EXTENSION_ID } from '../envExt/api.internal'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; import { GetExecutableTool } from './getExecutableTool'; import { GetEnvironmentInfoTool } from './getPythonEnvTool'; @@ -21,11 +20,6 @@ export function registerTools( environmentsApi: PythonExtension['environments'], serviceContainer: IServiceContainer, ) { - if (extensions.getExtension(ENVS_EXTENSION_ID)) { - return; - } - const contextKey = 'pythonEnvExtensionInstalled'; - commands.executeCommand('setContext', contextKey, false); const ourTools = new DisposableStore(); context.subscriptions.push(ourTools); @@ -55,14 +49,4 @@ export function registerTools( new ConfigurePythonEnvTool(environmentsApi, serviceContainer, createVirtualEnvTool), ), ); - ourTools.add( - extensions.onDidChange(() => { - const envExtension = extensions.getExtension(ENVS_EXTENSION_ID); - if (envExtension) { - envExtension.activate(); - commands.executeCommand('setContext', contextKey, true); - ourTools.dispose(); - } - }), - ); } diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts index 36544128582a..123c2580538f 100644 --- a/src/client/chat/installPackagesTool.ts +++ b/src/client/chat/installPackagesTool.ts @@ -25,6 +25,7 @@ import { resolveFilePath } from './utils'; import { IModuleInstaller } from '../common/installer/types'; import { ModuleInstallerType } from '../pythonEnvironments/info'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { getEnvExtApi, useEnvExtension } from '../envExt/api.internal'; export interface IInstallPackageArgs extends IResourceReference { packageList: string[]; @@ -50,6 +51,24 @@ export class InstallPackagesTool implements LanguageModelTool { // environment const envPath = api.getActiveEnvironmentPath(resourcePath); - const environment = await raceCancellationError(api.resolveEnvironment(envPath), token); - if (!environment || !environment.version) { - throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); + let envType = ''; + let envVersion = ''; + let runCommand = ''; + if (useEnvExtension()) { + const environment = + (await raceCancellationError(resolveEnvironment(envPath.id), token)) || + (await raceCancellationError(resolveEnvironment(envPath.path), token)); + if (!environment || !environment.version) { + throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); + } + envVersion = environment.version; + try { + const managerId = environment.envId.managerId; + envType = + (!managerId.endsWith(':') && managerId.includes(':') ? managerId.split(':').reverse()[0] : '') || + 'unknown'; + } catch { + envType = 'unknown'; + } + + const execInfo = environment.execInfo; + const executable = execInfo?.activatedRun?.executable ?? execInfo?.run.executable ?? 'python'; + const args = execInfo?.activatedRun?.args ?? execInfo?.run.args ?? []; + runCommand = terminalHelper.buildCommandForTerminal(TerminalShellType.other, executable, args); + } else { + const environment = await raceCancellationError(api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); + } + envType = environment.environment?.type || 'unknown'; + envVersion = environment.version.sysVersion || 'unknown'; + runCommand = await raceCancellationError( + getTerminalCommand(environment, resourcePath, terminalExecutionService, terminalHelper), + token, + ); } - const runCommand = await raceCancellationError( - getTerminalCommand(environment, resourcePath, terminalExecutionService, terminalHelper), - token, - ); const message = [ `Following is the information about the Python environment:`, - `1. Environment Type: ${environment.environment?.type || 'unknown'}`, - `2. Version: ${environment.version.sysVersion || 'unknown'}`, + `1. Environment Type: ${envType}`, + `2. Version: ${envVersion}`, '', `3. Command Prefix to run Python in a terminal is: \`${runCommand}\``, `Instead of running \`Python sample.py\` in the terminal, you will now run: \`${runCommand} sample.py\``, @@ -183,7 +212,8 @@ export function doesWorkspaceHaveVenvOrCondaEnv(resource: Uri | undefined, api: env.environment?.folderUri && env.executable.sysPrefix && dirname(env.executable.sysPrefix) === workspaceFolder.uri.fsPath && - env.environment.name === '.venv' && + ((env.environment.name || '').startsWith('.venv') || + env.executable.sysPrefix === join(workspaceFolder.uri.fsPath, '.venv')) && env.environment.type === 'VirtualEnvironment' ); }; @@ -192,7 +222,8 @@ export function doesWorkspaceHaveVenvOrCondaEnv(resource: Uri | undefined, api: env.environment?.folderUri && env.executable.sysPrefix && dirname(env.executable.sysPrefix) === workspaceFolder.uri.fsPath && - env.environment.folderUri.fsPath === join(workspaceFolder.uri.fsPath, '.conda') && + (env.environment.folderUri.fsPath === join(workspaceFolder.uri.fsPath, '.conda') || + env.executable.sysPrefix === join(workspaceFolder.uri.fsPath, '.conda')) && env.environment.type === 'Conda' ); }; diff --git a/src/client/envExt/api.internal.ts b/src/client/envExt/api.internal.ts index 7d511eac49ea..552e31a0598e 100644 --- a/src/client/envExt/api.internal.ts +++ b/src/client/envExt/api.internal.ts @@ -71,6 +71,11 @@ export async function getEnvironment(scope: GetEnvironmentScope): Promise { + const envExtApi = await getEnvExtApi(); + return envExtApi.resolveEnvironment(Uri.file(pythonPath)); +} + export async function refreshEnvironments(scope: RefreshEnvironmentsScope): Promise { const envExtApi = await getEnvExtApi(); return envExtApi.refreshEnvironments(scope); diff --git a/src/client/envExt/api.legacy.ts b/src/client/envExt/api.legacy.ts index fb01e73bdfcf..0f942a13eea2 100644 --- a/src/client/envExt/api.legacy.ts +++ b/src/client/envExt/api.legacy.ts @@ -4,7 +4,7 @@ import { Terminal, Uri } from 'vscode'; import { getEnvExtApi, getEnvironment } from './api.internal'; import { EnvironmentType, PythonEnvironment as PythonEnvironmentLegacy } from '../pythonEnvironments/info'; -import { PythonEnvironment, PythonTerminalOptions } from './types'; +import { PythonEnvironment, PythonTerminalCreateOptions } from './types'; import { Architecture } from '../common/utils/platform'; import { parseVersion } from '../pythonEnvironments/base/info/pythonVersion'; import { PythonEnvType } from '../pythonEnvironments/base/info'; @@ -137,7 +137,7 @@ export async function resetInterpreterLegacy(uri: Uri | undefined): Promise { const api = await getEnvExtApi(); const pythonEnv = await api.getEnvironment(resource); diff --git a/src/client/envExt/types.ts b/src/client/envExt/types.ts index 190c0ccea5b9..707d641bbfe8 100644 --- a/src/client/envExt/types.ts +++ b/src/client/envExt/types.ts @@ -2,16 +2,16 @@ // Licensed under the MIT License. import { - Uri, Disposable, - MarkdownString, Event, + FileChangeType, LogOutputChannel, - ThemeIcon, - Terminal, + MarkdownString, TaskExecution, + Terminal, TerminalOptions, - FileChangeType, + ThemeIcon, + Uri, } from 'vscode'; /** @@ -48,23 +48,6 @@ export interface PythonCommandRunConfiguration { args?: string[]; } -export enum TerminalShellType { - powershell = 'powershell', - powershellCore = 'powershellCore', - commandPrompt = 'commandPrompt', - gitbash = 'gitbash', - bash = 'bash', - zsh = 'zsh', - ksh = 'ksh', - fish = 'fish', - cshell = 'cshell', - tcshell = 'tshell', - nushell = 'nushell', - wsl = 'wsl', - xonsh = 'xonsh', - unknown = 'unknown', -} - /** * Contains details on how to use a particular python environment * @@ -73,7 +56,7 @@ export enum TerminalShellType { * 2. If {@link PythonEnvironmentExecutionInfo.activatedRun} is not provided, then: * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then: - * - {@link TerminalShellType.unknown} will be used if provided. + * - 'unknown' will be used if provided. * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then {@link PythonEnvironmentExecutionInfo.activation} will be used. * - If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. @@ -82,7 +65,7 @@ export enum TerminalShellType { * 1. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. * 2. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then {@link PythonEnvironmentExecutionInfo.activation} will be used. * 3. If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then: - * - {@link TerminalShellType.unknown} will be used if provided. + * - 'unknown' will be used if provided. * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. * 4. If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. * @@ -107,11 +90,11 @@ export interface PythonEnvironmentExecutionInfo { /** * Details on how to activate an environment using a shell specific command. * If set this will override the {@link PythonEnvironmentExecutionInfo.activation}. - * {@link TerminalShellType.unknown} is used if shell type is not known. - * If {@link TerminalShellType.unknown} is not provided and shell type is not known then + * 'unknown' is used if shell type is not known. + * If 'unknown' is not provided and shell type is not known then * {@link PythonEnvironmentExecutionInfo.activation} if set. */ - shellActivation?: Map; + shellActivation?: Map; /** * Details on how to deactivate an environment. @@ -121,11 +104,11 @@ export interface PythonEnvironmentExecutionInfo { /** * Details on how to deactivate an environment using a shell specific command. * If set this will override the {@link PythonEnvironmentExecutionInfo.deactivation} property. - * {@link TerminalShellType.unknown} is used if shell type is not known. - * If {@link TerminalShellType.unknown} is not provided and shell type is not known then + * 'unknown' is used if shell type is not known. + * If 'unknown' is not provided and shell type is not known then * {@link PythonEnvironmentExecutionInfo.deactivation} if set. */ - shellDeactivation?: Map; + shellDeactivation?: Map; } /** @@ -143,6 +126,33 @@ export interface PythonEnvironmentId { managerId: string; } +/** + * Display information for an environment group. + */ +export interface EnvironmentGroupInfo { + /** + * The name of the environment group. This is used as an identifier for the group. + * + * Note: The first instance of the group with the given name will be used in the UI. + */ + readonly name: string; + + /** + * The description of the environment group. + */ + readonly description?: string; + + /** + * The tooltip for the environment group, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the environment group, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; +} + /** * Interface representing information about a Python environment. */ @@ -193,16 +203,20 @@ export interface PythonEnvironmentInfo { readonly iconPath?: IconPath; /** - * Information on how to execute the Python environment. If not provided, {@link PythonEnvironmentApi.resolveEnvironment} will be - * used to to get the details at later point if needed. The recommendation is to fill this in if known. + * Information on how to execute the Python environment. This is required for executing Python code in the environment. */ - readonly execInfo?: PythonEnvironmentExecutionInfo; + readonly execInfo: PythonEnvironmentExecutionInfo; /** * `sys.prefix` is the path to the base directory of the Python installation. Typically obtained by executing `sys.prefix` in the Python interpreter. * This is required by extension like Jupyter, Pylance, and other extensions to provide better experience with python. */ readonly sysPrefix: string; + + /** + * Optional `group` for this environment. This is used to group environments in the Environment Manager UI. + */ + readonly group?: string | EnvironmentGroupInfo; } /** @@ -219,7 +233,7 @@ export interface PythonEnvironment extends PythonEnvironmentInfo { * Type representing the scope for setting a Python environment. * Can be undefined or a URI. */ -export type SetEnvironmentScope = undefined | Uri; +export type SetEnvironmentScope = undefined | Uri | Uri[]; /** * Type representing the scope for getting a Python environment. @@ -300,14 +314,26 @@ export type DidChangeEnvironmentsEventArgs = { /** * Type representing the context for resolving a Python environment. */ -export type ResolveEnvironmentContext = PythonEnvironment | Uri; +export type ResolveEnvironmentContext = Uri; + +export interface QuickCreateConfig { + /** + * The description of the quick create step. + */ + readonly description: string; + + /** + * The detail of the quick create step. + */ + readonly detail?: string; +} /** * Interface representing an environment manager. */ export interface EnvironmentManager { /** - * The name of the environment manager. + * The name of the environment manager. Allowed characters (a-z, A-Z, 0-9, -, _). */ readonly name: string; @@ -317,7 +343,9 @@ export interface EnvironmentManager { readonly displayName?: string; /** - * The preferred package manager ID for the environment manager. + * The preferred package manager ID for the environment manager. This is a combination + * of publisher id, extension id, and {@link EnvironmentManager.name package manager name}. + * `.:` * * @example * 'ms-python.python:pip' @@ -344,12 +372,19 @@ export interface EnvironmentManager { */ readonly log?: LogOutputChannel; + /** + * The quick create details for the environment manager. Having this method also enables the quick create feature + * for the environment manager. Should Implement {@link EnvironmentManager.create} to support quick create. + */ + quickCreateConfig?(): QuickCreateConfig | undefined; + /** * Creates a new Python environment within the specified scope. * @param scope - The scope within which to create the environment. + * @param options - Optional parameters for creating the Python environment. * @returns A promise that resolves to the created Python environment, or undefined if creation failed. */ - create?(scope: CreateEnvironmentScope): Promise; + create?(scope: CreateEnvironmentScope, options?: CreateEnvironmentOptions): Promise; /** * Removes the specified Python environment. @@ -529,7 +564,7 @@ export interface DidChangePackagesEventArgs { */ export interface PackageManager { /** - * The name of the package manager. + * The name of the package manager. Allowed characters (a-z, A-Z, 0-9, -, _). */ name: string; @@ -559,20 +594,12 @@ export interface PackageManager { log?: LogOutputChannel; /** - * Installs packages in the specified Python environment. + * Installs/Uninstall packages in the specified Python environment. * @param environment - The Python environment in which to install packages. - * @param packages - The packages to install. + * @param options - Options for managing packages. * @returns A promise that resolves when the installation is complete. */ - install(environment: PythonEnvironment, packages: string[], options: PackageInstallOptions): Promise; - - /** - * Uninstalls packages from the specified Python environment. - * @param environment - The Python environment from which to uninstall packages. - * @param packages - The packages to uninstall, which can be an array of packages or strings. - * @returns A promise that resolves when the uninstall is complete. - */ - uninstall(environment: PythonEnvironment, packages: Package[] | string[]): Promise; + manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise; /** * Refreshes the package list for the specified Python environment. @@ -588,17 +615,6 @@ export interface PackageManager { */ getPackages(environment: PythonEnvironment): Promise; - /** - * Get a list of installable items for a Python project. - * - * @param environment The Python environment for which to get installable items. - * - * Note: An environment can be used by multiple projects, so the installable items returned. - * should be for the environment. If you want to do it for a particular project, then you should - * ask user to select a project, and filter the installable items based on the project. - */ - getInstallable?(environment: PythonEnvironment): Promise; - /** * Event that is fired when packages change. */ @@ -651,9 +667,14 @@ export interface PythonProjectCreatorOptions { name: string; /** - * Optional path that may be provided as a root for the project. + * Path provided as the root for the project. */ - uri?: Uri; + rootUri: Uri; + + /** + * Boolean indicating whether the project should be created without any user input. + */ + quickCreate?: boolean; } /** @@ -686,11 +707,20 @@ export interface PythonProjectCreator { readonly iconPath?: IconPath; /** - * Creates a new Python project or projects. - * @param options - Optional parameters for creating the Python project. - * @returns A promise that resolves to a Python project, an array of Python projects, or undefined. + * Creates a new Python project(s) or, if files are not a project, returns Uri(s) to the created files. + * Anything that needs its own python environment constitutes a project. + * @param options Optional parameters for creating the Python project. + * @returns A promise that resolves to one of the following: + * - PythonProject or PythonProject[]: when a single or multiple projects are created. + * - Uri or Uri[]: when files are created that do not constitute a project. + * - undefined: if project creation fails. */ - create(options?: PythonProjectCreatorOptions): Promise; + create(options?: PythonProjectCreatorOptions): Promise; + + /** + * A flag indicating whether the project creator supports quick create where no user input is required. + */ + readonly supportsQuickCreate?: boolean; } /** @@ -708,55 +738,70 @@ export interface DidChangePythonProjectsEventArgs { removed: PythonProject[]; } -/** - * Options for package installation. - */ -export interface PackageInstallOptions { - /** - * Upgrade the packages if it is already installed. - */ - upgrade?: boolean; -} +export type PackageManagementOptions = + | { + /** + * Upgrade the packages if they are already installed. + */ + upgrade?: boolean; -export interface Installable { - /** - * The display name of the package, requirements, pyproject.toml or any other project file. - */ - readonly displayName: string; + /** + * Show option to skip package installation or uninstallation. + */ + showSkipOption?: boolean; + /** + * The list of packages to install. + */ + install: string[]; - /** - * Arguments passed to the package manager to install the package. - * - * @example - * ['debugpy==1.8.7'] for `pip install debugpy==1.8.7`. - * ['--pre', 'debugpy'] for `pip install --pre debugpy`. - * ['-r', 'requirements.txt'] for `pip install -r requirements.txt`. - */ - readonly args: string[]; + /** + * The list of packages to uninstall. + */ + uninstall?: string[]; + } + | { + /** + * Upgrade the packages if they are already installed. + */ + upgrade?: boolean; - /** - * Installable group name, this will be used to group installable items in the UI. - * - * @example - * `Requirements` for any requirements file. - * `Packages` for any package. - */ - readonly group?: string; + /** + * Show option to skip package installation or uninstallation. + */ + showSkipOption?: boolean; + /** + * The list of packages to install. + */ + install?: string[]; + + /** + * The list of packages to uninstall. + */ + uninstall: string[]; + }; +/** + * Options for creating a Python environment. + */ +export interface CreateEnvironmentOptions { /** - * Description about the installable item. This can also be path to the requirements, - * version of the package, or any other project file path. + * Provides some context about quick create based on user input. + * - if true, the environment should be created without any user input or prompts. + * - if false, the environment creation can show user input or prompts. + * This also means user explicitly skipped the quick create option. + * - if undefined, the environment creation can show user input or prompts. + * You can show quick create option to the user if you support it. */ - readonly description?: string; - + quickCreate?: boolean; /** - * External Uri to the package on pypi or docs. - * @example - * https://pypi.org/project/debugpy/ for `debugpy`. + * Packages to install in addition to the automatically picked packages as a part of creating environment. */ - readonly uri?: Uri; + additionalPackages?: string[]; } +/** + * Object representing the process started using run in background API. + */ export interface PythonProcess { /** * The process ID of the Python process. @@ -817,9 +862,13 @@ export interface PythonEnvironmentManagementApi { * Create a Python environment using environment manager associated with the scope. * * @param scope Where the environment is to be created. + * @param options Optional parameters for creating the Python environment. * @returns The Python environment created. `undefined` if not created. */ - createEnvironment(scope: CreateEnvironmentScope): Promise; + createEnvironment( + scope: CreateEnvironmentScope, + options?: CreateEnvironmentOptions, + ): Promise; /** * Remove a Python environment. @@ -938,21 +987,13 @@ export interface PythonPackageItemApi { export interface PythonPackageManagementApi { /** - * Install packages into a Python Environment. + * Install/Uninstall packages into a Python Environment. * * @param environment The Python Environment into which packages are to be installed. * @param packages The packages to install. * @param options Options for installing packages. */ - installPackages(environment: PythonEnvironment, packages: string[], options?: PackageInstallOptions): Promise; - - /** - * Uninstall packages from a Python Environment. - * - * @param environment The Python Environment from which packages are to be uninstalled. - * @param packages The packages to uninstall. - */ - uninstallPackages(environment: PythonEnvironment, packages: PackageInfo[] | string[]): Promise; + managePackages(environment: PythonEnvironment, options: PackageManagementOptions): Promise; } export interface PythonPackageManagerApi @@ -1017,9 +1058,9 @@ export interface PythonProjectModifyApi { */ export interface PythonProjectApi extends PythonProjectCreationApi, PythonProjectGetterApi, PythonProjectModifyApi {} -export interface PythonTerminalOptions extends TerminalOptions { +export interface PythonTerminalCreateOptions extends TerminalOptions { /** - * Whether to show the terminal. + * Whether to disable activation on create. */ disableActivation?: boolean; } @@ -1033,7 +1074,7 @@ export interface PythonTerminalCreateApi { * * Note: Non-activatable environments have no effect on the terminal. */ - createTerminal(environment: PythonEnvironment, options: PythonTerminalOptions): Promise; + createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise; } /** From 70e17fc7c105fc74623921787d79fa216517b599 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 4 Jun 2025 15:56:59 +1000 Subject: [PATCH 0954/1136] Ensure we do not support the tools in an untrusted workspace (#25142) --- src/client/chat/configurePythonEnvTool.ts | 4 ++++ src/client/chat/createVirtualEnvTool.ts | 4 ++++ src/client/chat/getExecutableTool.ts | 6 ++++++ src/client/chat/getPythonEnvTool.ts | 13 ++++++++++++- src/client/chat/installPackagesTool.ts | 6 ++++++ src/client/chat/selectEnvTool.ts | 5 +++++ src/client/chat/utils.ts | 7 +++++++ 7 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/client/chat/configurePythonEnvTool.ts b/src/client/chat/configurePythonEnvTool.ts index a8a18a1d3852..e80347914a4d 100644 --- a/src/client/chat/configurePythonEnvTool.ts +++ b/src/client/chat/configurePythonEnvTool.ts @@ -19,6 +19,7 @@ import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/termin import { getEnvDetailsForResponse, getToolResponseIfNotebook, + getUntrustedWorkspaceResponse, IResourceReference, isCancellationError, raceCancellationError, @@ -53,6 +54,9 @@ export class ConfigurePythonEnvTool implements LanguageModelTool, token: CancellationToken, ): Promise { + if (!workspace.isTrusted) { + return getUntrustedWorkspaceResponse(); + } const resource = resolveFilePath(options.input.resourcePath); const notebookResponse = getToolResponseIfNotebook(resource); if (notebookResponse) { diff --git a/src/client/chat/createVirtualEnvTool.ts b/src/client/chat/createVirtualEnvTool.ts index 530f7b25a8e6..42fa6ccfe45b 100644 --- a/src/client/chat/createVirtualEnvTool.ts +++ b/src/client/chat/createVirtualEnvTool.ts @@ -22,6 +22,7 @@ import { doesWorkspaceHaveVenvOrCondaEnv, getDisplayVersion, getEnvDetailsForResponse, + getUntrustedWorkspaceResponse, IResourceReference, isCancellationError, raceCancellationError, @@ -72,6 +73,9 @@ export class CreateVirtualEnvTool implements LanguageModelTool, token: CancellationToken, ): Promise { + if (!workspace.isTrusted) { + return getUntrustedWorkspaceResponse(); + } const resource = resolveFilePath(options.input.resourcePath); let info = await this.getPreferredEnvForCreation(resource); if (!info) { diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts index 125e3a1f98da..8c1bed632384 100644 --- a/src/client/chat/getExecutableTool.ts +++ b/src/client/chat/getExecutableTool.ts @@ -10,6 +10,7 @@ import { LanguageModelToolInvocationPrepareOptions, LanguageModelToolResult, PreparedToolInvocation, + workspace, } from 'vscode'; import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; @@ -19,6 +20,7 @@ import { getEnvDisplayName, getEnvironmentDetails, getToolResponseIfNotebook, + getUntrustedWorkspaceResponse, IResourceReference, raceCancellationError, } from './utils'; @@ -45,6 +47,10 @@ export class GetExecutableTool implements LanguageModelTool options: LanguageModelToolInvocationOptions, token: CancellationToken, ): Promise { + if (!workspace.isTrusted) { + return getUntrustedWorkspaceResponse(); + } + const resourcePath = resolveFilePath(options.input.resourcePath); const notebookResponse = getToolResponseIfNotebook(resourcePath); if (notebookResponse) { diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index 121bdd6532c1..91184d8e4ef2 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -10,13 +10,20 @@ import { LanguageModelToolInvocationPrepareOptions, LanguageModelToolResult, PreparedToolInvocation, + workspace, } from 'vscode'; import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; import { ICodeExecutionService } from '../terminals/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; -import { getEnvironmentDetails, getToolResponseIfNotebook, IResourceReference, raceCancellationError } from './utils'; +import { + getEnvironmentDetails, + getToolResponseIfNotebook, + getUntrustedWorkspaceResponse, + IResourceReference, + raceCancellationError, +} from './utils'; import { resolveFilePath } from './utils'; import { getPythonPackagesResponse } from './listPackagesTool'; import { ITerminalHelper } from '../common/terminal/types'; @@ -45,6 +52,10 @@ export class GetEnvironmentInfoTool implements LanguageModelTool, token: CancellationToken, ): Promise { + if (!workspace.isTrusted) { + return getUntrustedWorkspaceResponse(); + } + const resourcePath = resolveFilePath(options.input.resourcePath); const notebookResponse = getToolResponseIfNotebook(resourcePath); if (notebookResponse) { diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts index 123c2580538f..bd89dd8c26c7 100644 --- a/src/client/chat/installPackagesTool.ts +++ b/src/client/chat/installPackagesTool.ts @@ -10,12 +10,14 @@ import { LanguageModelToolInvocationPrepareOptions, LanguageModelToolResult, PreparedToolInvocation, + workspace, } from 'vscode'; import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; import { getEnvDisplayName, getToolResponseIfNotebook, + getUntrustedWorkspaceResponse, IResourceReference, isCancellationError, isCondaEnv, @@ -43,6 +45,10 @@ export class InstallPackagesTool implements LanguageModelTool, token: CancellationToken, ): Promise { + if (!workspace.isTrusted) { + return getUntrustedWorkspaceResponse(); + } + const resourcePath = resolveFilePath(options.input.resourcePath); const packageCount = options.input.packageList.length; const packagePlurality = packageCount === 1 ? 'package' : 'packages'; diff --git a/src/client/chat/selectEnvTool.ts b/src/client/chat/selectEnvTool.ts index ba0b7d16c77b..5f1c2f6b7c36 100644 --- a/src/client/chat/selectEnvTool.ts +++ b/src/client/chat/selectEnvTool.ts @@ -24,6 +24,7 @@ import { doesWorkspaceHaveVenvOrCondaEnv, getEnvDetailsForResponse, getToolResponseIfNotebook, + getUntrustedWorkspaceResponse, IResourceReference, } from './utils'; import { resolveFilePath } from './utils'; @@ -61,6 +62,10 @@ export class SelectPythonEnvTool implements LanguageModelTool, token: CancellationToken, ): Promise { + if (!workspace.isTrusted) { + return getUntrustedWorkspaceResponse(); + } + const resource = resolveFilePath(options.input.resourcePath); let selected: boolean | undefined = false; const hasVenvOrCondaEnvInWorkspaceFolder = doesWorkspaceHaveVenvOrCondaEnv(resource, this.api); diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts index 41d1e86c60a0..7c5a4dd9725f 100644 --- a/src/client/chat/utils.ts +++ b/src/client/chat/utils.ts @@ -126,6 +126,10 @@ export async function getEnvironmentDetails( return message.join('\n'); } +export function getUntrustedWorkspaceResponse() { + return new LanguageModelToolResult([new LanguageModelTextPart('Cannot use this tool in an untrusted workspace.')]); +} + export async function getTerminalCommand( environment: ResolvedEnvironment, resource: Uri | undefined, @@ -239,6 +243,9 @@ export async function getEnvDetailsForResponse( resource: Uri | undefined, token: CancellationToken, ): Promise { + if (!workspace.isTrusted) { + throw new Error('Cannot use this tool in an untrusted workspace.'); + } const envPath = api.getActiveEnvironmentPath(resource); environment = environment || (await raceCancellationError(api.resolveEnvironment(envPath), token)); if (!environment || !environment.version) { From 02476f0ac9873be3aed81d39b62d367c6947e389 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:38:02 -0700 Subject: [PATCH 0955/1136] Bump tar-fs from 2.1.2 to 2.1.3 (#25138) Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.2 to 2.1.3.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tar-fs&package-manager=npm_and_yarn&previous-version=2.1.2&new-version=2.1.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 846049071ce1..ded71768761f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12971,9 +12971,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", "dev": true, "license": "MIT", "optional": true, @@ -24763,9 +24763,9 @@ "dev": true }, "tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", "dev": true, "optional": true, "requires": { From 26fe4439e6d142296bcb88d20d8573ec1939cc96 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:26:39 -0700 Subject: [PATCH 0956/1136] Bump to 2025.8.0 (#25148) --- build/azure-pipeline.stable.yml | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index cae56854118e..76eb7f62b4f7 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -128,7 +128,7 @@ extends: project: 'Monaco' definition: 593 buildVersionToDownload: 'latestFromBranch' - branchName: 'refs/heads/release/2025.4' + branchName: 'refs/heads/release/2025.8' targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' artifactName: 'bin-$(buildTarget)' itemPattern: | diff --git a/package-lock.json b/package-lock.json index ded71768761f..240c6960b471 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.7.0-dev", + "version": "2025.8.0-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.7.0-dev", + "version": "2025.8.0-rc", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index cd465252aaad..bab5b690e5a8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.7.0-dev", + "version": "2025.8.0-rc", "featureFlags": { "usingNewInterpreterStorage": true }, From 7d8ac2fcf845c55f6092f596aca718d53032b45c Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 5 Jun 2025 15:57:30 -0700 Subject: [PATCH 0957/1136] bump to 2025.9 (#25149) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 240c6960b471..45bd49cb896d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.8.0-rc", + "version": "2025.9.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.8.0-rc", + "version": "2025.9.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index bab5b690e5a8..0414e9df1b04 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.8.0-rc", + "version": "2025.9.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 5070566155d8a6f71158db26a803d3155d2ab046 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 18 Jun 2025 06:27:57 +1000 Subject: [PATCH 0958/1136] Hide progress messages when creating env or installing packages (#25174) https://github.com/microsoft/vscode-python/issues/25143 Hides progress when tools use components from Python extension --- src/client/chat/createVirtualEnvTool.ts | 2 + src/client/chat/installPackagesTool.ts | 5 +- .../common/installer/moduleInstaller.ts | 2 +- src/client/common/installer/types.ts | 1 + .../provider/condaCreationProvider.ts | 83 +++++++++++-------- .../creation/provider/hideEnvCreation.ts | 21 +++++ .../creation/provider/venvCreationProvider.ts | 58 +++++++------ 7 files changed, 111 insertions(+), 61 deletions(-) create mode 100644 src/client/pythonEnvironments/creation/provider/hideEnvCreation.ts diff --git a/src/client/chat/createVirtualEnvTool.ts b/src/client/chat/createVirtualEnvTool.ts index 42fa6ccfe45b..9bbcc466fe28 100644 --- a/src/client/chat/createVirtualEnvTool.ts +++ b/src/client/chat/createVirtualEnvTool.ts @@ -43,6 +43,7 @@ import { traceError, traceVerbose, traceWarn } from '../logging'; import { StopWatch } from '../common/utils/stopWatch'; import { useEnvExtension } from '../envExt/api.internal'; import { PythonEnvironment } from '../envExt/types'; +import { hideEnvCreation } from '../pythonEnvironments/creation/provider/hideEnvCreation'; interface ICreateVirtualEnvToolParams extends IResourceReference { packageList?: string[]; // Added only becausewe have ability to create a virtual env with list of packages same tool within the in Python Env extension. @@ -86,6 +87,7 @@ export class CreateVirtualEnvTool implements LanguageModelTool(IInterpreterPathService); const disposables = new DisposableStore(); try { + disposables.add(hideEnvCreation()); const interpreterChanged = new Promise((resolve) => { disposables.add(interpreterPathService.onDidChange(() => resolve())); }); diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts index bd89dd8c26c7..e359fce110db 100644 --- a/src/client/chat/installPackagesTool.ts +++ b/src/client/chat/installPackagesTool.ts @@ -93,7 +93,10 @@ export class InstallPackagesTool implements LanguageModelTool(IApplicationShell); const options: ProgressOptions = { location: ProgressLocation.Notification, diff --git a/src/client/common/installer/types.ts b/src/client/common/installer/types.ts index 679b8b0ea668..a85017ff0092 100644 --- a/src/client/common/installer/types.ts +++ b/src/client/common/installer/types.ts @@ -83,4 +83,5 @@ export enum ModuleInstallFlags { export type InstallOptions = { installAsProcess?: boolean; + hideProgress?: boolean; }; diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index d923179fac73..a7e4e9a21cd1 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { CancellationToken, ProgressLocation, WorkspaceFolder } from 'vscode'; +import { CancellationToken, CancellationTokenSource, ProgressLocation, WorkspaceFolder } from 'vscode'; import * as path from 'path'; import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; import { traceError, traceInfo, traceLog } from '../../../logging'; @@ -35,6 +35,8 @@ import { CreateEnvironmentResult, CreateEnvironmentProvider, } from '../proposed.createEnvApis'; +import { shouldDisplayEnvCreationProgress } from './hideEnvCreation'; +import { noop } from '../../../common/utils/misc'; function generateCommandArgs(version?: string, options?: CreateEnvironmentOptions): string[] { let addGitIgnore = true; @@ -261,6 +263,50 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise { + progress.report({ + message: CreateEnv.statusStarting, + }); + + let envPath: string | undefined; + try { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'conda', + pythonVersion: version, + }); + if (workspace) { + envPath = await createCondaEnv( + workspace, + getExecutableCommand(conda), + generateCommandArgs(version, options), + progress, + token, + ); + + if (envPath) { + return { path: envPath, workspaceFolder: workspace }; + } + + throw new Error('Failed to create conda environment. See Output > Python for more info.'); + } else { + throw new Error('A workspace is needed to create conda environment'); + } + } catch (ex) { + traceError(ex); + showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); + return { error: ex as Error }; + } + }; + + if (!shouldDisplayEnvCreationProgress()) { + const token = new CancellationTokenSource(); + try { + return await createEnvInternal({ report: noop }, token.token); + } finally { + token.dispose(); + } + } + return withProgress( { location: ProgressLocation.Notification, @@ -270,40 +316,7 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise => { - progress.report({ - message: CreateEnv.statusStarting, - }); - - let envPath: string | undefined; - try { - sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { - environmentType: 'conda', - pythonVersion: version, - }); - if (workspace) { - envPath = await createCondaEnv( - workspace, - getExecutableCommand(conda), - generateCommandArgs(version, options), - progress, - token, - ); - - if (envPath) { - return { path: envPath, workspaceFolder: workspace }; - } - - throw new Error('Failed to create conda environment. See Output > Python for more info.'); - } else { - throw new Error('A workspace is needed to create conda environment'); - } - } catch (ex) { - traceError(ex); - showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); - return { error: ex as Error }; - } - }, + ): Promise => createEnvInternal(progress, token), ); } diff --git a/src/client/pythonEnvironments/creation/provider/hideEnvCreation.ts b/src/client/pythonEnvironments/creation/provider/hideEnvCreation.ts new file mode 100644 index 000000000000..5c29a8d7128d --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/hideEnvCreation.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable } from 'vscode'; + +const envCreationTracker: Disposable[] = []; + +export function hideEnvCreation(): Disposable { + const disposable = new Disposable(() => { + const index = envCreationTracker.indexOf(disposable); + if (index > -1) { + envCreationTracker.splice(index, 1); + } + }); + envCreationTracker.push(disposable); + return disposable; +} + +export function shouldDisplayEnvCreationProgress(): boolean { + return envCreationTracker.length === 0; +} diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index 9f5d746d55ae..c5c82b85357f 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import * as os from 'os'; -import { CancellationToken, ProgressLocation, WorkspaceFolder } from 'vscode'; +import { CancellationToken, CancellationTokenSource, ProgressLocation, WorkspaceFolder } from 'vscode'; import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; import { createVenvScript } from '../../../common/process/internal/scripts'; import { execObservable } from '../../../common/process/rawProcessApis'; @@ -31,6 +31,8 @@ import { CreateEnvironmentOptions, CreateEnvironmentResult, } from '../proposed.createEnvApis'; +import { shouldDisplayEnvCreationProgress } from './hideEnvCreation'; +import { noop } from '../../../common/utils/misc'; interface IVenvCommandArgs { argv: string[]; @@ -333,6 +335,36 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { } const args = generateCommandArgs(installInfo, addGitIgnore); + const createEnvInternal = async (progress: CreateEnvironmentProgress, token: CancellationToken) => { + progress.report({ + message: CreateEnv.statusStarting, + }); + + let envPath: string | undefined; + try { + if (interpreter && workspace) { + envPath = await createVenv(workspace, interpreter, args, progress, token); + if (envPath) { + return { path: envPath, workspaceFolder: workspace }; + } + throw new Error('Failed to create virtual environment. See Output > Python for more info.'); + } + throw new Error('Failed to create virtual environment. Either interpreter or workspace is undefined.'); + } catch (ex) { + traceError(ex); + showErrorMessageWithLogs(CreateEnv.Venv.errorCreatingEnvironment); + return { error: ex as Error }; + } + }; + + if (!shouldDisplayEnvCreationProgress()) { + const token = new CancellationTokenSource(); + try { + return await createEnvInternal({ report: noop }, token.token); + } finally { + token.dispose(); + } + } return withProgress( { @@ -343,29 +375,7 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { async ( progress: CreateEnvironmentProgress, token: CancellationToken, - ): Promise => { - progress.report({ - message: CreateEnv.statusStarting, - }); - - let envPath: string | undefined; - try { - if (interpreter && workspace) { - envPath = await createVenv(workspace, interpreter, args, progress, token); - if (envPath) { - return { path: envPath, workspaceFolder: workspace }; - } - throw new Error('Failed to create virtual environment. See Output > Python for more info.'); - } - throw new Error( - 'Failed to create virtual environment. Either interpreter or workspace is undefined.', - ); - } catch (ex) { - traceError(ex); - showErrorMessageWithLogs(CreateEnv.Venv.errorCreatingEnvironment); - return { error: ex as Error }; - } - }, + ): Promise => createEnvInternal(progress, token), ); } From 2faa16417084e4b3f9a448127f361dcb336d3ce6 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 19 Jun 2025 10:21:06 -0700 Subject: [PATCH 0959/1136] fix linting (#25194) --- python_files/printEnvVariablesToFile.py | 2 +- python_files/unittestadapter/execution.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python_files/printEnvVariablesToFile.py b/python_files/printEnvVariablesToFile.py index eae01b3d073c..f6013a8c24cf 100644 --- a/python_files/printEnvVariablesToFile.py +++ b/python_files/printEnvVariablesToFile.py @@ -12,5 +12,5 @@ raise ValueError("Missing output file argument") with open(output_file, "w") as outfile: # noqa: PTH123 - for key, val in os.environ.items(): + for key, val in os.environ.items(): # noqa: FURB122 outfile.write(f"{key}={val}\n") diff --git a/python_files/unittestadapter/execution.py b/python_files/unittestadapter/execution.py index 176703c20ce0..a0a48c61470a 100644 --- a/python_files/unittestadapter/execution.py +++ b/python_files/unittestadapter/execution.py @@ -189,8 +189,8 @@ def run_tests( pattern: str, top_level_dir: Optional[str], verbosity: int, - failfast: Optional[bool], - locals_: Optional[bool] = None, + failfast: Optional[bool], # noqa: FBT001 + locals_: Optional[bool] = None, # noqa: FBT001 ) -> ExecutionPayloadDict: cwd = os.path.abspath(start_dir) # noqa: PTH100 if "/" in start_dir: # is a subdir From df13df0f99d7fd7dc10c7f8f44e2a6696ccfe795 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 23 Jun 2025 13:11:13 -0700 Subject: [PATCH 0960/1136] add python.useEnvironmentsExtension (#25204) step 1 of insiders envs ext rollout. In the activation function, set `python.config.useEnvironmentsExtension:true` (so for everyone with envs extension installed). Result: all users with extension envs extension installed (and using it while we have this bake) will get this in their user settings and thus when experimentation starts they will override any control/treatment group. --- package.json | 10 ++++++++++ package.nls.json | 1 + 2 files changed, 11 insertions(+) diff --git a/package.json b/package.json index 0414e9df1b04..47cb84c49fbd 100644 --- a/package.json +++ b/package.json @@ -440,6 +440,16 @@ "scope": "resource", "type": "string" }, + "python.useEnvironmentsExtension": { + "default": false, + "description": "%python.useEnvironmentsExtension.description%", + "scope": "machine-overridable", + "type": "boolean", + "tags": [ + "onExP", + "preview" + ] + }, "python.experiments.enabled": { "default": true, "description": "%python.experiments.enabled.description%", diff --git a/package.nls.json b/package.nls.json index b6ba75b332f2..560e78de05a5 100644 --- a/package.nls.json +++ b/package.nls.json @@ -38,6 +38,7 @@ "python.debugger.deprecatedMessage": "This configuration will be deprecated soon. Please replace `python` with `debugpy` to use the new Python Debugger extension.", "python.defaultInterpreterPath.description": "Path to default Python to use when extension loads up for the first time, no longer used once an interpreter is selected for the workspace. See [here](https://aka.ms/AAfekmf) to understand when this is used", "python.envFile.description": "Absolute path to a file containing environment variable definitions.", + "python.useEnvironmentsExtension.description": "Enables the Python Environments extension.", "python.experiments.enabled.description": "Enables A/B tests experiments in the Python extension. If enabled, you may get included in proposed enhancements and/or features.", "python.experiments.optInto.description": "List of experiments to opt into. If empty, user is assigned the default experiment groups. See [here](https://github.com/microsoft/vscode-python/wiki/AB-Experiments) for more details.", "python.experiments.optOutFrom.description": "List of experiments to opt out of. If empty, user is assigned the default experiment groups. See [here](https://github.com/microsoft/vscode-python/wiki/AB-Experiments) for more details.", From cde514f6508f848022740703bf4b56706ae8665f Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Mon, 23 Jun 2025 16:23:50 -0700 Subject: [PATCH 0961/1136] Update node to 20.18.1 (#25205) We merged https://github.com/microsoft/vscode-python/pull/25168 into release branch. Need to update main branch accodingly. --- .github/workflows/build.yml | 2 +- .github/workflows/pr-check.yml | 2 +- build/azure-pipeline.pre-release.yml | 2 +- build/azure-pipeline.stable.yml | 2 +- build/azure-pipelines/pipeline.yml | 6 +++--- pythonExtensionApi/package-lock.json | 2 +- pythonExtensionApi/package.json | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 78cbd9dfd0e4..ca75f6ef7727 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: permissions: {} env: - NODE_VERSION: 20.18.0 + NODE_VERSION: 20.18.1 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 # Force a path with spaces and to test extension works in these scenarios # Unicode characters are causing 2.7 failures so skip that for now. diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 81c427a31c7b..65e80e1f6280 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -10,7 +10,7 @@ on: permissions: {} env: - NODE_VERSION: 20.18.0 + NODE_VERSION: 20.18.1 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). Also enables a reporter which exits the process running the tests if it haven't already. ARTIFACT_NAME_VSIX: ms-python-insiders-vsix diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index 6c6600365529..ab087673f1e7 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -66,7 +66,7 @@ extends: buildSteps: - task: NodeTool@0 inputs: - versionSpec: '20.18.0' + versionSpec: '20.18.1' displayName: Select Node version - task: UsePythonVersion@0 diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 76eb7f62b4f7..11de6b0806d4 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -60,7 +60,7 @@ extends: buildSteps: - task: NodeTool@0 inputs: - versionSpec: '20.18.0' + versionSpec: '20.18.1' displayName: Select Node version - task: UsePythonVersion@0 diff --git a/build/azure-pipelines/pipeline.yml b/build/azure-pipelines/pipeline.yml index 7b611de683a8..ebb8a141d9d3 100644 --- a/build/azure-pipelines/pipeline.yml +++ b/build/azure-pipelines/pipeline.yml @@ -37,13 +37,13 @@ extends: testPlatforms: - name: Linux nodeVersions: - - 20.18.0 + - 20.18.1 - name: MacOS nodeVersions: - - 20.18.0 + - 20.18.1 - name: Windows nodeVersions: - - 20.18.0 + - 20.18.1 testSteps: - template: /build/azure-pipelines/templates/test-steps.yml@self parameters: diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index ad49ed836900..ec175f1aaa5d 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -14,7 +14,7 @@ "typescript": "~5.2" }, "engines": { - "node": ">=20.18.0", + "node": ">=20.18.1", "vscode": "^1.93.0" } }, diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index d7d976642bb3..9a27e5a09b0a 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -13,7 +13,7 @@ "main": "./out/main.js", "types": "./out/main.d.ts", "engines": { - "node": ">=20.18.0", + "node": ">=20.18.1", "vscode": "^1.93.0" }, "license": "MIT", From e5e52411d72a258fd55bcce2285680aa190006ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 20:06:34 +0000 Subject: [PATCH 0962/1136] Bump pbkdf2 from 3.0.17 to 3.1.3 (#25207) Bumps [pbkdf2](https://github.com/crypto-browserify/pbkdf2) from 3.0.17 to 3.1.3.
Changelog

Sourced from pbkdf2's changelog.

v3.1.3 - 2025-06-20

Commits

  • Only apps should have lockfiles 8b06730
  • [lint] fix whitespace 9a76e2f
  • [lint] fix parens/curlies/semis/etc 6fd84bf
  • [meta] add auto-changelog 796c38d
  • [Tests] fix tests in node 17 3661fb0
  • Revert "[Tests] fix tests in node < 3" 7431b57
  • [Tests] fix tests in node < 3 eb9f97a
  • [Fix] ensure unknown algorithms throw + known ones match node 26d4fd3
  • [Tests] add GHA, always run nyc 513906a
  • [lint] fix a few more rules ab04da8
  • [lint] switch to eslint 89694cf
  • [Tests] add coverage d0d534b
  • [Refactor] use to-buffer e3102a8
  • [readme] improve badges fca0c9d
  • [Tests] remove unused travis file a2c7d93
  • [meta] switch from files to npmignore 7f31fbc
  • [Tests] use .nycrc 8d628e8
  • [Refactor] minor tweaks fc61005
  • [Deps] update create-hmac, safe-buffer, sha.js ae2a7d0
  • [Fix] pin create-hash, ripemd160 due to breaking changes e079968
  • [Tests] fix tests in node 3 45fbcf3
  • [meta] skip publishing benchmarks 19ea57b
  • [Dev Deps] add missing peer dep 645e252

v3.1.2 - 2021-04-09

Commits

v3.1.1 - 2020-06-04

Commits

v3.1.0 - 2020-06-03

Merged

Commits

... (truncated)

Commits
Maintainer changes

This version was pushed to npm by ljharb, a new releaser for pbkdf2 since your current version.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pbkdf2&package-manager=npm_and_yarn&previous-version=3.0.17&new-version=3.1.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 532 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 410 insertions(+), 122 deletions(-) diff --git a/package-lock.json b/package-lock.json index 45bd49cb896d..e07f5d775c3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3798,16 +3798,47 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -5264,6 +5295,21 @@ "semver": "bin/semver" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -5482,13 +5528,11 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -5509,10 +5553,11 @@ "dev": true }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -6819,12 +6864,19 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/for-in": { @@ -7078,16 +7130,22 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -7117,6 +7175,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -7357,12 +7429,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7880,10 +7953,11 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8697,12 +8771,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -9691,6 +9766,16 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -11392,21 +11477,78 @@ } }, "node_modules/pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz", + "integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==", "dev": true, + "license": "MIT", "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "create-hash": "~1.1.3", + "create-hmac": "^1.1.7", + "ripemd160": "=2.0.1", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.11", + "to-buffer": "^1.2.0" }, "engines": { "node": ">=0.12" } }, + "node_modules/pbkdf2/node_modules/create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "sha.js": "^2.4.0" + } + }, + "node_modules/pbkdf2/node_modules/hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1" + } + }, + "node_modules/pbkdf2/node_modules/ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^2.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/pbkdf2/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -13245,12 +13387,48 @@ "dev": true }, "node_modules/to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, + "node_modules/to-buffer/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -13628,14 +13806,15 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -14547,15 +14726,18 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -17854,16 +18036,35 @@ } }, "call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "requires": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" } }, "callsites": { @@ -18965,6 +19166,17 @@ } } }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -19157,13 +19369,10 @@ } }, "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "requires": { - "get-intrinsic": "^1.2.4" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true }, "es-errors": { "version": "1.3.0", @@ -19178,9 +19387,9 @@ "dev": true }, "es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "requires": { "es-errors": "^1.3.0" @@ -20146,12 +20355,12 @@ } }, "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "requires": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" } }, "for-in": { @@ -20344,16 +20553,21 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" } }, "get-package-type": { @@ -20368,6 +20582,16 @@ "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", "dev": true }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -20560,13 +20784,10 @@ } }, "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true }, "got": { "version": "8.3.2", @@ -20958,9 +21179,9 @@ "dev": true }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true }, "has-to-string-tag-x": { @@ -21514,12 +21735,12 @@ } }, "is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "requires": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" } }, "is-typedarray": { @@ -22307,6 +22528,12 @@ } } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true + }, "md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -23586,16 +23813,56 @@ "dev": true }, "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz", + "integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==", "dev": true, "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "create-hash": "~1.1.3", + "create-hmac": "^1.1.7", + "ripemd160": "=2.0.1", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.11", + "to-buffer": "^1.2.0" + }, + "dependencies": { + "create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "sha.js": "^2.4.0" + } + }, + "hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==", + "dev": true, + "requires": { + "inherits": "^2.0.1" + } + }, + "ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==", + "dev": true, + "requires": { + "hash-base": "^2.0.0", + "inherits": "^2.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "pend": { @@ -24983,10 +25250,29 @@ "dev": true }, "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", - "dev": true + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "dev": true, + "requires": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } }, "to-regex-range": { "version": "5.0.1", @@ -25261,14 +25547,14 @@ "dev": true }, "typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "requires": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" } }, "typed-array-byte-length": { @@ -25957,15 +26243,17 @@ } }, "which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, "requires": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, From 01d872b0a709fc3000546da68088f4248b443ee9 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 26 Jun 2025 09:22:04 -0700 Subject: [PATCH 0963/1136] Disable PyREPL (#25216) Resolves: https://github.com/microsoft/vscode-python/issues/25164 We can enable shell integration again before https://github.com/python/cpython/issues/126131 Since we are using `PYTHON_BASIC_REPL` there is no need to use bracketed paste mode to avoid indentation error prevalent on the PyREPL from cpython >= 3.13 When there is upstream fix in the future, we should start using bracketed paste mode again, and won't have to inject `PYTHON_BASIC_REPL` --- src/client/extensionActivation.ts | 3 ++- src/client/terminals/codeExecution/helper.ts | 11 +---------- src/client/terminals/pythonStartup.ts | 5 +++++ src/test/terminals/codeExecution/helper.test.ts | 4 ++-- .../terminals/shellIntegration/pythonStartup.test.ts | 10 +++++++++- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 8330d5010f7a..8fae9d5131ff 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -53,7 +53,7 @@ import { DebuggerTypeName } from './debugger/constants'; import { StopWatch } from './common/utils/stopWatch'; import { registerReplCommands, registerReplExecuteOnEnter, registerStartNativeReplCommand } from './repl/replCommands'; import { registerTriggerForTerminalREPL } from './terminals/codeExecution/terminalReplWatcher'; -import { registerPythonStartup } from './terminals/pythonStartup'; +import { registerBasicRepl, registerPythonStartup } from './terminals/pythonStartup'; import { registerPixiFeatures } from './pythonEnvironments/common/environmentManagers/pixi'; import { registerCustomTerminalLinkProvider } from './terminals/pythonStartupLinkProvider'; import { registerEnvExtFeatures } from './envExt/api.internal'; @@ -184,6 +184,7 @@ async function activateLegacy(ext: ExtensionState, startupStopWatch: StopWatch): serviceManager.get(ITerminalAutoActivation).register(); await registerPythonStartup(ext.context); + await registerBasicRepl(ext.context); serviceManager.get(ICodeExecutionManager).registerCommands(); diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index 31a626207bc4..26ebe35aae43 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -55,7 +55,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { public async normalizeLines( code: string, - replType: ReplType, + _replType: ReplType, wholeFileContent?: string, resource?: Uri, ): Promise { @@ -124,15 +124,6 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const lineOffset = object.nextBlockLineno - activeEditor!.selection.start.line - 1; await this.moveToNextBlock(lineOffset, activeEditor); } - // For new _pyrepl for Python3.13 and above, we need to send code via bracketed paste mode. - if (object.attach_bracket_paste && replType === ReplType.terminal) { - let trimmedNormalized = object.normalized.replace(/\n$/, ''); - if (trimmedNormalized.endsWith(':\n')) { - // In case where statement is unfinished via :, truncate so auto-indentation lands nicely. - trimmedNormalized = trimmedNormalized.replace(/\n$/, ''); - } - return `\u001b[200~${trimmedNormalized}\u001b[201~`; - } return parse(object.normalized); } catch (ex) { diff --git a/src/client/terminals/pythonStartup.ts b/src/client/terminals/pythonStartup.ts index 28878713a8db..1a2576dce772 100644 --- a/src/client/terminals/pythonStartup.ts +++ b/src/client/terminals/pythonStartup.ts @@ -36,3 +36,8 @@ export async function registerPythonStartup(context: ExtensionContext): Promise< }), ); } + +export async function registerBasicRepl(context: ExtensionContext): Promise { + // TODO: Configurable by setting + context.environmentVariableCollection.replace('PYTHON_BASIC_REPL', '1'); +} diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts index a43c5f8746ed..b7e0d1617884 100644 --- a/src/test/terminals/codeExecution/helper.test.ts +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -137,7 +137,7 @@ suite('Terminal - Code Execution Helper', async () => { editor.setup((e) => e.document).returns(() => document.object); }); - test('normalizeLines should handle attach_bracket_paste correctly', async () => { + test('normalizeLines with BASIC_REPL does not attach bracketed paste mode', async () => { configurationService .setup((c) => c.getSettings(TypeMoq.It.isAny())) .returns({ @@ -163,7 +163,7 @@ suite('Terminal - Code Execution Helper', async () => { const result = await helper.normalizeLines('print("Looks like you are on 3.13")', ReplType.terminal); - expect(result).to.equal(`\u001b[200~print("Looks like you are on 3.13")\u001b[201~`); + expect(result).to.equal(`print("Looks like you are on 3.13")`); jsonParseStub.restore(); }); diff --git a/src/test/terminals/shellIntegration/pythonStartup.test.ts b/src/test/terminals/shellIntegration/pythonStartup.test.ts index 45535d0ceecc..3c755adf0d9b 100644 --- a/src/test/terminals/shellIntegration/pythonStartup.test.ts +++ b/src/test/terminals/shellIntegration/pythonStartup.test.ts @@ -16,7 +16,7 @@ import { } from 'vscode'; import { assert } from 'chai'; import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; -import { registerPythonStartup } from '../../../client/terminals/pythonStartup'; +import { registerBasicRepl, registerPythonStartup } from '../../../client/terminals/pythonStartup'; import { IExtensionContext } from '../../../client/common/types'; import * as pythonStartupLinkProvider from '../../../client/terminals/pythonStartupLinkProvider'; import { CustomTerminalLinkProvider } from '../../../client/terminals/pythonStartupLinkProvider'; @@ -135,6 +135,14 @@ suite('Terminal - Shell Integration with PYTHONSTARTUP', () => { globalEnvironmentVariableCollection.verify((c) => c.delete('PYTHONSTARTUP'), TypeMoq.Times.once()); }); + test('PYTHON_BASIC_REPL is set when registerBasicRepl is called', async () => { + await registerBasicRepl(context.object); + globalEnvironmentVariableCollection.verify( + (c) => c.replace('PYTHON_BASIC_REPL', '1', TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); + }); + test('Ensure registering terminal link calls registerTerminalLinkProvider', async () => { const registerTerminalLinkProviderStub = sinon.stub( pythonStartupLinkProvider, From 180ba38f0605847e2c7e722e631bf164f235562b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 27 Jun 2025 11:01:50 +1000 Subject: [PATCH 0964/1136] Capture invocation and failure reasons for tools (#25220) --- src/client/chat/baseTool.ts | 75 +++++++++++++++++++++++ src/client/chat/configurePythonEnvTool.ts | 17 +++-- src/client/chat/createVirtualEnvTool.ts | 20 +++--- src/client/chat/getExecutableTool.ts | 24 +++----- src/client/chat/getPythonEnvTool.ts | 38 +++++------- src/client/chat/installPackagesTool.ts | 40 +++++++----- src/client/chat/selectEnvTool.ts | 19 +++--- src/client/chat/utils.ts | 22 ++++--- src/client/common/errors/errorUtils.ts | 11 +--- src/client/telemetry/constants.ts | 1 + src/client/telemetry/index.ts | 26 ++++++++ 11 files changed, 195 insertions(+), 98 deletions(-) create mode 100644 src/client/chat/baseTool.ts diff --git a/src/client/chat/baseTool.ts b/src/client/chat/baseTool.ts new file mode 100644 index 000000000000..2eedbbe226e3 --- /dev/null +++ b/src/client/chat/baseTool.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, + workspace, +} from 'vscode'; +import { IResourceReference, isCancellationError, resolveFilePath } from './utils'; +import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; + +export abstract class BaseTool implements LanguageModelTool { + constructor(private readonly toolName: string) {} + + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + if (!workspace.isTrusted) { + return new LanguageModelToolResult([ + new LanguageModelTextPart('Cannot use this tool in an untrusted workspace.'), + ]); + } + let error: Error | undefined; + const resource = resolveFilePath(options.input.resourcePath); + try { + return await this.invokeImpl(options, resource, token); + } catch (ex) { + error = ex as any; + throw ex; + } finally { + const isCancelled = token.isCancellationRequested || (error ? isCancellationError(error) : false); + const failed = !!error || isCancelled; + const failureCategory = isCancelled + ? 'cancelled' + : error + ? error instanceof ErrorWithTelemetrySafeReason + ? error.telemetrySafeReason + : 'error' + : undefined; + sendTelemetryEvent(EventName.INVOKE_TOOL, undefined, { + toolName: this.toolName, + failed, + failureCategory, + }); + } + } + protected abstract invokeImpl( + options: LanguageModelToolInvocationOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise; + + async prepareInvocation( + options: LanguageModelToolInvocationPrepareOptions, + token: CancellationToken, + ): Promise { + const resource = resolveFilePath(options.input.resourcePath); + return this.prepareInvocationImpl(options, resource, token); + } + + protected abstract prepareInvocationImpl( + options: LanguageModelToolInvocationPrepareOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise; +} diff --git a/src/client/chat/configurePythonEnvTool.ts b/src/client/chat/configurePythonEnvTool.ts index e80347914a4d..0634b9c9ac34 100644 --- a/src/client/chat/configurePythonEnvTool.ts +++ b/src/client/chat/configurePythonEnvTool.ts @@ -19,18 +19,18 @@ import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/termin import { getEnvDetailsForResponse, getToolResponseIfNotebook, - getUntrustedWorkspaceResponse, IResourceReference, isCancellationError, raceCancellationError, } from './utils'; -import { resolveFilePath } from './utils'; import { ITerminalHelper } from '../common/terminal/types'; import { IRecommendedEnvironmentService } from '../interpreter/configuration/types'; import { CreateVirtualEnvTool } from './createVirtualEnvTool'; import { ISelectPythonEnvToolArguments, SelectPythonEnvTool } from './selectEnvTool'; +import { BaseTool } from './baseTool'; -export class ConfigurePythonEnvTool implements LanguageModelTool { +export class ConfigurePythonEnvTool extends BaseTool + implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; private readonly terminalHelper: ITerminalHelper; private readonly recommendedEnvService: IRecommendedEnvironmentService; @@ -40,6 +40,7 @@ export class ConfigurePythonEnvTool implements LanguageModelTool( ICodeExecutionService, 'standard', @@ -50,14 +51,11 @@ export class ConfigurePythonEnvTool implements LanguageModelTool, + resource: Uri | undefined, token: CancellationToken, ): Promise { - if (!workspace.isTrusted) { - return getUntrustedWorkspaceResponse(); - } - const resource = resolveFilePath(options.input.resourcePath); const notebookResponse = getToolResponseIfNotebook(resource); if (notebookResponse) { return notebookResponse; @@ -101,8 +99,9 @@ export class ConfigurePythonEnvTool implements LanguageModelTool, + _resource: Uri | undefined, _token: CancellationToken, ): Promise { return { diff --git a/src/client/chat/createVirtualEnvTool.ts b/src/client/chat/createVirtualEnvTool.ts index 9bbcc466fe28..56760d2b4bef 100644 --- a/src/client/chat/createVirtualEnvTool.ts +++ b/src/client/chat/createVirtualEnvTool.ts @@ -22,12 +22,10 @@ import { doesWorkspaceHaveVenvOrCondaEnv, getDisplayVersion, getEnvDetailsForResponse, - getUntrustedWorkspaceResponse, IResourceReference, isCancellationError, raceCancellationError, } from './utils'; -import { resolveFilePath } from './utils'; import { ITerminalHelper } from '../common/terminal/types'; import { raceTimeout, sleep } from '../common/utils/async'; import { IInterpreterPathService } from '../common/types'; @@ -44,12 +42,14 @@ import { StopWatch } from '../common/utils/stopWatch'; import { useEnvExtension } from '../envExt/api.internal'; import { PythonEnvironment } from '../envExt/types'; import { hideEnvCreation } from '../pythonEnvironments/creation/provider/hideEnvCreation'; +import { BaseTool } from './baseTool'; interface ICreateVirtualEnvToolParams extends IResourceReference { packageList?: string[]; // Added only becausewe have ability to create a virtual env with list of packages same tool within the in Python Env extension. } -export class CreateVirtualEnvTool implements LanguageModelTool { +export class CreateVirtualEnvTool extends BaseTool + implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; private readonly terminalHelper: ITerminalHelper; private readonly recommendedEnvService: IRecommendedEnvironmentService; @@ -60,6 +60,7 @@ export class CreateVirtualEnvTool implements LanguageModelTool( ICodeExecutionService, 'standard', @@ -70,14 +71,11 @@ export class CreateVirtualEnvTool implements LanguageModelTool, + resource: Uri | undefined, token: CancellationToken, ): Promise { - if (!workspace.isTrusted) { - return getUntrustedWorkspaceResponse(); - } - const resource = resolveFilePath(options.input.resourcePath); let info = await this.getPreferredEnvForCreation(resource); if (!info) { traceWarn(`Called ${CreateVirtualEnvTool.toolName} tool not invoked, no preferred environment found.`); @@ -170,11 +168,11 @@ export class CreateVirtualEnvTool implements LanguageModelTool, + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions, + resource: Uri | undefined, token: CancellationToken, ): Promise { - const resource = resolveFilePath(options.input.resourcePath); const info = await raceCancellationError(this.getPreferredEnvForCreation(resource), token); if (!info) { return {}; diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts index 8c1bed632384..746a540d14f8 100644 --- a/src/client/chat/getExecutableTool.ts +++ b/src/client/chat/getExecutableTool.ts @@ -10,7 +10,7 @@ import { LanguageModelToolInvocationPrepareOptions, LanguageModelToolResult, PreparedToolInvocation, - workspace, + Uri, } from 'vscode'; import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; @@ -20,15 +20,14 @@ import { getEnvDisplayName, getEnvironmentDetails, getToolResponseIfNotebook, - getUntrustedWorkspaceResponse, IResourceReference, raceCancellationError, } from './utils'; -import { resolveFilePath } from './utils'; import { ITerminalHelper } from '../common/terminal/types'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { BaseTool } from './baseTool'; -export class GetExecutableTool implements LanguageModelTool { +export class GetExecutableTool extends BaseTool implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; private readonly terminalHelper: ITerminalHelper; public static readonly toolName = 'get_python_executable_details'; @@ -37,21 +36,18 @@ export class GetExecutableTool implements LanguageModelTool private readonly serviceContainer: IServiceContainer, private readonly discovery: IDiscoveryAPI, ) { + super(GetExecutableTool.toolName); this.terminalExecutionService = this.serviceContainer.get( ICodeExecutionService, 'standard', ); this.terminalHelper = this.serviceContainer.get(ITerminalHelper); } - async invoke( - options: LanguageModelToolInvocationOptions, + async invokeImpl( + _options: LanguageModelToolInvocationOptions, + resourcePath: Uri | undefined, token: CancellationToken, ): Promise { - if (!workspace.isTrusted) { - return getUntrustedWorkspaceResponse(); - } - - const resourcePath = resolveFilePath(options.input.resourcePath); const notebookResponse = getToolResponseIfNotebook(resourcePath); if (notebookResponse) { return notebookResponse; @@ -68,11 +64,11 @@ export class GetExecutableTool implements LanguageModelTool return new LanguageModelToolResult([new LanguageModelTextPart(message)]); } - async prepareInvocation?( - options: LanguageModelToolInvocationPrepareOptions, + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions, + resourcePath: Uri | undefined, token: CancellationToken, ): Promise { - const resourcePath = resolveFilePath(options.input.resourcePath); if (getToolResponseIfNotebook(resourcePath)) { return {}; } diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index 91184d8e4ef2..ed1dd0374424 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -10,26 +10,22 @@ import { LanguageModelToolInvocationPrepareOptions, LanguageModelToolResult, PreparedToolInvocation, - workspace, + Uri, } from 'vscode'; import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; import { ICodeExecutionService } from '../terminals/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; -import { - getEnvironmentDetails, - getToolResponseIfNotebook, - getUntrustedWorkspaceResponse, - IResourceReference, - raceCancellationError, -} from './utils'; -import { resolveFilePath } from './utils'; +import { getEnvironmentDetails, getToolResponseIfNotebook, IResourceReference, raceCancellationError } from './utils'; import { getPythonPackagesResponse } from './listPackagesTool'; import { ITerminalHelper } from '../common/terminal/types'; import { getEnvExtApi, useEnvExtension } from '../envExt/api.internal'; +import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils'; +import { BaseTool } from './baseTool'; -export class GetEnvironmentInfoTool implements LanguageModelTool { +export class GetEnvironmentInfoTool extends BaseTool + implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; private readonly pythonExecFactory: IPythonExecutionFactory; private readonly processServiceFactory: IProcessServiceFactory; @@ -39,6 +35,7 @@ export class GetEnvironmentInfoTool implements LanguageModelTool( ICodeExecutionService, 'standard', @@ -48,15 +45,11 @@ export class GetEnvironmentInfoTool implements LanguageModelTool(ITerminalHelper); } - async invoke( - options: LanguageModelToolInvocationOptions, + async invokeImpl( + _options: LanguageModelToolInvocationOptions, + resourcePath: Uri | undefined, token: CancellationToken, ): Promise { - if (!workspace.isTrusted) { - return getUntrustedWorkspaceResponse(); - } - - const resourcePath = resolveFilePath(options.input.resourcePath); const notebookResponse = getToolResponseIfNotebook(resourcePath); if (notebookResponse) { return notebookResponse; @@ -66,7 +59,10 @@ export class GetEnvironmentInfoTool implements LanguageModelTool, + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions, + resourcePath: Uri | undefined, _token: CancellationToken, ): Promise { - const resourcePath = resolveFilePath(options.input.resourcePath); if (getToolResponseIfNotebook(resourcePath)) { return {}; } diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts index e359fce110db..f7795620cf13 100644 --- a/src/client/chat/installPackagesTool.ts +++ b/src/client/chat/installPackagesTool.ts @@ -10,46 +10,45 @@ import { LanguageModelToolInvocationPrepareOptions, LanguageModelToolResult, PreparedToolInvocation, - workspace, + Uri, } from 'vscode'; import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; import { getEnvDisplayName, getToolResponseIfNotebook, - getUntrustedWorkspaceResponse, IResourceReference, isCancellationError, isCondaEnv, raceCancellationError, } from './utils'; -import { resolveFilePath } from './utils'; import { IModuleInstaller } from '../common/installer/types'; import { ModuleInstallerType } from '../pythonEnvironments/info'; import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; import { getEnvExtApi, useEnvExtension } from '../envExt/api.internal'; +import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils'; +import { BaseTool } from './baseTool'; export interface IInstallPackageArgs extends IResourceReference { packageList: string[]; } -export class InstallPackagesTool implements LanguageModelTool { +export class InstallPackagesTool extends BaseTool + implements LanguageModelTool { public static readonly toolName = 'install_python_packages'; constructor( private readonly api: PythonExtension['environments'], private readonly serviceContainer: IServiceContainer, private readonly discovery: IDiscoveryAPI, - ) {} + ) { + super(InstallPackagesTool.toolName); + } - async invoke( + async invokeImpl( options: LanguageModelToolInvocationOptions, + resourcePath: Uri | undefined, token: CancellationToken, ): Promise { - if (!workspace.isTrusted) { - return getUntrustedWorkspaceResponse(); - } - - const resourcePath = resolveFilePath(options.input.resourcePath); const packageCount = options.input.packageList.length; const packagePlurality = packageCount === 1 ? 'package' : 'packages'; const notebookResponse = getToolResponseIfNotebook(resourcePath); @@ -80,17 +79,26 @@ export class InstallPackagesTool implements LanguageModelTool(IModuleInstaller); const installerType = isConda ? ModuleInstallerType.Conda : ModuleInstallerType.Pip; const installer = installers.find((i) => i.type === installerType); if (!installer) { - throw new Error(`No installer found for the environment type: ${installerType}`); + throw new ErrorWithTelemetrySafeReason( + `No installer found for the environment type: ${installerType}`, + 'noInstallerFound', + ); } if (!installer.isSupported(resourcePath)) { - throw new Error(`Installer ${installerType} not supported for the environment type: ${installerType}`); + throw new ErrorWithTelemetrySafeReason( + `Installer ${installerType} not supported for the environment type: ${installerType}`, + 'installerNotSupported', + ); } for (const packageName of options.input.packageList) { await installer.installModule(packageName, resourcePath, token, undefined, { @@ -110,11 +118,11 @@ export class InstallPackagesTool implements LanguageModelTool, + resourcePath: Uri | undefined, token: CancellationToken, ): Promise { - const resourcePath = resolveFilePath(options.input.resourcePath); const packageCount = options.input.packageList.length; if (getToolResponseIfNotebook(resourcePath)) { return {}; diff --git a/src/client/chat/selectEnvTool.ts b/src/client/chat/selectEnvTool.ts index 5f1c2f6b7c36..9eeebdfc1b56 100644 --- a/src/client/chat/selectEnvTool.ts +++ b/src/client/chat/selectEnvTool.ts @@ -24,10 +24,8 @@ import { doesWorkspaceHaveVenvOrCondaEnv, getEnvDetailsForResponse, getToolResponseIfNotebook, - getUntrustedWorkspaceResponse, IResourceReference, } from './utils'; -import { resolveFilePath } from './utils'; import { ITerminalHelper } from '../common/terminal/types'; import { raceTimeout } from '../common/utils/async'; import { Commands, Octicons } from '../common/constants'; @@ -38,12 +36,14 @@ import { Common, InterpreterQuickPickList } from '../common/utils/localize'; import { showQuickPick } from '../common/vscodeApis/windowApis'; import { DisposableStore } from '../common/utils/resourceLifecycle'; import { traceError, traceVerbose, traceWarn } from '../logging'; +import { BaseTool } from './baseTool'; export interface ISelectPythonEnvToolArguments extends IResourceReference { reason?: 'cancelled'; } -export class SelectPythonEnvTool implements LanguageModelTool { +export class SelectPythonEnvTool extends BaseTool + implements LanguageModelTool { private readonly terminalExecutionService: TerminalCodeExecutionProvider; private readonly terminalHelper: ITerminalHelper; public static readonly toolName = 'selectEnvironment'; @@ -51,6 +51,7 @@ export class SelectPythonEnvTool implements LanguageModelTool( ICodeExecutionService, 'standard', @@ -58,15 +59,11 @@ export class SelectPythonEnvTool implements LanguageModelTool(ITerminalHelper); } - async invoke( + async invokeImpl( options: LanguageModelToolInvocationOptions, + resource: Uri | undefined, token: CancellationToken, ): Promise { - if (!workspace.isTrusted) { - return getUntrustedWorkspaceResponse(); - } - - const resource = resolveFilePath(options.input.resourcePath); let selected: boolean | undefined = false; const hasVenvOrCondaEnvInWorkspaceFolder = doesWorkspaceHaveVenvOrCondaEnv(resource, this.api); if (options.input.reason === 'cancelled' || hasVenvOrCondaEnvInWorkspaceFolder) { @@ -115,11 +112,11 @@ export class SelectPythonEnvTool implements LanguageModelTool, + resource: Uri | undefined, _token: CancellationToken, ): Promise { - const resource = resolveFilePath(options.input.resourcePath); if (getToolResponseIfNotebook(resource)) { return {}; } diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts index 7c5a4dd9725f..bddd26049668 100644 --- a/src/client/chat/utils.ts +++ b/src/client/chat/utils.ts @@ -18,6 +18,7 @@ import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; import { JUPYTER_EXTENSION_ID, NotebookCellScheme } from '../common/constants'; import { dirname, join } from 'path'; import { resolveEnvironment, useEnvExtension } from '../envExt/api.internal'; +import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils'; export interface IResourceReference { resourcePath?: string; @@ -85,7 +86,10 @@ export async function getEnvironmentDetails( (await raceCancellationError(resolveEnvironment(envPath.id), token)) || (await raceCancellationError(resolveEnvironment(envPath.path), token)); if (!environment || !environment.version) { - throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resourcePath?.fsPath, + 'noEnvFound', + ); } envVersion = environment.version; try { @@ -104,7 +108,10 @@ export async function getEnvironmentDetails( } else { const environment = await raceCancellationError(api.resolveEnvironment(envPath), token); if (!environment || !environment.version) { - throw new Error('No environment found for the provided resource path: ' + resourcePath?.fsPath); + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resourcePath?.fsPath, + 'noEnvFound', + ); } envType = environment.environment?.type || 'unknown'; envVersion = environment.version.sysVersion || 'unknown'; @@ -126,10 +133,6 @@ export async function getEnvironmentDetails( return message.join('\n'); } -export function getUntrustedWorkspaceResponse() { - return new LanguageModelToolResult([new LanguageModelTextPart('Cannot use this tool in an untrusted workspace.')]); -} - export async function getTerminalCommand( environment: ResolvedEnvironment, resource: Uri | undefined, @@ -244,12 +247,15 @@ export async function getEnvDetailsForResponse( token: CancellationToken, ): Promise { if (!workspace.isTrusted) { - throw new Error('Cannot use this tool in an untrusted workspace.'); + throw new ErrorWithTelemetrySafeReason('Cannot use this tool in an untrusted workspace.', 'untrustedWorkspace'); } const envPath = api.getActiveEnvironmentPath(resource); environment = environment || (await raceCancellationError(api.resolveEnvironment(envPath), token)); if (!environment || !environment.version) { - throw new Error('No environment found for the provided resource path: ' + resource?.fsPath); + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resource?.fsPath, + 'noEnvFound', + ); } const message = await getEnvironmentDetails( resource, diff --git a/src/client/common/errors/errorUtils.ts b/src/client/common/errors/errorUtils.ts index 2c666acb105b..7867d5ccfe30 100644 --- a/src/client/common/errors/errorUtils.ts +++ b/src/client/common/errors/errorUtils.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { EOL } from 'os'; - export class ErrorUtils { public static outputHasModuleNotInstalledError(moduleName: string, content?: string): boolean { return content && @@ -14,13 +12,10 @@ export class ErrorUtils { } /** - * Wraps an error with a custom error message, retaining the call stack information. + * An error class that contains a telemetry safe reason. */ -export class WrappedError extends Error { - constructor(message: string, originalException: Error) { +export class ErrorWithTelemetrySafeReason extends Error { + constructor(message: string, public readonly telemetrySafeReason: string) { super(message); - // Retain call stack that trapped the error and rethrows this error. - // Also retain the call stack of the original error. - this.stack = `${new Error('').stack}${EOL}${EOL}${originalException.stack}`; } } diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index ecc44177338a..eff32a6e3299 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -7,6 +7,7 @@ export enum EventName { FORMAT_ON_TYPE = 'FORMAT.FORMAT_ON_TYPE', EDITOR_LOAD = 'EDITOR.LOAD', REPL = 'REPL', + INVOKE_TOOL = 'INVOKE_TOOL', CREATE_NEW_FILE_COMMAND = 'CREATE_NEW_FILE_COMMAND', SELECT_INTERPRETER = 'SELECT_INTERPRETER', SELECT_INTERPRETER_ENTER_BUTTON = 'SELECT_INTERPRETER_ENTER_BUTTON', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 6c97bd083d96..a387e45d694a 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1980,6 +1980,32 @@ export interface IEventNamePropertyMapping { */ replType: 'Terminal' | 'Native' | 'manualTerminal' | `runningScript`; }; + /** + * Telemetry event sent when invoking a Tool + */ + /* __GDPR__ + "invokeTool" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, + "toolName" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "failed": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"Whether there was a failure. Common to most of the events.", "owner": "donjayamanne" }, + "failureCategory": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"A reason that we generate (e.g. kerneldied, noipykernel, etc), more like a category of the error. Common to most of the events.", "owner": "donjayamanne" } + } + */ + [EventName.INVOKE_TOOL]: { + /** + * Tool name. + */ + toolName: string; + /** + * Whether there was a failure. + * Common to most of the events. + */ + failed: boolean; + /** + * A reason the error was thrown. + */ + failureCategory?: string; + }; /** * Telemetry event sent if and when user configure tests command. This command can be trigerred from multiple places in the extension. (Command palette, prompt etc.) */ From 6e9c76c16b9503373c6708541fc5b28e92198dab Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 30 Jun 2025 09:01:28 -0700 Subject: [PATCH 0965/1136] add exp setting for envs ext (#25195) --- gulpfile.js | 8 +++++--- package.nls.json | 12 ++++++------ src/client/envExt/api.internal.ts | 5 ++++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index f921ff7fd1b1..0b919f16572a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -98,9 +98,11 @@ async function addExtensionPackDependencies() { // extension dependencies need not be installed during development const packageJsonContents = await fsExtra.readFile('package.json', 'utf-8'); const packageJson = JSON.parse(packageJsonContents); - packageJson.extensionPack = ['ms-python.vscode-pylance', 'ms-python.debugpy'].concat( - packageJson.extensionPack ? packageJson.extensionPack : [], - ); + packageJson.extensionPack = [ + 'ms-python.vscode-pylance', + 'ms-python.debugpy', + 'ms-python.vscode-python-envs', + ].concat(packageJson.extensionPack ? packageJson.extensionPack : []); // Remove potential duplicates. packageJson.extensionPack = packageJson.extensionPack.filter( (item, index) => packageJson.extensionPack.indexOf(item) === index, diff --git a/package.nls.json b/package.nls.json index 560e78de05a5..00c96c09b19a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -38,7 +38,7 @@ "python.debugger.deprecatedMessage": "This configuration will be deprecated soon. Please replace `python` with `debugpy` to use the new Python Debugger extension.", "python.defaultInterpreterPath.description": "Path to default Python to use when extension loads up for the first time, no longer used once an interpreter is selected for the workspace. See [here](https://aka.ms/AAfekmf) to understand when this is used", "python.envFile.description": "Absolute path to a file containing environment variable definitions.", - "python.useEnvironmentsExtension.description": "Enables the Python Environments extension.", + "python.useEnvironmentsExtension.description": "Enables the Python Environments extension. Requires window reload on change.", "python.experiments.enabled.description": "Enables A/B tests experiments in the Python extension. If enabled, you may get included in proposed enhancements and/or features.", "python.experiments.optInto.description": "List of experiments to opt into. If empty, user is assigned the default experiment groups. See [here](https://github.com/microsoft/vscode-python/wiki/AB-Experiments) for more details.", "python.experiments.optOutFrom.description": "List of experiments to opt out of. If empty, user is assigned the default experiment groups. See [here](https://github.com/microsoft/vscode-python/wiki/AB-Experiments) for more details.", @@ -92,7 +92,7 @@ "python.venvPath.description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", "walkthrough.pythonWelcome.title": "Get Started with Python Development", "walkthrough.pythonWelcome.description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!", - "walkthrough.step.python.createPythonFile.title": "Create a Python file", + "walkthrough.step.python.createPythonFile.title": "Create a Python file", "walkthrough.step.python.createPythonFolder.title": "Open a Python project folder", "walkthrough.step.python.createPythonFile.description": { "message": "[Open](command:toSide:workbench.action.files.openFile) or [create](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D) a Python file - make sure to save it as \".py\".\n[Create Python File](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D)", @@ -104,7 +104,7 @@ }, "walkthrough.step.python.createPythonFolder.description": { "message": "[Open](command:workbench.action.files.openFolder) or create a project folder.\n[Open Project Folder](command:workbench.action.files.openFolder)", - "comment": [ + "comment": [ "{Locked='](command:workbench.action.files.openFolder'}", "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" @@ -134,7 +134,7 @@ "walkthrough.step.python.createEnvironment.title": "Select or create a Python environment", "walkthrough.step.python.createEnvironment.description": { "message": "Create an environment for your Python project or use [Select Python Interpreter](command:python.setInterpreter) to select an existing one.\n[Create Environment](command:python.createEnvironment)\n**Tip**: Run the ``Python: Create Environment`` command in the [Command Palette](command:workbench.action.showCommands).", - "comment": [ + "comment": [ "{Locked='](command:python.createEnvironment'}", "{Locked='](command:workbench.action.showCommands'}", "{Locked='](command:python.setInterpreter'}", @@ -146,8 +146,8 @@ "walkthrough.step.python.runAndDebug.description": "Open your Python file and click on the play button on the top right of the editor, or press F5 when on the file and select \"Python File\" to run with the debugger. \n \n[Learn more](https://code.visualstudio.com/docs/python/python-tutorial#_run-hello-world)", "walkthrough.step.python.learnMoreWithDS.title": "Keep exploring!", "walkthrough.step.python.learnMoreWithDS.description": { - "message":"🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Follow along with the Python Tutorial](https://aka.ms/AA8dqti)", - "comment":[ + "message": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Follow along with the Python Tutorial](https://aka.ms/AA8dqti)", + "comment": [ "{Locked='](command:workbench.action.showCommands'}", "{Locked='](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D'}", "Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code", diff --git a/src/client/envExt/api.internal.ts b/src/client/envExt/api.internal.ts index 552e31a0598e..c4247f63a3c5 100644 --- a/src/client/envExt/api.internal.ts +++ b/src/client/envExt/api.internal.ts @@ -14,6 +14,7 @@ import { } from './types'; import { executeCommand } from '../common/vscodeApis/commandApis'; import { IInterpreterPathService } from '../common/types'; +import { getConfiguration } from '../common/vscodeApis/workspaceApis'; export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; @@ -22,7 +23,9 @@ export function useEnvExtension(): boolean { if (_useExt !== undefined) { return _useExt; } - _useExt = !!getExtension(ENVS_EXTENSION_ID); + const inExpSetting = getConfiguration('python').get('useEnvironmentsExtension', false); + // If extension is installed and in experiment, then use it. + _useExt = !!getExtension(ENVS_EXTENSION_ID) && inExpSetting; return _useExt; } From 8c3a49fe6ec4aabef26f10feda9452aa259811bc Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 30 Jun 2025 13:10:04 -0700 Subject: [PATCH 0966/1136] edit gulpfile so envs ext is only bundled in pre-release (#25227) --- .github/actions/build-vsix/action.yml | 9 ++++++++- build/azure-pipeline.pre-release.yml | 2 +- gulpfile.js | 19 +++++++++++++------ package.json | 1 + 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index eaabe5141e8b..1922f6196ee5 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -70,8 +70,15 @@ runs: shell: bash - name: Update optional extension dependencies - run: npm run addExtensionPackDependencies + run: | + if [[ "${VSIX_NAME}" == *"insiders"* ]]; then + npm run addExtensionPackDependenciesPreRelease + else + npm run addExtensionPackDependencies + fi shell: bash + env: + VSIX_NAME: ${{ inputs.vsix_name }} - name: Build Webpack run: | diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index ab087673f1e7..82c991189a82 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -91,7 +91,7 @@ extends: - script: python ./build/update_package_file.py displayName: Update telemetry in package.json - - script: npm run addExtensionPackDependencies + - script: npm run addExtensionPackDependenciesPreRelease displayName: Update optional extension dependencies - script: npx gulp prePublishBundle diff --git a/gulpfile.js b/gulpfile.js index 0b919f16572a..3a03332c4f5c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -93,16 +93,23 @@ gulp.task('addExtensionPackDependencies', async () => { await addExtensionPackDependencies(); }); -async function addExtensionPackDependencies() { +// This task adds 'ms-python.vscode-python-envs' as required deps only for pre-release builds. +gulp.task('addExtensionPackDependenciesPreRelease', async () => { + await buildLicense(); + await addExtensionPackDependencies(true); +}); + +async function addExtensionPackDependencies(isPreRelease = false) { // Update the package.json to add extension pack dependencies at build time so that // extension dependencies need not be installed during development const packageJsonContents = await fsExtra.readFile('package.json', 'utf-8'); const packageJson = JSON.parse(packageJsonContents); - packageJson.extensionPack = [ - 'ms-python.vscode-pylance', - 'ms-python.debugpy', - 'ms-python.vscode-python-envs', - ].concat(packageJson.extensionPack ? packageJson.extensionPack : []); + let deps = ['ms-python.vscode-pylance', 'ms-python.debugpy']; + if (isPreRelease) { + deps.push('ms-python.vscode-python-envs'); + } + packageJson.extensionPack = deps.concat(packageJson.extensionPack ? packageJson.extensionPack : []); + // Remove potential duplicates. packageJson.extensionPack = packageJson.extensionPack.filter( (item, index) => packageJson.extensionPack.indexOf(item) === index, diff --git a/package.json b/package.json index 47cb84c49fbd..6d76ded9febc 100644 --- a/package.json +++ b/package.json @@ -1671,6 +1671,7 @@ "format-fix": "prettier --write 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", "clean": "gulp clean", "addExtensionPackDependencies": "gulp addExtensionPackDependencies", + "addExtensionPackDependenciesPreRelease": "gulp addExtensionPackDependenciesPreRelease", "updateBuildNumber": "gulp updateBuildNumber", "verifyBundle": "gulp verifyBundle", "webpack": "webpack" From 81ee4de55b55fbddcd9e7a201c320b21a9dc3a3a Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 30 Jun 2025 14:59:57 -0700 Subject: [PATCH 0967/1136] Update brace-expansion to version 2.0.2 (#25229) --- package-lock.json | 64 +++++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index e07f5d775c3f..8d500006eedb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2157,10 +2157,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -3472,9 +3473,10 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9918,9 +9920,10 @@ } }, "node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -10062,9 +10065,9 @@ "dev": true }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14369,9 +14372,10 @@ } }, "node_modules/vscode-languageclient/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -16778,9 +16782,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -17779,9 +17783,9 @@ "dev": true }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -22645,9 +22649,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "requires": { "balanced-match": "^1.0.0" } @@ -22723,9 +22727,9 @@ "dev": true }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -25989,9 +25993,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "requires": { "balanced-match": "^1.0.0" } From 9b89180cd498d91f5c68e013f7d796b216052d1f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 2 Jul 2025 22:13:52 +0000 Subject: [PATCH 0968/1136] fix bugs in telemetry for envs ext (#25232) - wasn't sending `EventName == "ms-python.python/execution_code"&&Properties["trigger"]=="icon` when I run a python file using the play button - when opening the editor and have a venv selected in the panel, telemetry event is sending the editor.load event with InterpreterType as System instead of venv --- src/client/startupTelemetry.ts | 17 ++++++++++++++--- .../codeExecution/codeExecutionManager.ts | 3 +++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/client/startupTelemetry.ts b/src/client/startupTelemetry.ts index 544fb24251ef..5a2c12e2dd37 100644 --- a/src/client/startupTelemetry.ts +++ b/src/client/startupTelemetry.ts @@ -16,6 +16,7 @@ import { sendTelemetryEvent } from './telemetry'; import { EventName } from './telemetry/constants'; import { EditorLoadTelemetry } from './telemetry/types'; import { IStartupDurations } from './types'; +import { useEnvExtension } from './envExt/api.internal'; export async function sendStartupTelemetry( activatedPromise: Promise, @@ -105,9 +106,19 @@ async function getActivationTelemetryProps( // finish. API getActiveInterpreter() does not block on windows registry by default as // it is slow. await interpreterService.refreshPromise; - const interpreter = await interpreterService - .getActiveInterpreter() - .catch(() => undefined); + let interpreter: PythonEnvironment | undefined; + + // include main workspace uri if using env extension + if (useEnvExtension()) { + interpreter = await interpreterService + .getActiveInterpreter(mainWorkspaceUri) + .catch(() => undefined); + } else { + interpreter = await interpreterService + .getActiveInterpreter() + .catch(() => undefined); + } + const pythonVersion = interpreter && interpreter.version ? interpreter.version.raw : undefined; const interpreterType = interpreter ? interpreter.envType : undefined; if (interpreterType === EnvironmentType.Unknown) { diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index b1dc328d137e..d5eb69efba20 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -48,6 +48,9 @@ export class CodeExecutionManager implements ICodeExecutionManager { } catch (ex) { traceError('Failed to execute file in terminal', ex); } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { + trigger: 'run-in-terminal', + }); return; } From 3699940850f13a5fcf89d1cb51b6ab71b0c67142 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 11:31:38 -0700 Subject: [PATCH 0969/1136] Bump typing-extensions from 4.13.2 to 4.14.1 (#25237) Bumps [typing-extensions](https://github.com/python/typing_extensions) from 4.13.2 to 4.14.1.
Release notes

Sourced from typing-extensions's releases.

4.14.1

Release 4.14.1 (July 4, 2025)

  • Fix usage of typing_extensions.TypedDict nested inside other types (e.g., typing.Type[typing_extensions.TypedDict]). This is not allowed by the type system but worked on older versions, so we maintain support.

4.14.0

This release adds several new features, including experimental support for inline typed dictionaries (PEP 764) and sentinels (PEP 661), and support for changes in Python 3.14. In addition, Python 3.8 is no longer supported.

Changes since 4.14.0rc1:

  • Remove __or__ and __ror__ methods from typing_extensions.Sentinel on Python versions <3.10. PEP 604 was introduced in Python 3.10, and typing_extensions does not generally attempt to backport PEP-604 methods to prior versions.
  • Further update typing_extensions.evaluate_forward_ref with changes in Python 3.14.

Changes included in 4.14.0rc1:

  • Drop support for Python 3.8 (including PyPy-3.8). Patch by Victorien Plot.
  • Do not attempt to re-export names that have been removed from typing, anticipating the removal of typing.no_type_check_decorator in Python 3.15. Patch by Jelle Zijlstra.
  • Update typing_extensions.Format, typing_extensions.evaluate_forward_ref, and typing_extensions.TypedDict to align with changes in Python 3.14. Patches by Jelle Zijlstra.
  • Fix tests for Python 3.14 and 3.15. Patches by Jelle Zijlstra.

New features:

  • Add support for inline typed dictionaries (PEP 764). Patch by Victorien Plot.
  • Add typing_extensions.Reader and typing_extensions.Writer. Patch by Sebastian Rittau.
  • Add support for sentinels (PEP 661). Patch by Victorien Plot.

4.14.0rc1

Major changes:

  • Drop support for Python 3.8 (including PyPy-3.8). Patch by Victorien Plot.
  • Do not attempt to re-export names that have been removed from typing, anticipating the removal of typing.no_type_check_decorator in Python 3.15. Patch by Jelle Zijlstra.
  • Update typing_extensions.Format, typing_extensions.evaluate_forward_ref, and typing_extensions.TypedDict to align with changes in Python 3.14. Patches by Jelle Zijlstra.
  • Fix tests for Python 3.14 and 3.15. Patches by Jelle Zijlstra.

... (truncated)

Changelog

Sourced from typing-extensions's changelog.

Release 4.14.1 (July 4, 2025)

  • Fix usage of typing_extensions.TypedDict nested inside other types (e.g., typing.Type[typing_extensions.TypedDict]). This is not allowed by the type system but worked on older versions, so we maintain support.

Release 4.14.0 (June 2, 2025)

Changes since 4.14.0rc1:

  • Remove __or__ and __ror__ methods from typing_extensions.Sentinel on Python versions <3.10. PEP 604 was introduced in Python 3.10, and typing_extensions does not generally attempt to backport PEP-604 methods to prior versions.
  • Further update typing_extensions.evaluate_forward_ref with changes in Python 3.14.

Release 4.14.0rc1 (May 24, 2025)

  • Drop support for Python 3.8 (including PyPy-3.8). Patch by Victorien Plot.
  • Do not attempt to re-export names that have been removed from typing, anticipating the removal of typing.no_type_check_decorator in Python 3.15. Patch by Jelle Zijlstra.
  • Update typing_extensions.Format, typing_extensions.evaluate_forward_ref, and typing_extensions.TypedDict to align with changes in Python 3.14. Patches by Jelle Zijlstra.
  • Fix tests for Python 3.14 and 3.15. Patches by Jelle Zijlstra.

New features:

  • Add support for inline typed dictionaries (PEP 764). Patch by Victorien Plot.
  • Add typing_extensions.Reader and typing_extensions.Writer. Patch by Sebastian Rittau.
  • Add support for sentinels (PEP 661). Patch by Victorien Plot.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typing-extensions&package-manager=pip&previous-version=4.13.2&new-version=4.14.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.in | 2 +- requirements.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.in b/requirements.in index a1e2243c553e..ba2339b1e966 100644 --- a/requirements.in +++ b/requirements.in @@ -4,7 +4,7 @@ # 2) uv pip compile --generate-hashes --upgrade requirements.in -o requirements.txt # Unittest test adapter -typing-extensions==4.13.2 +typing-extensions==4.14.1 # Fallback env creator for debian microvenv diff --git a/requirements.txt b/requirements.txt index 3983d5414c54..92d813a57109 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,9 +46,9 @@ tomli==2.2.1 \ --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 # via -r requirements.in -typing-extensions==4.13.2 \ - --hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \ - --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef +typing-extensions==4.14.1 \ + --hash=sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36 \ + --hash=sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76 # via -r requirements.in zipp==3.21.0 \ --hash=sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4 \ From a57b431cf753d1bd5c97f5d0b025a4b7c3a28866 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 7 Jul 2025 12:46:59 -0700 Subject: [PATCH 0970/1136] Bump 2025.10 (#25246) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8d500006eedb..bc35999e36a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.9.0-dev", + "version": "2025.10.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.9.0-dev", + "version": "2025.10.0", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 6d76ded9febc..c9bed4e4b520 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.9.0-dev", + "version": "2025.10.0", "featureFlags": { "usingNewInterpreterStorage": true }, From 3fdb1b015bc93e0b0abe7bab4f52f012e25edb3c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 7 Jul 2025 13:01:26 -0700 Subject: [PATCH 0971/1136] Bump dev 2025.11 (#25247) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc35999e36a7..a5805f351472 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.10.0", + "version": "2025.11.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.10.0", + "version": "2025.11.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index c9bed4e4b520..a8a94ada4027 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.10.0", + "version": "2025.11.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 3a996cc45c57299d829181e7310ed75b0990fb22 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:27:44 -0700 Subject: [PATCH 0972/1136] update release plan to remove -rc step (#25248) --- .github/release_plan.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/release_plan.md b/.github/release_plan.md index bc9e623bc774..091ed559825b 100644 --- a/.github/release_plan.md +++ b/.github/release_plan.md @@ -40,7 +40,7 @@ NOTE: the number of this release is in the issue title and can be substituted in - [ ] Update `pet`: - [ ] Go to the [pet](https://github.com/microsoft/python-environment-tools) repo and check `main` and latest `release/*` branch. If there are new changes in `main` then create a branch called `release/YYYY.minor` (matching python extension release `major.minor`). - [ ] Update `build\azure-pipeline.stable.yml` to point to the latest `release/YYYY.minor` for `python-environment-tools`. -- [ ] Change the version in `package.json` to the next **even** number and switch the `-dev` to `-rc`. (🤖) +- [ ] Change the version in `package.json` to the next **even** number. (🤖) - [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` at this point which update the version number **only**)_. (🤖) - [ ] Update `ThirdPartyNotices-Repository.txt` as appropriate. You can check by looking at the [commit history](https://github.com/microsoft/vscode-python/commits/main) and scrolling through to see if there's anything listed there which might have pulled in some code directly into the repository from somewhere else. If you are still unsure you can check with the team. - [ ] Create a PR from your branch **`bump-release-[YYYY.minor]`** to `main`. Add the `"no change-log"` tag to the PR so it does not show up on the release notes before merging it. @@ -64,7 +64,7 @@ NOTE: If there are release branches that are two versions old you can delete the ### Step 4: Return `main` to dev and unfreeze (❄️ ➡ 💧) NOTE: The purpose of this step is ensuring that main always is on a dev version number for every night's 🌃 pre-release. Therefore it is imperative that you do this directly after the previous steps to reset the version in main to a dev version **before** a pre-release goes out. - [ ] Create a branch called **`bump-dev-version-YYYY.[minor+1]`**. -- [ ] Bump the minor version number in the `package.json` to the next `YYYY.[minor+1]` which will be an odd number, and switch the `-rc` to `-dev`.(🤖) +- [ ] Bump the minor version number in the `package.json` to the next `YYYY.[minor+1]` which will be an odd number, and add `-dev`.(🤖) - [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` only relating to the new version number)_ . (🤖) - [ ] Create a PR from this branch against `main` and merge it. @@ -83,12 +83,6 @@ NOTE: this PR should make all CI relating to `main` be passing again (such as th ### Step 6: Take the release branch from a candidate to the finalized release - [ ] Make sure the [appropriate pull requests](https://github.com/microsoft/vscode-docs/pulls) for the [documentation](https://code.visualstudio.com/docs/python/python-tutorial) -- including the [WOW](https://code.visualstudio.com/docs/languages/python) page -- are ready. - [ ] Check to make sure any final updates to the **`release/YYYY.minor`** branch have been merged. -- [ ] Create a branch against **`release/YYYY.minor`** called **`finalized-release-[YYYY.minor]`**. -- [ ] Update the version in `package.json` to remove the `-rc` (🤖) from the version. -- [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(the only update should be the version number if `package-lock.json` has been kept up-to-date)_. (🤖) -- [ ] Update `ThirdPartyNotices-Repository.txt` manually if necessary. -- [ ] Create a PR from **`finalized-release-[YYYY.minor]`** against `release/YYYY.minor` and merge it. - ### Step 7: Execute the Release - [ ] Make sure CI is passing for **`release/YYYY.minor`** release branch (🤖). From a8a2c19c950dcb737204664f2fe5542fcee6d3f3 Mon Sep 17 00:00:00 2001 From: Dhanika-Botejue <156615547+Dhanika-Botejue@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:38:10 -0400 Subject: [PATCH 0973/1136] docs: Fix grammar in macOS Python note in Quick Start (#25235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes minor grammar issue: `note: that the system` → `note: the system` in README.md. Co-authored-by: Dhanika Botejue --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b69de7351679..9fe3669ce6b0 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ If you encounter issues with any of the listed extensions, please file an issue ## Quick start -- **Step 1.** [Install a supported version of Python on your system](https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites) (note: that the system install of Python on macOS is not supported). +- **Step 1.** [Install a supported version of Python on your system](https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites) (note: the system install of Python on macOS is not supported). - **Step 2.** [Install the Python extension for Visual Studio Code](https://code.visualstudio.com/docs/editor/extension-gallery). - **Step 3.** Open or create a Python file and start coding! From af3cc219d25deab727c9f8a7aa4757bcbdcaf993 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 2 Jul 2025 09:09:53 -0700 Subject: [PATCH 0974/1136] Fix disable Python extension in untrusted workspaces (#10) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a8a94ada4027..ce389b9236a9 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ }, "capabilities": { "untrustedWorkspaces": { - "supported": "limited", - "description": "Only Partial IntelliSense with Pylance is supported. Cannot execute Python with untrusted files." + "supported": false, + "description": "Python extension is not supported in untrusted workspaces. Use Pylance extension to explore." }, "virtualWorkspaces": { "supported": "limited", From 9d0aab54a5f14de5fc72a0400d976c787b2d77b3 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 2 Jul 2025 16:27:52 -0700 Subject: [PATCH 0975/1136] fix: display text when in untrusted mode (#12) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce389b9236a9..86146d63d503 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "capabilities": { "untrustedWorkspaces": { "supported": false, - "description": "Python extension is not supported in untrusted workspaces. Use Pylance extension to explore." + "description": "The Python extension is not available in untrusted workspaces. Use Pylance to get partial IntelliSense support for Python files." }, "virtualWorkspaces": { "supported": "limited", From 40f807ac5020d733f57717aee02fcd27a9cf58b1 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Jul 2025 20:38:10 +0000 Subject: [PATCH 0976/1136] Fix skip-issue-check to support short issue references (#123) (#25260) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the PR issue check to recognize both full GitHub issue URLs and short issue references in the format `#123`. ## Problem Currently, the `skip-issue-check` workflow only accepts full GitHub issue URLs like `https://github.com/microsoft/vscode-python/issues/123` in PR descriptions. Contributors using the more common short format like `#123` would need to add the `skip-issue-check` label to bypass the check. ## Solution Modified the GitHub Actions script in `.github/workflows/pr-file-check.yml` to check for both formats: - Full URLs: `https://github.com/microsoft/vscode-python/issues/123` (existing behavior) - Short references: `#123` (new behavior) The check now passes if either format is found in the PR description. ## Changes - Added regex pattern `/#\d+/` to match short issue references - Updated condition to pass if either format is detected: `if (!issueLink && !issueReference)` - Maintains backward compatibility and existing `skip-issue-check` label behavior ## Testing Verified the updated logic handles all scenarios correctly: - ✅ Full GitHub URLs (existing) - ✅ Short format like `#123` (new) - ✅ Both formats in same PR description - ✅ Multiple short references - ✅ Skip label behavior unchanged - ❌ No issue references (correctly fails) - ❌ Invalid formats like `#` without numbers (correctly fails) Fixes #25259. --- 💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click [here](https://survey.alchemer.com/s3/8343779/Copilot-Coding-agent) to start the survey. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- .github/workflows/pr-file-check.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index 180ab16a74c3..688c48d865d8 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -51,7 +51,8 @@ jobs: if (!labels.includes('skip-issue-check')) { const prBody = context.payload.pull_request.body || ''; const issueLink = prBody.match(/https:\/\/github\.com\/\S+\/issues\/\d+/); - if (!issueLink) { + const issueReference = prBody.match(/#\d+/); + if (!issueLink && !issueReference) { core.setFailed('No associated issue found in the PR description.'); } } From dfa81d9c47858fd2a5e6f8e41e27078e78df3bda Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:18:24 -0700 Subject: [PATCH 0977/1136] Bump importlib-metadata from 8.6.1 to 8.7.0 (#25017) Bumps [importlib-metadata](https://github.com/python/importlib_metadata) from 8.6.1 to 8.7.0.
Changelog

Sourced from importlib-metadata's changelog.

v8.7.0

Features

  • .metadata() (and Distribution.metadata) can now return None if the metadata directory exists but not metadata file is present. (#493)

Bugfixes

  • Raise consistent ValueError for invalid EntryPoint.value (#518)
Commits
  • 708dff4 Finalize
  • b3065f0 Merge pull request #519 from python/bugfix/493-metadata-missing
  • e4351c2 Add a new test capturing the new expectation.
  • 5a65705 Refactor the casting into a wrapper for brevity and to document its purpose.
  • 0830c39 Add news fragment.
  • 22bb567 Fix type errors where metadata could be None.
  • 57f31d7 Allow metadata to return None when there is no metadata present.
  • b9c4be4 Merge pull request #518 from python/bugfix/488-bad-ep-value
  • 9f8af01 Prefer a cached property, as the property is likely to be retrieved at least ...
  • f179e28 Also raise ValueError on construction if the value is invalid.
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=importlib-metadata&package-manager=pip&previous-version=8.6.1&new-version=8.7.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 92d813a57109..e3563501e309 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ # This file was autogenerated by uv via the following command: # uv pip compile --generate-hashes requirements.in -o requirements.txt -importlib-metadata==8.6.1 \ - --hash=sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e \ - --hash=sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580 +importlib-metadata==8.7.0 \ + --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ + --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd # via -r requirements.in microvenv==2023.5.post1 \ --hash=sha256:32c46afea874e300f69f1add0806eb0795fd02b5fb251092fba0b73c059a7d1f \ From 500cec5868c0fd012b2ee1d931d80ac466fdf60a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:19:24 -0700 Subject: [PATCH 0978/1136] Bump mheap/github-action-required-labels from 5.5.0 to 5.5.1 (#25202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mheap/github-action-required-labels](https://github.com/mheap/github-action-required-labels) from 5.5.0 to 5.5.1.
Release notes

Sourced from mheap/github-action-required-labels's releases.

v5.5.1

What's Changed

New Contributors

Full Changelog: https://github.com/mheap/github-action-required-labels/compare/v5.5.0...v5.5.1

Commits
  • 8afbe8a Automatic compilation
  • 8eb7f59 fix: Ensure all label pages are traversed and remove per_page from API and te...
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=mheap/github-action-required-labels&package-manager=github_actions&previous-version=5.5.0&new-version=5.5.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 3b82068de5aa..04f87a6d49ba 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -17,7 +17,7 @@ jobs: pull-requests: write steps: - name: 'PR impact specified' - uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5.5.0 + uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5.5.1 with: mode: exactly count: 1 From c7fd90a631433e66bb44230f5ba8636dae928950 Mon Sep 17 00:00:00 2001 From: igorgaming <69463610+igorgaming@users.noreply.github.com> Date: Wed, 9 Jul 2025 02:16:59 +0300 Subject: [PATCH 0979/1136] Add __file__ variable to globals in exec() (#25225) Closes #25056 --- python_files/unittestadapter/django_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_files/unittestadapter/django_handler.py b/python_files/unittestadapter/django_handler.py index 77c50efc27d0..574aee7af7fa 100644 --- a/python_files/unittestadapter/django_handler.py +++ b/python_files/unittestadapter/django_handler.py @@ -104,7 +104,7 @@ def django_execution_runner(manage_py_path: str, test_ids: List[str], args: List manage_file = manage_path.open() with argv_context, suppress_context, manage_file: manage_code = manage_file.read() - exec(manage_code, {"__name__": "__main__"}) + exec(manage_code, {"__name__": "__main__", "__file__": manage_path}) except OSError as e: raise VSCodeUnittestError("Error running Django, unable to read manage.py") from e except Exception as e: From 58bd7b6290d6456e9107e2ed4b69b9d8051b7b9d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 08:16:27 -0700 Subject: [PATCH 0980/1136] Fix auto test discovery to respect setting changes without reload (#25261) Modified the event handler to check both the setting and the pattern before triggering test discovery: This ensures that when users disable the setting, test discovery immediately stops running on file saves without requiring a VS Code reload. Fixes #24117. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- src/client/testing/testController/controller.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index fe384709c371..aefc97117da5 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -549,7 +549,10 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.disposables.push( onDidSaveTextDocument(async (doc: TextDocument) => { const settings = this.configSettings.getSettings(doc.uri); - if (minimatch.default(doc.uri.fsPath, settings.testing.autoTestDiscoverOnSavePattern)) { + if ( + settings.testing.autoTestDiscoverOnSaveEnabled && + minimatch.default(doc.uri.fsPath, settings.testing.autoTestDiscoverOnSavePattern) + ) { traceVerbose(`Testing: Trigger refresh after saving ${doc.uri.fsPath}`); this.sendTriggerTelemetry('watching'); this.refreshData.trigger(doc.uri, false); From bad502a09650b1382af4dc23191a3d1ba9fbf2b1 Mon Sep 17 00:00:00 2001 From: tgrue-openai <165065431+tgrue-openai@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:37:48 -0700 Subject: [PATCH 0981/1136] Allow Activated Virtual Envs to Be Detected w/ a Workspace File (#25141) Addresses https://github.com/microsoft/vscode-python/issues/25140 (which I just filed). 1) Removed the check that a workspaceFile isn't used when looking for Activate Virtual Envs 2) Adds a debug log to help make other forms of early exit. Of note, I'm not sure if the existing `traceVerbose('VS Code was not launched from the command line');` is correct. As far as I can tell, workspaceFile != multiroot workspaces. I'm not sure why this check previously existed. I don't know if this might cause others problems; but right now this check causes a lot of challenge for our workflow. --------- Co-authored-by: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> --- .../virtualEnvs/activatedEnvLaunch.ts | 5 +-- .../activatedEnvLaunch.unit.test.ts | 31 ------------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts b/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts index 21bcc12b0d06..6b4334e13100 100644 --- a/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts +++ b/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts @@ -85,12 +85,9 @@ export class ActivatedEnvironmentLaunch implements IActivatedEnvironmentLaunch { @cache(-1, true) private async _selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection = false): Promise { - if (this.workspaceService.workspaceFile) { - // Assuming multiroot workspaces cannot be directly launched via `code .` command. - return undefined; - } if (process.env.VSCODE_CLI !== '1') { // We only want to select the interpreter if VS Code was launched from the command line. + traceLog("Skipping ActivatedEnv Detection: process.env.VSCODE_CLI !== '1'"); return undefined; } traceVerbose('VS Code was not launched from the command line'); diff --git a/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts b/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts index 2ebdecd000d1..860970bd641e 100644 --- a/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts +++ b/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts @@ -263,37 +263,6 @@ suite('Activated Env Launch', async () => { expect(_promptIfApplicable.notCalled).to.equal(true, 'Prompt should not be displayed'); }); - test('Does not update interpreter path if a multiroot workspace is opened', async () => { - process.env.VIRTUAL_ENV = virtualEnvPrefix; - interpreterService - .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); - workspaceService.setup((w) => w.workspaceFile).returns(() => uri); - const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; - workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); - pythonPathUpdaterService - .setup((p) => - p.updatePythonPath( - TypeMoq.It.isValue(virtualEnvPrefix), - TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), - TypeMoq.It.isValue('load'), - TypeMoq.It.isValue(uri), - ), - ) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.never()); - activatedEnvLaunch = new ActivatedEnvironmentLaunch( - workspaceService.object, - appShell.object, - pythonPathUpdaterService.object, - interpreterService.object, - processServiceFactory.object, - ); - const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); - expect(result).to.be.equal(undefined, 'Incorrect value'); - pythonPathUpdaterService.verifyAll(); - }); - test('Returns `undefined` if env was already selected', async () => { activatedEnvLaunch = new ActivatedEnvironmentLaunch( workspaceService.object, From 7607548b178484a45c86d038748f9109f70ad47c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:54:00 -0700 Subject: [PATCH 0982/1136] support copy testing path (#25257) fixes https://github.com/microsoft/vscode-python/issues/20047 --- package.json | 18 ++++++++ package.nls.json | 1 + src/client/common/application/commands.ts | 3 +- src/client/common/constants.ts | 1 + src/client/testing/main.ts | 16 ++++++- src/client/testing/utils.ts | 49 ++++++++++++++++++++++ src/test/testing/utils.unit.test.ts | 51 +++++++++++++++++++++++ 7 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 src/client/testing/utils.ts create mode 100644 src/test/testing/utils.unit.test.ts diff --git a/package.json b/package.json index 86146d63d503..b407c68caa35 100644 --- a/package.json +++ b/package.json @@ -272,6 +272,11 @@ "category": "Python", "command": "python.createNewFile" }, + { + "category": "Python", + "command": "python.copyTestId", + "title": "%python.command.python.testing.copyTestId.title%" + }, { "category": "Python", "command": "python.analysis.restartLanguageServer", @@ -1231,6 +1236,13 @@ "command": "python.reportIssue" } ], + "testing/item/context": [ + { + "command": "python.copyTestId", + "group": "navigation", + "when": "controllerId == 'python-tests'" + } + ], "commandPalette": [ { "category": "Python", @@ -1306,6 +1318,12 @@ "title": "%python.command.python.execSelectionInTerminal.title%", "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, + { + "category": "Python", + "command": "python.copyTestId", + "title": "%python.command.python.testing.copyTestId.title%", + "when": "false" + }, { "category": "Python", "command": "python.execInREPL", diff --git a/package.nls.json b/package.nls.json index 00c96c09b19a..37a9ce435f2f 100644 --- a/package.nls.json +++ b/package.nls.json @@ -27,6 +27,7 @@ "python.command.python.analysis.restartLanguageServer.title": "Restart Language Server", "python.command.python.launchTensorBoard.title": "Launch TensorBoard", "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", + "python.command.python.testing.copyTestId.title": "Copy Test Id", "python.createEnvironment.contentButton.description": "Show or hide Create Environment button in the editor for `requirements.txt` or other dependency files.", "python.createEnvironment.trigger.description": "Detect if environment creation is required for the current project", "python.menu.createNewFile.title": "Python File", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 98ea2669d773..402025ee38db 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -3,7 +3,7 @@ 'use strict'; -import { CancellationToken, Position, TextDocument, Uri } from 'vscode'; +import { CancellationToken, Position, TestItem, TextDocument, Uri } from 'vscode'; import { Commands as LSCommands } from '../../activation/commands'; import { Channel, Commands, CommandSource } from '../constants'; import { CreateEnvironmentOptions } from '../../pythonEnvironments/creation/proposed.createEnvApis'; @@ -50,6 +50,7 @@ export type AllCommands = keyof ICommandNameArgumentTypeMapping; * Used to provide strong typing for command & args. */ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgumentTypeMapping { + [Commands.CopyTestId]: [TestItem]; [Commands.Create_Environment]: [CreateEnvironmentOptions]; ['vscode.openWith']: [Uri, string]; ['workbench.action.quickOpen']: [string]; diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 4a8962e86b58..15fd037a3d9f 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -39,6 +39,7 @@ export namespace Commands { export const CreateNewFile = 'python.createNewFile'; export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter'; export const Create_Environment = 'python.createEnvironment'; + export const CopyTestId = 'python.copyTestId'; export const Create_Environment_Button = 'python.createEnvironment-button'; export const Create_Environment_Check = 'python.createEnvironmentCheck'; export const Create_Terminal = 'python.createTerminal'; diff --git a/src/client/testing/main.ts b/src/client/testing/main.ts index c2675ed4a72b..1941ce5e57c2 100644 --- a/src/client/testing/main.ts +++ b/src/client/testing/main.ts @@ -1,7 +1,16 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { ConfigurationChangeEvent, Disposable, Uri, tests, TestResultState, WorkspaceFolder, Command } from 'vscode'; +import { + ConfigurationChangeEvent, + Disposable, + Uri, + tests, + TestResultState, + WorkspaceFolder, + Command, + TestItem, +} from 'vscode'; import { IApplicationShell, ICommandManager, IContextKeyManager, IWorkspaceService } from '../common/application/types'; import * as constants from '../common/constants'; import '../common/extensions'; @@ -21,6 +30,7 @@ import { ExtensionContextKey } from '../common/application/contextKeys'; import { checkForFailedTests, updateTestResultMap } from './testController/common/testItemUtilities'; import { Testing } from '../common/utils/localize'; import { traceVerbose } from '../logging'; +import { writeTestIdToClipboard } from './utils'; @injectable() export class TestingService implements ITestingService { @@ -158,7 +168,6 @@ export class UnitTestManagementService implements IExtensionActivationService { private registerCommands(): void { const commandManager = this.serviceContainer.get(ICommandManager); - this.disposableRegistry.push( commandManager.registerCommand( constants.Commands.Tests_Configure, @@ -195,6 +204,9 @@ export class UnitTestManagementService implements IExtensionActivationService { }, }; }), + commandManager.registerCommand(constants.Commands.CopyTestId, async (testItem: TestItem) => { + writeTestIdToClipboard(testItem); + }), ); } diff --git a/src/client/testing/utils.ts b/src/client/testing/utils.ts new file mode 100644 index 000000000000..c1027d4a8dc1 --- /dev/null +++ b/src/client/testing/utils.ts @@ -0,0 +1,49 @@ +import { TestItem, env } from 'vscode'; +import { traceLog } from '../logging'; + +export async function writeTestIdToClipboard(testItem: TestItem): Promise { + if (testItem && typeof testItem.id === 'string') { + if (testItem.id.includes('\\') && testItem.id.indexOf('::') === -1) { + // Convert the id to a module.class.method format as this is a unittest + const moduleClassMethod = idToModuleClassMethod(testItem.id); + if (moduleClassMethod) { + await env.clipboard.writeText(moduleClassMethod); + traceLog('Testing: Copied test id to clipboard, id: ' + moduleClassMethod); + return; + } + } + // Otherwise use the id as is for pytest + await clipboardWriteText(testItem.id); + traceLog('Testing: Copied test id to clipboard, id: ' + testItem.id); + } +} + +export function idToModuleClassMethod(id: string): string | undefined { + // Split by backslash + const parts = id.split('\\'); + if (parts.length === 1) { + // Only one part, likely a parent folder or file + return parts[0]; + } + if (parts.length === 2) { + // Two parts: filePath and className + const [filePath, className] = parts.slice(-2); + const fileName = filePath.split(/[\\/]/).pop(); + if (!fileName) { + return undefined; + } + const module = fileName.replace(/\.py$/, ''); + return `${module}.${className}`; + } + // Three or more parts: filePath, className, methodName + const [filePath, className, methodName] = parts.slice(-3); + const fileName = filePath.split(/[\\/]/).pop(); + if (!fileName) { + return undefined; + } + const module = fileName.replace(/\.py$/, ''); + return `${module}.${className}.${methodName}`; +} +export function clipboardWriteText(text: string): Thenable { + return env.clipboard.writeText(text); +} diff --git a/src/test/testing/utils.unit.test.ts b/src/test/testing/utils.unit.test.ts new file mode 100644 index 000000000000..8efa0cee0e65 --- /dev/null +++ b/src/test/testing/utils.unit.test.ts @@ -0,0 +1,51 @@ +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as utils from '../../client/testing/utils'; +import sinon from 'sinon'; +use(chaiAsPromised.default); + +function test_idToModuleClassMethod() { + try { + expect(utils.idToModuleClassMethod('foo')).to.equal('foo'); + expect(utils.idToModuleClassMethod('a/b/c.pyMyClass')).to.equal('c.MyClass'); + expect(utils.idToModuleClassMethod('a/b/c.pyMyClassmy_method')).to.equal('c.MyClass.my_method'); + expect(utils.idToModuleClassMethod('\\MyClass')).to.be.undefined; + console.log('test_idToModuleClassMethod passed'); + } catch (e) { + console.error('test_idToModuleClassMethod failed:', e); + } +} + +async function test_writeTestIdToClipboard() { + let clipboardStub = sinon.stub(utils, 'clipboardWriteText').resolves(); + const { writeTestIdToClipboard } = utils; + try { + // unittest id + const testItem = { id: 'a/b/c.pyMyClass\\my_method' }; + await writeTestIdToClipboard(testItem as any); + sinon.assert.calledOnceWithExactly(clipboardStub, 'c.MyClass.my_method'); + clipboardStub.resetHistory(); + + // pytest id + const testItem2 = { id: 'tests/test_foo.py::TestClass::test_method' }; + await writeTestIdToClipboard(testItem2 as any); + sinon.assert.calledOnceWithExactly(clipboardStub, 'tests/test_foo.py::TestClass::test_method'); + clipboardStub.resetHistory(); + + // undefined + await writeTestIdToClipboard(undefined as any); + sinon.assert.notCalled(clipboardStub); + + console.log('test_writeTestIdToClipboard passed'); + } catch (e) { + console.error('test_writeTestIdToClipboard failed:', e); + } finally { + sinon.restore(); + } +} + +// Run tests +(async () => { + test_idToModuleClassMethod(); + await test_writeTestIdToClipboard(); +})(); From 1f8949c0df7aadd90de85e09066037c679bb716e Mon Sep 17 00:00:00 2001 From: Aaron Munger <2019016+amunger@users.noreply.github.com> Date: Thu, 10 Jul 2025 08:25:12 -0700 Subject: [PATCH 0983/1136] Only select the python REPL kernel when creating/restoring the REPL (#25263) fix https://github.com/microsoft/vscode-python/issues/23971 --- src/client/repl/nativeRepl.ts | 31 ++++++++------------------- src/client/repl/replCommandHandler.ts | 26 +++++++++------------- src/client/repl/replCommands.ts | 6 ++---- 3 files changed, 21 insertions(+), 42 deletions(-) diff --git a/src/client/repl/nativeRepl.ts b/src/client/repl/nativeRepl.ts index 9b002655d714..62e314172da6 100644 --- a/src/client/repl/nativeRepl.ts +++ b/src/client/repl/nativeRepl.ts @@ -3,15 +3,7 @@ // Native Repl class that holds instance of pythonServer and replController -import { - NotebookController, - NotebookControllerAffinity, - NotebookDocument, - QuickPickItem, - TextEditor, - Uri, - WorkspaceFolder, -} from 'vscode'; +import { NotebookController, NotebookDocument, QuickPickItem, TextEditor, Uri, WorkspaceFolder } from 'vscode'; import { Disposable } from 'vscode-jsonrpc'; import { PVSC_EXTENSION_ID } from '../common/constants'; import { showQuickPick } from '../common/vscodeApis/windowApis'; @@ -172,24 +164,19 @@ export class NativeRepl implements Disposable { } } - const notebookEditor = await openInteractiveREPL( - this.replController, - this.notebookDocument ?? wsMementoUri, - preserveFocus, - ); - if (notebookEditor) { - this.notebookDocument = notebookEditor.notebook; + const result = await openInteractiveREPL(this.notebookDocument ?? wsMementoUri, preserveFocus); + if (result) { + this.notebookDocument = result.notebookEditor.notebook; await updateWorkspaceStateValue( NATIVE_REPL_URI_MEMENTO, this.notebookDocument.uri.toString(), ); - if (this.notebookDocument) { - this.replController.updateNotebookAffinity(this.notebookDocument, NotebookControllerAffinity.Default); - await selectNotebookKernel(notebookEditor, this.replController.id, PVSC_EXTENSION_ID); - if (code) { - await executeNotebookCell(notebookEditor, code); - } + if (result.documentCreated) { + await selectNotebookKernel(result.notebookEditor, this.replController.id, PVSC_EXTENSION_ID); + } + if (code) { + await executeNotebookCell(result.notebookEditor, code); } } } diff --git a/src/client/repl/replCommandHandler.ts b/src/client/repl/replCommandHandler.ts index f65580dd1e17..630eddfdd565 100644 --- a/src/client/repl/replCommandHandler.ts +++ b/src/client/repl/replCommandHandler.ts @@ -1,5 +1,4 @@ import { - NotebookController, NotebookEditor, ViewColumn, NotebookDocument, @@ -10,7 +9,6 @@ import { Uri, } from 'vscode'; import { getExistingReplViewColumn, getTabNameForUri } from './replUtils'; -import { PVSC_EXTENSION_ID } from '../common/constants'; import { showNotebookDocument } from '../common/vscodeApis/windowApis'; import { openNotebookDocument, applyEdit } from '../common/vscodeApis/workspaceApis'; import { executeCommand } from '../common/vscodeApis/commandApis'; @@ -19,11 +17,11 @@ import { executeCommand } from '../common/vscodeApis/commandApis'; * Function that opens/show REPL using IW UI. */ export async function openInteractiveREPL( - notebookController: NotebookController, notebookDocument: NotebookDocument | Uri | undefined, preserveFocus: boolean = true, -): Promise { +): Promise<{ notebookEditor: NotebookEditor; documentCreated: boolean } | undefined> { let viewColumn = ViewColumn.Beside; + let alreadyExists = false; if (notebookDocument instanceof Uri) { // Case where NotebookDocument is undefined, but workspace mementoURI exists. notebookDocument = await openNotebookDocument(notebookDocument); @@ -31,12 +29,14 @@ export async function openInteractiveREPL( // Case where NotebookDocument (REPL document already exists in the tab) const existingReplViewColumn = getExistingReplViewColumn(notebookDocument); viewColumn = existingReplViewColumn ?? viewColumn; + alreadyExists = true; } else if (!notebookDocument) { // Case where NotebookDocument doesnt exist, or // became outdated (untitled.ipynb created without Python extension knowing, effectively taking over original Python REPL's URI) notebookDocument = await openNotebookDocument('jupyter-notebook'); } - const editor = await showNotebookDocument(notebookDocument!, { + + const notebookEditor = await showNotebookDocument(notebookDocument!, { viewColumn, asRepl: 'Python REPL', preserveFocus, @@ -44,21 +44,15 @@ export async function openInteractiveREPL( // Sanity check that we opened a Native REPL from showNotebookDocument. if ( - !editor || - !editor.notebook || - !editor.notebook.uri || - getTabNameForUri(editor.notebook.uri) !== 'Python REPL' + !notebookEditor || + !notebookEditor.notebook || + !notebookEditor.notebook.uri || + getTabNameForUri(notebookEditor.notebook.uri) !== 'Python REPL' ) { return undefined; } - await executeCommand('notebook.selectKernel', { - editor, - id: notebookController.id, - extension: PVSC_EXTENSION_ID, - }); - - return editor; + return { notebookEditor, documentCreated: !alreadyExists }; } /** diff --git a/src/client/repl/replCommands.ts b/src/client/repl/replCommands.ts index f260185988a8..993a0cc91b19 100644 --- a/src/client/repl/replCommands.ts +++ b/src/client/repl/replCommands.ts @@ -31,10 +31,8 @@ export async function registerStartNativeReplCommand( sendTelemetryEvent(EventName.REPL, undefined, { replType: 'Native' }); const interpreter = await getActiveInterpreter(uri, interpreterService); if (interpreter) { - if (interpreter) { - const nativeRepl = await getNativeRepl(interpreter, disposables); - await nativeRepl.sendToNativeRepl(undefined, false); - } + const nativeRepl = await getNativeRepl(interpreter, disposables); + await nativeRepl.sendToNativeRepl(undefined, false); } }), ); From 8810766af38b7a78cc8853e442de7609d46b8732 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 10 Jul 2025 08:52:44 -0700 Subject: [PATCH 0984/1136] remove stale code for displaying config error (#25269) Two parts: - removes notification on run without configuration, this was meant to be removed as stated in the TODO comment - removes unused function `displayTestFrameworkError` & tests related to it --- src/client/common/utils/localize.ts | 1 - src/client/testing/common/types.ts | 1 - src/client/testing/configuration/index.ts | 22 --- src/client/testing/main.ts | 19 +- src/test/testing/configuration.unit.test.ts | 194 +------------------- 5 files changed, 4 insertions(+), 233 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 97fe6201e4fb..067275ad732c 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -411,7 +411,6 @@ export namespace DebugConfigStrings { export namespace Testing { export const configureTests = l10n.t('Configure Test Framework'); - export const testNotConfigured = l10n.t('No test framework configured.'); export const cancelUnittestDiscovery = l10n.t('Canceled unittest test discovery'); export const errorUnittestDiscovery = l10n.t('Unittest test discovery error'); export const cancelPytestDiscovery = l10n.t('Canceled pytest test discovery'); diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts index 17d528d234f9..562005386633 100644 --- a/src/client/testing/common/types.ts +++ b/src/client/testing/common/types.ts @@ -50,7 +50,6 @@ export interface ITestsHelper { export const ITestConfigurationService = Symbol('ITestConfigurationService'); export interface ITestConfigurationService { hasConfiguredTests(wkspace: Uri): boolean; - displayTestFrameworkError(wkspace: Uri): Promise; selectTestRunner(placeHolderMessage: string): Promise; enableTest(wkspace: Uri, product: UnitTestProduct): Promise; promptToEnableAndConfigureTestFramework(wkspace: Uri): Promise; diff --git a/src/client/testing/configuration/index.ts b/src/client/testing/configuration/index.ts index 4825e9aa4f6a..b78475293594 100644 --- a/src/client/testing/configuration/index.ts +++ b/src/client/testing/configuration/index.ts @@ -40,28 +40,6 @@ export class UnitTestConfigurationService implements ITestConfigurationService { return settings.testing.pytestEnabled || settings.testing.unittestEnabled || false; } - public async displayTestFrameworkError(wkspace: Uri): Promise { - const settings = this.configurationService.getSettings(wkspace); - let enabledCount = settings.testing.pytestEnabled ? 1 : 0; - enabledCount += settings.testing.unittestEnabled ? 1 : 0; - if (enabledCount > 1) { - return this._promptToEnableAndConfigureTestFramework( - wkspace, - 'Enable only one of the test frameworks (unittest or pytest).', - true, - ); - } - const option = 'Enable and configure a Test Framework'; - const item = await this.appShell.showInformationMessage( - 'No test framework configured (unittest, or pytest)', - option, - ); - if (item !== option) { - throw NONE_SELECTED; - } - return this._promptToEnableAndConfigureTestFramework(wkspace); - } - public async selectTestRunner(placeHolderMessage: string): Promise { const items = [ { diff --git a/src/client/testing/main.ts b/src/client/testing/main.ts index 1941ce5e57c2..e794d5711f2a 100644 --- a/src/client/testing/main.ts +++ b/src/client/testing/main.ts @@ -29,7 +29,7 @@ import { DelayedTrigger, IDelayedTrigger } from '../common/utils/delayTrigger'; import { ExtensionContextKey } from '../common/application/contextKeys'; import { checkForFailedTests, updateTestResultMap } from './testController/common/testItemUtilities'; import { Testing } from '../common/utils/localize'; -import { traceVerbose } from '../logging'; +import { traceVerbose, traceWarn } from '../logging'; import { writeTestIdToClipboard } from './utils'; @injectable() @@ -103,22 +103,9 @@ export class UnitTestManagementService implements IExtensionActivationService { if (unconfigured.length === workspaces.length) { const commandManager = this.serviceContainer.get(ICommandManager); await commandManager.executeCommand('workbench.view.testing.focus'); - - // TODO: this is a workaround for https://github.com/microsoft/vscode/issues/130696 - // Once that is fixed delete this notification and test should be configured from the test view. - const app = this.serviceContainer.get(IApplicationShell); - const response = await app.showInformationMessage( - Testing.testNotConfigured, - Testing.configureTests, + traceWarn( + 'Testing: Run attempted but no test configurations found for any workspace, use command palette to configure tests for python if desired.', ); - if (response === Testing.configureTests) { - await commandManager.executeCommand( - constants.Commands.Tests_Configure, - undefined, - constants.CommandSource.ui, - unconfigured[0].uri, - ); - } } }); } diff --git a/src/test/testing/configuration.unit.test.ts b/src/test/testing/configuration.unit.test.ts index 98d19dca9cbc..e259587ecccd 100644 --- a/src/test/testing/configuration.unit.test.ts +++ b/src/test/testing/configuration.unit.test.ts @@ -25,7 +25,7 @@ import { ITestsHelper, } from '../../client/testing/common/types'; import { ITestingSettings } from '../../client/testing/configuration/types'; -import { NONE_SELECTED, UnitTestConfigurationService } from '../../client/testing/configuration'; +import { UnitTestConfigurationService } from '../../client/testing/configuration'; suite('Unit Tests - ConfigurationService', () => { UNIT_TEST_PRODUCTS.forEach((product) => { @@ -259,198 +259,6 @@ suite('Unit Tests - ConfigurationService', () => { enabled = true; expect(testConfigService.target.hasConfiguredTests(workspaceUri)).to.equal(true); }); - test('Prompt to enable a test if a test framework is not enabled', async () => { - unitTestSettings.setup((u) => u.pytestEnabled).returns(() => false); - unitTestSettings.setup((u) => u.unittestEnabled).returns(() => false); - - appShell - .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typeMoq.Times.once()); - - let exceptionThrown = false; - try { - await testConfigService.target.displayTestFrameworkError(workspaceUri); - } catch (exc) { - if (exc !== NONE_SELECTED) { - throw exc; - } - exceptionThrown = true; - } - - expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); - appShell.verifyAll(); - }); - test('Prompt to select a test if a test framework is not enabled', async () => { - unitTestSettings.setup((u) => u.pytestEnabled).returns(() => false); - unitTestSettings.setup((u) => u.unittestEnabled).returns(() => false); - - appShell - .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((_msg, option) => Promise.resolve(option)) - .verifiable(typeMoq.Times.once()); - - let exceptionThrown = false; - let selectTestRunnerInvoked = false; - try { - testConfigService.callBase = false; - testConfigService - .setup((t) => t.selectTestRunner(typeMoq.It.isAny())) - .returns(() => { - selectTestRunnerInvoked = true; - return Promise.resolve(undefined); - }); - await testConfigService.target.displayTestFrameworkError(workspaceUri); - } catch (exc) { - if (exc !== NONE_SELECTED) { - throw exc; - } - exceptionThrown = true; - } - - expect(selectTestRunnerInvoked).to.be.equal(true, 'Method not invoked'); - expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); - appShell.verifyAll(); - }); - test('Configure selected test framework and disable others', async () => { - unitTestSettings.setup((u) => u.pytestEnabled).returns(() => false); - unitTestSettings.setup((u) => u.unittestEnabled).returns(() => false); - - const workspaceConfig = typeMoq.Mock.ofType( - undefined, - typeMoq.MockBehavior.Strict, - ); - workspaceConfig - .setup((w) => w.get(typeMoq.It.isAny())) - .returns(() => true) - .verifiable(typeMoq.Times.once()); - workspaceService - .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) - .returns(() => workspaceConfig.object) - .verifiable(typeMoq.Times.once()); - - appShell - .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((_msg, option) => Promise.resolve(option)) - .verifiable(typeMoq.Times.once()); - - let selectTestRunnerInvoked = false; - testConfigService.callBase = false; - testConfigService - .setup((t) => t.selectTestRunner(typeMoq.It.isAny())) - .returns(() => { - selectTestRunnerInvoked = true; - return Promise.resolve(product); - }); - - const configMgr = typeMoq.Mock.ofType( - undefined, - typeMoq.MockBehavior.Strict, - ); - factory - .setup((f) => - f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product), typeMoq.It.isAny()), - ) - .returns(() => configMgr.object) - .verifiable(typeMoq.Times.once()); - - configMgr - .setup((c) => c.configure(typeMoq.It.isValue(workspaceUri))) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - configMgr - .setup((c) => c.enable()) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - - await testConfigService.target.displayTestFrameworkError(workspaceUri); - - expect(selectTestRunnerInvoked).to.be.equal(true, 'Select Test Runner not invoked'); - appShell.verifyAll(); - factory.verifyAll(); - configMgr.verifyAll(); - workspaceConfig.verifyAll(); - }); - test('If more than one test framework is enabled, then prompt to select a test framework', async () => { - unitTestSettings.setup((u) => u.pytestEnabled).returns(() => true); - unitTestSettings.setup((u) => u.unittestEnabled).returns(() => true); - - appShell - .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typeMoq.Times.never()); - appShell - .setup((s) => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typeMoq.Times.once()); - - let exceptionThrown = false; - try { - await testConfigService.target.displayTestFrameworkError(workspaceUri); - } catch (exc) { - if (exc !== NONE_SELECTED) { - throw exc; - } - exceptionThrown = true; - } - - expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); - appShell.verifyAll(); - }); - test('If more than one test framework is enabled, then prompt to select a test framework and enable test, but do not configure', async () => { - unitTestSettings.setup((u) => u.pytestEnabled).returns(() => true); - unitTestSettings.setup((u) => u.unittestEnabled).returns(() => true); - - appShell - .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((_msg, option) => Promise.resolve(option)) - .verifiable(typeMoq.Times.never()); - - let selectTestRunnerInvoked = false; - testConfigService.callBase = false; - testConfigService - .setup((t) => t.selectTestRunner(typeMoq.It.isAny())) - .returns(() => { - selectTestRunnerInvoked = true; - return Promise.resolve(product); - }); - - let enableTestInvoked = false; - testConfigService - .setup((t) => t.enableTest(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product))) - .returns(() => { - enableTestInvoked = true; - return Promise.resolve(); - }); - - const configMgr = typeMoq.Mock.ofType( - undefined, - typeMoq.MockBehavior.Strict, - ); - factory - .setup((f) => - f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product), typeMoq.It.isAny()), - ) - .returns(() => configMgr.object) - .verifiable(typeMoq.Times.once()); - - configMgr - .setup((c) => c.configure(typeMoq.It.isValue(workspaceUri))) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.never()); - configMgr - .setup((c) => c.enable()) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - - await testConfigService.target.displayTestFrameworkError(workspaceUri); - - expect(selectTestRunnerInvoked).to.be.equal(true, 'Select Test Runner not invoked'); - expect(enableTestInvoked).to.be.equal(false, 'Enable Test is invoked'); - factory.verifyAll(); - appShell.verifyAll(); - configMgr.verifyAll(); - }); test('Prompt to enable and configure selected test framework', async () => { unitTestSettings.setup((u) => u.pytestEnabled).returns(() => false); From 3ac006b13c5168493d3d228a843607569c19f4d1 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 10 Jul 2025 10:07:27 -0700 Subject: [PATCH 0985/1136] clean-up stale code in testing (#25270) --- .../testController/common/argumentsHelper.ts | 16 ---------------- .../testing/testController/pytest/runner.ts | 10 ---------- .../testing/testController/serviceRegistry.ts | 6 +----- .../testing/testController/unittest/runner.ts | 11 ----------- 4 files changed, 1 insertion(+), 42 deletions(-) delete mode 100644 src/client/testing/testController/pytest/runner.ts delete mode 100644 src/client/testing/testController/unittest/runner.ts diff --git a/src/client/testing/testController/common/argumentsHelper.ts b/src/client/testing/testController/common/argumentsHelper.ts index ef2999551f02..c155d0197da7 100644 --- a/src/client/testing/testController/common/argumentsHelper.ts +++ b/src/client/testing/testController/common/argumentsHelper.ts @@ -3,22 +3,6 @@ import { traceWarn } from '../../../logging'; -export function getOptionValues(args: string[], option: string): string[] { - const values: string[] = []; - let returnNextValue = false; - for (const arg of args) { - if (returnNextValue) { - values.push(arg); - returnNextValue = false; - } else if (arg.startsWith(`${option}=`)) { - values.push(arg.substring(`${option}=`.length)); - } else if (arg === option) { - returnNextValue = true; - } - } - return values; -} - export function getPositionalArguments( args: string[], optionsWithArguments: string[] = [], diff --git a/src/client/testing/testController/pytest/runner.ts b/src/client/testing/testController/pytest/runner.ts deleted file mode 100644 index e62902e4060a..000000000000 --- a/src/client/testing/testController/pytest/runner.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { ITestsRunner } from '../common/types'; - -export class PytestRunner implements ITestsRunner { - // eslint-disable-next-line @typescript-eslint/no-useless-constructor - constructor() { - // not used, but required for DI - } -} diff --git a/src/client/testing/testController/serviceRegistry.ts b/src/client/testing/testController/serviceRegistry.ts index 783af6fc8bda..03bf883e8eb1 100644 --- a/src/client/testing/testController/serviceRegistry.ts +++ b/src/client/testing/testController/serviceRegistry.ts @@ -4,23 +4,19 @@ import { IExtensionSingleActivationService } from '../../activation/types'; import { IServiceManager } from '../../ioc/types'; import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../common/constants'; -import { ITestFrameworkController, ITestsRunner, ITestController } from './common/types'; +import { ITestFrameworkController, ITestController } from './common/types'; import { PythonTestController } from './controller'; import { PytestController } from './pytest/pytestController'; -import { PytestRunner } from './pytest/runner'; -import { UnittestRunner } from './unittest/runner'; import { UnittestController } from './unittest/unittestController'; export function registerTestControllerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(ITestFrameworkController, PytestController, PYTEST_PROVIDER); - serviceManager.addSingleton(ITestsRunner, PytestRunner, PYTEST_PROVIDER); serviceManager.addSingleton( ITestFrameworkController, UnittestController, UNITTEST_PROVIDER, ); - serviceManager.addSingleton(ITestsRunner, UnittestRunner, UNITTEST_PROVIDER); serviceManager.addSingleton(ITestController, PythonTestController); serviceManager.addBinding(ITestController, IExtensionSingleActivationService); } diff --git a/src/client/testing/testController/unittest/runner.ts b/src/client/testing/testController/unittest/runner.ts deleted file mode 100644 index 45a0bddaeb75..000000000000 --- a/src/client/testing/testController/unittest/runner.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { ITestsRunner } from '../common/types'; - -export class UnittestRunner implements ITestsRunner { - // eslint-disable-next-line @typescript-eslint/no-useless-constructor - constructor() { - // not used, but required for DI - } -} From 0986bdc6188902f1beadae28c40a57f0e0e67646 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 10 Jul 2025 10:15:28 -0700 Subject: [PATCH 0986/1136] switch to absolute ids for class type testing objects (#25266) problem with continuity here: https://github.com/microsoft/vscode-python/pull/25257 --- .../expected_discovery_test_output.py | 85 +++++++++++++++---- python_files/vscode_pytest/__init__.py | 2 +- 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/python_files/tests/pytestadapter/expected_discovery_test_output.py b/python_files/tests/pytestadapter/expected_discovery_test_output.py index d7e82acc6890..13bb6ee983cf 100644 --- a/python_files/tests/pytestadapter/expected_discovery_test_output.py +++ b/python_files/tests/pytestadapter/expected_discovery_test_output.py @@ -91,7 +91,10 @@ ), } ], - "id_": "unittest_pytest_same_file.py::TestExample", + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample", + unit_pytest_same_file_path, + ), }, { "name": "test_true_pytest", @@ -200,7 +203,10 @@ ), }, ], - "id_": "unittest_folder/test_add.py::TestAddFunction", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction", + test_add_path, + ), }, { "name": "TestDuplicateFunction", @@ -225,7 +231,10 @@ ), }, ], - "id_": "unittest_folder/test_add.py::TestDuplicateFunction", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction", + test_add_path, + ), }, ], }, @@ -275,7 +284,10 @@ ), }, ], - "id_": "unittest_folder/test_subtract.py::TestSubtractFunction", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction", + test_subtract_path, + ), }, { "name": "TestDuplicateFunction", @@ -300,7 +312,10 @@ ), }, ], - "id_": "unittest_folder/test_subtract.py::TestDuplicateFunction", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction", + test_subtract_path, + ), }, ], }, @@ -534,7 +549,10 @@ "name": "TestClass", "path": os.fspath(parameterize_tests_path), "type_": "class", - "id_": "parametrize_tests.py::TestClass", + "id_": get_absolute_test_id( + "parametrize_tests.py::TestClass", + parameterize_tests_path, + ), "children": [ { "name": "test_adding", @@ -907,13 +925,19 @@ "name": "TestFirstClass", "path": str(TEST_MULTI_CLASS_NEST_PATH), "type_": "class", - "id_": "test_multi_class_nest.py::TestFirstClass", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass", + TEST_MULTI_CLASS_NEST_PATH, + ), "children": [ { "name": "TestSecondClass", "path": str(TEST_MULTI_CLASS_NEST_PATH), "type_": "class", - "id_": "test_multi_class_nest.py::TestFirstClass::TestSecondClass", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass", + TEST_MULTI_CLASS_NEST_PATH, + ), "children": [ { "name": "test_second", @@ -954,7 +978,10 @@ "name": "TestSecondClass2", "path": str(TEST_MULTI_CLASS_NEST_PATH), "type_": "class", - "id_": "test_multi_class_nest.py::TestFirstClass::TestSecondClass2", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass2", + TEST_MULTI_CLASS_NEST_PATH, + ), "children": [ { "name": "test_second2", @@ -1196,7 +1223,10 @@ + "::TestNotEmpty::test_string", }, ], - "id_": "same_function_new_class_param.py::TestNotEmpty", + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), }, { "name": "TestEmpty", @@ -1264,7 +1294,10 @@ + "::TestEmpty::test_string", }, ], - "id_": "same_function_new_class_param.py::TestEmpty", + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestEmpty", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), }, ], } @@ -1334,7 +1367,10 @@ ), } ], - "id_": "test_param_span_class.py::TestClass1", + "id_": get_absolute_test_id( + "test_param_span_class.py::TestClass1", + TEST_DATA_PATH / "test_param_span_class.py", + ), }, { "name": "TestClass2", @@ -1387,7 +1423,10 @@ ), } ], - "id_": "test_param_span_class.py::TestClass2", + "id_": get_absolute_test_id( + "test_param_span_class.py::TestClass2", + TEST_DATA_PATH / "test_param_span_class.py", + ), }, ], } @@ -1460,7 +1499,10 @@ ), }, ], - "id_": "pytest_describe_plugin/describe_only.py::describe_A", + "id_": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A", + describe_only_path, + ), } ], } @@ -1540,7 +1582,10 @@ ), }, ], - "id_": "pytest_describe_plugin/nested_describe.py::describe_list::describe_append", + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append", + nested_describe_path, + ), }, { "name": "describe_remove", @@ -1565,10 +1610,16 @@ ), } ], - "id_": "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove", + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove", + nested_describe_path, + ), }, ], - "id_": "pytest_describe_plugin/nested_describe.py::describe_list", + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list", + nested_describe_path, + ), } ], } diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 18469cd0627f..de396d8520ef 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -769,7 +769,7 @@ def create_class_node(class_module: pytest.Class | DescribeBlock) -> TestNode: "path": get_node_path(class_module), "type_": "class", "children": [], - "id_": class_module.nodeid, + "id_": get_absolute_test_id(class_module.nodeid, get_node_path(class_module)), } From 285734a2251b3d26259e08e6c05067985afd367f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:09:54 -0700 Subject: [PATCH 0987/1136] support --black as arg for testing (#25271) fixes https://github.com/microsoft/vscode-python/issues/24966 --- .../pytest-json-test-builder.instructions.md | 126 ++++++++++++++++++ build/test-requirements.txt | 1 + .../.data/2496-black-formatter/app.py | 6 + .../.data/2496-black-formatter/test_app.py | 14 ++ .../expected_discovery_test_output.py | 107 +++++++++++++++ .../tests/pytestadapter/test_discovery.py | 38 ++++-- python_files/vscode_pytest/__init__.py | 1 - .../testing/testController/common/utils.ts | 9 +- 8 files changed, 292 insertions(+), 10 deletions(-) create mode 100644 .github/instructions/pytest-json-test-builder.instructions.md create mode 100644 python_files/tests/pytestadapter/.data/2496-black-formatter/app.py create mode 100644 python_files/tests/pytestadapter/.data/2496-black-formatter/test_app.py diff --git a/.github/instructions/pytest-json-test-builder.instructions.md b/.github/instructions/pytest-json-test-builder.instructions.md new file mode 100644 index 000000000000..436bce0c9cd8 --- /dev/null +++ b/.github/instructions/pytest-json-test-builder.instructions.md @@ -0,0 +1,126 @@ +--- +applyTo: 'python_files/tests/pytestadapter/test_discovery.py' +description: 'A guide for adding new tests for pytest discovery and JSON formatting in the test_pytest_collect suite.' +--- + +# How to Add New Pytest Discovery Tests + +This guide explains how to add new tests for pytest discovery and JSON formatting in the `test_pytest_collect` suite. Follow these steps to ensure your tests are consistent and correct. + +--- + +## 1. Add Your Test File + +- Place your new test file/files in the appropriate subfolder under: + ``` + python_files/tests/pytestadapter/.data/ + ``` +- Organize folders and files to match the structure you want to test. For example, to test nested folders, create the corresponding directory structure. +- In your test file, mark each test function with a comment: + ```python + def test_function(): # test_marker--test_function + ... + ``` + +**Root Node Matching:** + +- The root node in your expected output must match the folder or file you pass to pytest discovery. For example, if you run discovery on a subfolder, the root `"name"`, `"path"`, and `"id_"` in your expected output should be that subfolder, not the parent `.data` folder. +- Only use `.data` as the root if you are running discovery on the entire `.data` folder. + +**Example:** +If you run: + +```python +helpers.runner([os.fspath(TEST_DATA_PATH / "myfolder"), "--collect-only"]) +``` + +then your expected output root should be: + +```python +{ + "name": "myfolder", + "path": os.fspath(TEST_DATA_PATH / "myfolder"), + "type_": "folder", + ... +} +``` + +--- + +## 2. Update `expected_discovery_test_output.py` + +- Open `expected_discovery_test_output.py` in the same test suite. +- Add a new expected output dictionary for your test file, following the format of existing entries. +- Use the helper functions and path conventions: + - Use `os.fspath()` for all paths. + - Use `find_test_line_number("function_name", file_path)` for the `lineno` field. + - Use `get_absolute_test_id("relative_path::function_name", file_path)` for `id_` and `runID`. + - Always use current path concatenation (e.g., `TEST_DATA_PATH / "your_folder" / "your_file.py"`). + - Create new constants as needed to keep the code clean and maintainable. + +**Important:** + +- Do **not** read the entire `expected_discovery_test_output.py` file if you only need to add or reference a single constant. This file is very large; prefer searching for the relevant section or appending to the end. + +**Example:** +If you run discovery on a subfolder: + +```python +helpers.runner([os.fspath(TEST_DATA_PATH / "myfolder"), "--collect-only"]) +``` + +then your expected output root should be: + +```python +myfolder_path = TEST_DATA_PATH / "myfolder" +my_expected_output = { + "name": "myfolder", + "path": os.fspath(myfolder_path), + "type_": "folder", + ... +} +``` + +- Add a comment above your dictionary describing the structure, as in the existing examples. + +--- + +## 3. Add Your Test to `test_discovery.py` + +- In `test_discovery.py`, add your new test as a parameterized case to the main `test_pytest_collect` function. Do **not** create a standalone test function for new discovery cases. +- Reference your new expected output constant from `expected_discovery_test_output.py`. + +**Example:** + +```python +@pytest.mark.parametrize( + ("file", "expected_const"), + [ + ("myfolder", my_expected_output), + # ... other cases ... + ], +) +def test_pytest_collect(file, expected_const): + ... +``` + +--- + +## 4. Run and Verify + +- Run the test suite to ensure your new test is discovered and passes. +- If the test fails, check your expected output dictionary for path or structure mismatches. + +--- + +## 5. Tips + +- Always use the helper functions for line numbers and IDs. +- Match the folder/file structure in `.data` to the expected JSON structure. +- Use comments to document the expected output structure for clarity. +- Ensure all `"path"` and `"id_"` fields in your expected output match exactly what pytest returns, including absolute paths and root node structure. + +--- + +**Reference:** +See `expected_discovery_test_output.py` for more examples and formatting. Use search or jump to the end of the file to avoid reading the entire file when possible. diff --git a/build/test-requirements.txt b/build/test-requirements.txt index df9fd2b08c6e..6d64ff72ac7f 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -39,3 +39,4 @@ pytest-describe # for pytest-ruff related tests pytest-ruff +pytest-black diff --git a/python_files/tests/pytestadapter/.data/2496-black-formatter/app.py b/python_files/tests/pytestadapter/.data/2496-black-formatter/app.py new file mode 100644 index 000000000000..3b474e9d911e --- /dev/null +++ b/python_files/tests/pytestadapter/.data/2496-black-formatter/app.py @@ -0,0 +1,6 @@ +def add(a, b): + return a + b + + +def subtract(a, b): + return a - b diff --git a/python_files/tests/pytestadapter/.data/2496-black-formatter/test_app.py b/python_files/tests/pytestadapter/.data/2496-black-formatter/test_app.py new file mode 100644 index 000000000000..ef4398feb786 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/2496-black-formatter/test_app.py @@ -0,0 +1,14 @@ +import pytest +from app import add, subtract + + +def test_add(): # test_marker--test_add + assert add(2, 3) == 5 + assert add(-1, 1) == 0 + assert add(0, 0) == 0 + + +def test_subtract(): # test_marker--test_subtract + assert subtract(5, 3) == 2 + assert subtract(0, 0) == 0 + assert subtract(-1, -1) == 0 diff --git a/python_files/tests/pytestadapter/expected_discovery_test_output.py b/python_files/tests/pytestadapter/expected_discovery_test_output.py index 13bb6ee983cf..e00db5d660a3 100644 --- a/python_files/tests/pytestadapter/expected_discovery_test_output.py +++ b/python_files/tests/pytestadapter/expected_discovery_test_output.py @@ -1719,3 +1719,110 @@ ], "id_": TEST_DATA_PATH_STR, } + +# This is the expected output for the 2496-black-formatter folder when run with black plugin +# └── .data +# └── 2496-black-formatter +# └── app.py +# └── black +# └── test_app.py +# └── black +# └── test_add +# └── test_subtract +black_formatter_folder_path = TEST_DATA_PATH / "2496-black-formatter" +black_app_path = black_formatter_folder_path / "app.py" +black_test_app_path = black_formatter_folder_path / "test_app.py" +black_formatter_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "2496-black-formatter", + "path": os.fspath(black_formatter_folder_path), + "type_": "folder", + "id_": os.fspath(black_formatter_folder_path), + "children": [ + { + "name": "app.py", + "path": os.fspath(black_app_path), + "type_": "file", + "id_": os.fspath(black_app_path), + "children": [ + { + "name": "black", + "path": os.fspath(black_app_path), + "lineno": "0", + "type_": "test", + "id_": get_absolute_test_id( + "2496-black-formatter/app.py::black", + black_app_path, + ), + "runID": get_absolute_test_id( + "2496-black-formatter/app.py::black", + black_app_path, + ), + } + ], + }, + { + "name": "test_app.py", + "path": os.fspath(black_test_app_path), + "type_": "file", + "id_": os.fspath(black_test_app_path), + "children": [ + { + "name": "black", + "path": os.fspath(black_test_app_path), + "lineno": "0", + "type_": "test", + "id_": get_absolute_test_id( + "2496-black-formatter/test_app.py::black", + black_test_app_path, + ), + "runID": get_absolute_test_id( + "2496-black-formatter/test_app.py::black", + black_test_app_path, + ), + }, + { + "name": "test_add", + "path": os.fspath(black_test_app_path), + "lineno": find_test_line_number( + "test_add", + black_test_app_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "2496-black-formatter/test_app.py::test_add", + black_test_app_path, + ), + "runID": get_absolute_test_id( + "2496-black-formatter/test_app.py::test_add", + black_test_app_path, + ), + }, + { + "name": "test_subtract", + "path": os.fspath(black_test_app_path), + "lineno": find_test_line_number( + "test_subtract", + black_test_app_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "2496-black-formatter/test_app.py::test_subtract", + black_test_app_path, + ), + "runID": get_absolute_test_id( + "2496-black-formatter/test_app.py::test_subtract", + black_test_app_path, + ), + }, + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} diff --git a/python_files/tests/pytestadapter/test_discovery.py b/python_files/tests/pytestadapter/test_discovery.py index 4f9fe3eb19ac..842ee3c7c707 100644 --- a/python_files/tests/pytestadapter/test_discovery.py +++ b/python_files/tests/pytestadapter/test_discovery.py @@ -337,15 +337,37 @@ def test_config_sub_folder(): assert tests.get("name") == "config_sub_folder" -def test_ruff_plugin(): - """Here the session node will be a subfolder of the workspace root and the test are in another subfolder. +@pytest.mark.parametrize( + ("file", "expected_const", "extra_arg"), + [ + ( + "folder_with_script", + expected_discovery_test_output.ruff_test_expected_output, + "--ruff", + ), + ( + "2496-black-formatter", + expected_discovery_test_output.black_formatter_expected_output, + "--black", + ), + ], +) +def test_plugin_collect(file, expected_const, extra_arg): + """Test pytest discovery on a folder with a plugin argument (e.g., --ruff, --black). - This tests checks to see if test node path are under the session node and if so the - session node is correctly updated to the common path. + Uses variables from expected_discovery_test_output.py to store the expected + dictionary return. Only handles discovery and therefore already contains the arg + --collect-only. All test discovery will succeed, be in the correct cwd, and match + expected test output. + + Keyword arguments: + file -- a string with the file or folder to run pytest discovery on. + expected_const -- the expected output from running pytest discovery on the file. + extra_arg -- the extra plugin argument to pass (e.g., --ruff, --black) """ - file_path = helpers.TEST_DATA_PATH / "folder_with_script" + file_path = helpers.TEST_DATA_PATH / file actual = helpers.runner( - [os.fspath(file_path), "--collect-only", "--ruff"], + [os.fspath(file_path), "--collect-only", extra_arg], ) assert actual @@ -359,8 +381,8 @@ def test_ruff_plugin(): assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) assert is_same_tree( actual_item.get("tests"), - expected_discovery_test_output.ruff_test_expected_output, + expected_const, ["id_", "lineno", "name", "runID"], ), ( - f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.ruff_test_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_const, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" ) diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index de396d8520ef..72eaa7a787d5 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -17,7 +17,6 @@ if TYPE_CHECKING: from pluggy import Result - USES_PYTEST_DESCRIBE = False with contextlib.suppress(ImportError): diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index c624ef034cf1..f4647d20666d 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -209,7 +209,14 @@ export function populateTestTree( let range: Range | undefined; if (child.lineno) { - range = new Range(new Position(Number(child.lineno) - 1, 0), new Position(Number(child.lineno), 0)); + if (Number(child.lineno) === 0) { + range = new Range(new Position(0, 0), new Position(0, 0)); + } else { + range = new Range( + new Position(Number(child.lineno) - 1, 0), + new Position(Number(child.lineno), 0), + ); + } } testItem.canResolveChildren = false; testItem.range = range; From 81f3397bceb804305383b3651180910e7fbdb19e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 10 Jul 2025 22:18:18 +0000 Subject: [PATCH 0988/1136] Enhance pytest installation flow and error handling with Environment Extension integration (#25252) ## Overview This PR addresses the issue where pytest configuration attempts would proceed without user confirmation when pytest is not installed, and provides better error messages when pytest installation issues occur. ## Changes Made ### 1. Enhanced User Prompt for pytest Installation **Before**: Extension would silently attempt to install pytest without user input. **After**: Shows a user-friendly prompt when pytest is selected but not installed: ``` pytest selected but not installed. Would you like to install pytest? [Install pytest] [Ignore] ``` ### 2. Python Environments Extension Integration When the Python Environments extension is available: - Uses the `managePackages` API for proper environment-targeted installation - Ensures pytest is installed in the correct Python environment - Provides better integration with the extension ecosystem **New Class**: `PytestInstallationHelper` handles the enhanced installation flow with fallback to traditional installer when the environment extension is not available. ## Technical Implementation - **New**: `src/client/testing/configuration/pytestInstallationHelper.ts` - Handles enhanced installation flow - **Enhanced**: `src/client/testing/configuration/pytest/testConfigurationManager.ts` - Integrates new installation helper - **Enhanced**: `src/client/testing/testController/common/utils.ts` - Improved error message detection - **Comprehensive test coverage** with unit tests for all scenarios Fixes #[25251](https://github.com/microsoft/vscode-python/issues/25251). also fixes https://github.com/microsoft/vscode-python/issues/17772 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Co-authored-by: anthonykim1 <62267334+anthonykim1@users.noreply.github.com> --- .../pytest/testConfigurationManager.ts | 23 ++- .../configuration/pytestInstallationHelper.ts | 95 +++++++++++++ .../testing/testController/common/utils.ts | 26 +++- .../pytestInstallationHelper.unit.test.ts | 131 ++++++++++++++++++ .../common/buildErrorNodeOptions.unit.test.ts | 51 +++++++ 5 files changed, 322 insertions(+), 4 deletions(-) create mode 100644 src/client/testing/configuration/pytestInstallationHelper.ts create mode 100644 src/test/testing/configuration/pytestInstallationHelper.unit.test.ts create mode 100644 src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts diff --git a/src/client/testing/configuration/pytest/testConfigurationManager.ts b/src/client/testing/configuration/pytest/testConfigurationManager.ts index 89f4246346ef..08f88f8564c7 100644 --- a/src/client/testing/configuration/pytest/testConfigurationManager.ts +++ b/src/client/testing/configuration/pytest/testConfigurationManager.ts @@ -3,12 +3,19 @@ import { QuickPickItem, Uri } from 'vscode'; import { IFileSystem } from '../../../common/platform/types'; import { Product } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; +import { IApplicationShell } from '../../../common/application/types'; import { TestConfigurationManager } from '../../common/testConfigurationManager'; import { ITestConfigSettingsService } from '../../common/types'; +import { PytestInstallationHelper } from '../pytestInstallationHelper'; +import { traceInfo } from '../../../logging'; export class ConfigurationManager extends TestConfigurationManager { + private readonly pytestInstallationHelper: PytestInstallationHelper; + constructor(workspace: Uri, serviceContainer: IServiceContainer, cfg?: ITestConfigSettingsService) { super(workspace, Product.pytest, serviceContainer, cfg); + const appShell = serviceContainer.get(IApplicationShell); + this.pytestInstallationHelper = new PytestInstallationHelper(appShell); } public async requiresUserToConfigure(wkspace: Uri): Promise { @@ -42,10 +49,22 @@ export class ConfigurationManager extends TestConfigurationManager { args.push(testDir); } const installed = await this.installer.isInstalled(Product.pytest); + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); if (!installed) { - await this.installer.install(Product.pytest); + // Check if Python Environments extension is available for enhanced installation flow + if (this.pytestInstallationHelper.isEnvExtensionAvailable()) { + traceInfo('pytest not installed, prompting user with environment extension integration'); + const installAttempted = await this.pytestInstallationHelper.promptToInstallPytest(wkspace); + if (!installAttempted) { + // User chose to ignore or installation failed + return; + } + } else { + // Fall back to traditional installer + traceInfo('pytest not installed, falling back to traditional installer'); + await this.installer.install(Product.pytest); + } } - await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); } private async getConfigFiles(rootDir: string): Promise { diff --git a/src/client/testing/configuration/pytestInstallationHelper.ts b/src/client/testing/configuration/pytestInstallationHelper.ts new file mode 100644 index 000000000000..bd5fbcd5bb37 --- /dev/null +++ b/src/client/testing/configuration/pytestInstallationHelper.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri, l10n } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { traceInfo, traceError } from '../../logging'; +import { useEnvExtension, getEnvExtApi } from '../../envExt/api.internal'; +import { getEnvironment } from '../../envExt/api.internal'; + +/** + * Helper class to handle pytest installation using the appropriate method + * based on whether the Python Environments extension is available. + */ +export class PytestInstallationHelper { + constructor(private readonly appShell: IApplicationShell) {} + + /** + * Prompts the user to install pytest with appropriate installation method. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was attempted, false otherwise + */ + async promptToInstallPytest(workspaceUri: Uri): Promise { + const message = l10n.t('pytest selected but not installed. Would you like to install pytest?'); + const installOption = l10n.t('Install pytest'); + + const selection = await this.appShell.showInformationMessage(message, { modal: true }, installOption); + + if (selection === installOption) { + return this.installPytest(workspaceUri); + } + + return false; + } + + /** + * Installs pytest using the appropriate method based on available extensions. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was successful, false otherwise + */ + private async installPytest(workspaceUri: Uri): Promise { + try { + if (useEnvExtension()) { + return this.installPytestWithEnvExtension(workspaceUri); + } else { + // Fall back to traditional installer if environments extension is not available + traceInfo( + 'Python Environments extension not available, installation cannot proceed via environment extension', + ); + return false; + } + } catch (error) { + traceError('Error installing pytest:', error); + return false; + } + } + + /** + * Installs pytest using the Python Environments extension. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was successful, false otherwise + */ + private async installPytestWithEnvExtension(workspaceUri: Uri): Promise { + try { + const envExtApi = await getEnvExtApi(); + const environment = await getEnvironment(workspaceUri); + + if (!environment) { + traceError('No Python environment found for workspace:', workspaceUri.fsPath); + await this.appShell.showErrorMessage( + l10n.t('No Python environment found. Please set up a Python environment first.'), + ); + return false; + } + + traceInfo('Installing pytest using Python Environments extension...'); + await envExtApi.managePackages(environment, { + install: ['pytest'], + }); + + traceInfo('pytest installation completed successfully'); + return true; + } catch (error) { + traceError('Failed to install pytest using Python Environments extension:', error); + return false; + } + } + + /** + * Checks if the Python Environments extension is available for package management. + * @returns True if the extension is available, false otherwise + */ + isEnvExtensionAvailable(): boolean { + return useEnvExtension(); + } +} diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index f4647d20666d..0bbf0e449dcd 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -174,12 +174,34 @@ export async function startDiscoveryNamedPipe( return pipeName; } +/** + * Detects if an error message indicates that pytest is not installed. + * @param message The error message to check + * @returns True if the error indicates pytest is not installed + */ +function isPytestNotInstalledError(message: string): boolean { + return ( + (message.includes('ModuleNotFoundError') && message.includes('pytest')) || + (message.includes('No module named') && message.includes('pytest')) || + (message.includes('ImportError') && message.includes('pytest')) + ); +} + export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { - const labelText = testType === 'pytest' ? 'pytest Discovery Error' : 'Unittest Discovery Error'; + let labelText = testType === 'pytest' ? 'pytest Discovery Error' : 'Unittest Discovery Error'; + let errorMessage = message; + + // Provide more specific error message if pytest is not installed + if (testType === 'pytest' && isPytestNotInstalledError(message)) { + labelText = 'pytest Not Installed'; + errorMessage = + 'pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.'; + } + return { id: `DiscoveryError:${uri.fsPath}`, label: `${labelText} [${path.basename(uri.fsPath)}]`, - error: message, + error: errorMessage, }; } diff --git a/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts new file mode 100644 index 000000000000..d7a1313df591 --- /dev/null +++ b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { PytestInstallationHelper } from '../../../client/testing/configuration/pytestInstallationHelper'; +import * as envExtApi from '../../../client/envExt/api.internal'; + +suite('PytestInstallationHelper', () => { + let appShell: TypeMoq.IMock; + let helper: PytestInstallationHelper; + let useEnvExtensionStub: sinon.SinonStub; + let getEnvExtApiStub: sinon.SinonStub; + let getEnvironmentStub: sinon.SinonStub; + + const workspaceUri = Uri.file('/test/workspace'); + + setup(() => { + appShell = TypeMoq.Mock.ofType(); + helper = new PytestInstallationHelper(appShell.object); + + useEnvExtensionStub = sinon.stub(envExtApi, 'useEnvExtension'); + getEnvExtApiStub = sinon.stub(envExtApi, 'getEnvExtApi'); + getEnvironmentStub = sinon.stub(envExtApi, 'getEnvironment'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('promptToInstallPytest should return false if user selects ignore', async () => { + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve('Ignore')) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('promptToInstallPytest should return false if user cancels', async () => { + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('isEnvExtensionAvailable should return result from useEnvExtension', () => { + useEnvExtensionStub.returns(true); + + const result = helper.isEnvExtensionAvailable(); + + expect(result).to.be.true; + expect(useEnvExtensionStub.calledOnce).to.be.true; + }); + + test('promptToInstallPytest should return false if env extension not available', async () => { + useEnvExtensionStub.returns(false); + + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve('Install pytest')) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('promptToInstallPytest should attempt installation when env extension is available', async () => { + useEnvExtensionStub.returns(true); + + const mockEnvironment = { envId: { id: 'test-env', managerId: 'test-manager' } }; + const mockEnvExtApi = { + managePackages: sinon.stub().resolves(), + }; + + getEnvExtApiStub.resolves(mockEnvExtApi); + getEnvironmentStub.resolves(mockEnvironment); + + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.is((msg: string) => msg.includes('pytest selected but not installed')), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve('Install pytest')) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.true; + expect(mockEnvExtApi.managePackages.calledOnceWithExactly(mockEnvironment, { install: ['pytest'] })).to.be.true; + appShell.verifyAll(); + }); +}); diff --git a/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts new file mode 100644 index 000000000000..cf41136db697 --- /dev/null +++ b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { Uri } from 'vscode'; +import { buildErrorNodeOptions } from '../../../../client/testing/testController/common/utils'; + +suite('buildErrorNodeOptions - pytest not installed detection', () => { + const workspaceUri = Uri.file('/test/workspace'); + + test('Should detect pytest ModuleNotFoundError and provide specific message', () => { + const errorMessage = + 'Traceback (most recent call last):\n File "", line 1, in \n import pytest\nModuleNotFoundError: No module named \'pytest\''; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('pytest Not Installed [workspace]'); + expect(result.error).to.equal( + 'pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.', + ); + }); + + test('Should detect pytest ImportError and provide specific message', () => { + const errorMessage = 'ImportError: No module named pytest'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('pytest Not Installed [workspace]'); + expect(result.error).to.equal( + 'pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.', + ); + }); + + test('Should use generic error for non-pytest-related errors', () => { + const errorMessage = 'Some other error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('pytest Discovery Error [workspace]'); + expect(result.error).to.equal('Some other error occurred'); + }); + + test('Should use generic error for unittest errors', () => { + const errorMessage = "ModuleNotFoundError: No module named 'pytest'"; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest'); + + expect(result.label).to.equal('Unittest Discovery Error [workspace]'); + expect(result.error).to.equal("ModuleNotFoundError: No module named 'pytest'"); + }); +}); From efc5101305518c44df21e32fcec09682b5c3574a Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 11 Jul 2025 10:36:14 -0700 Subject: [PATCH 0989/1136] update telemetry events to support envs ext (#25277) fixes for the following events: `execution_code"` `environment.created"` `environment.creating"` `python_interpreter_activation_for_terminal` --- src/client/common/terminal/activator/index.ts | 5 +++++ .../pythonEnvironments/creation/createEnvApi.ts | 17 +++++++++++++++++ src/client/telemetry/index.ts | 2 +- .../codeExecution/codeExecutionManager.ts | 11 +++++++++-- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/client/common/terminal/activator/index.ts b/src/client/common/terminal/activator/index.ts index 6501688b548a..24ffb5008364 100644 --- a/src/client/common/terminal/activator/index.ts +++ b/src/client/common/terminal/activator/index.ts @@ -10,6 +10,8 @@ import { ITerminalActivationHandler, ITerminalActivator, ITerminalHelper, Termin import { BaseTerminalActivator } from './base'; import { inTerminalEnvVarExperiment } from '../../experiments/helpers'; import { useEnvExtension } from '../../../envExt/api.internal'; +import { EventName } from '../../../telemetry/constants'; +import { sendTelemetryEvent } from '../../../telemetry'; @injectable() export class TerminalActivator implements ITerminalActivator { @@ -43,6 +45,9 @@ export class TerminalActivator implements ITerminalActivator { const activateEnvironment = settings.terminal.activateEnvironment && !inTerminalEnvVarExperiment(this.experimentService); if (!activateEnvironment || options?.hideFromUser || useEnvExtension()) { + if (useEnvExtension()) { + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL); + } return false; } diff --git a/src/client/pythonEnvironments/creation/createEnvApi.ts b/src/client/pythonEnvironments/creation/createEnvApi.ts index d585256200d8..899f57728804 100644 --- a/src/client/pythonEnvironments/creation/createEnvApi.ts +++ b/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -72,11 +72,28 @@ export function registerCreateEnvironmentFeatures( ): Promise => { if (useEnvExtension()) { try { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: undefined, + pythonVersion: undefined, + }); const result = await executeCommand( 'python-envs.createAny', options, ); if (result) { + const managerId = result.envId.managerId; + if (managerId === 'ms-python.python:venv') { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'created', + }); + } + if (managerId === 'ms-python.python:conda') { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'conda', + reason: 'created', + }); + } return { path: result.environmentPath.path }; } } catch (err) { diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index a387e45d694a..581fcfed1f63 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -2336,7 +2336,7 @@ export interface IEventNamePropertyMapping { } */ [EventName.ENVIRONMENT_CREATING]: { - environmentType: 'venv' | 'conda' | 'microvenv'; + environmentType: 'venv' | 'conda' | 'microvenv' | undefined; pythonVersion: string | undefined; }; /** diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index d5eb69efba20..740256ca78b3 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -41,6 +41,8 @@ export class CodeExecutionManager implements ICodeExecutionManager { this.disposableRegistry.push( this.commandManager.registerCommand(cmd as any, async (file: Resource) => { traceVerbose(`Attempting to run Python file`, file?.fsPath); + const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; + const newTerminalPerFile = cmd === Commands.Exec_In_Separate_Terminal; if (useEnvExtension()) { try { @@ -51,6 +53,11 @@ export class CodeExecutionManager implements ICodeExecutionManager { sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'run-in-terminal', }); + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { + scope: 'file', + trigger, + newTerminalPerFile, + }); return; } @@ -66,9 +73,9 @@ export class CodeExecutionManager implements ICodeExecutionManager { trigger: 'run-in-terminal', }); triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); - const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; + await this.executeFileInTerminal(file, trigger, { - newTerminalPerFile: cmd === Commands.Exec_In_Separate_Terminal, + newTerminalPerFile, }) .then(() => { if (this.shouldTerminalFocusOnStart(file)) From c9c8b6831b11161374991c5252eb4f3068ba0d74 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:06:09 -0700 Subject: [PATCH 0990/1136] Add comprehensive tests for populateTestTree function (#25273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds comprehensive unit tests for the `populateTestTree` function in the VSCode Python extension's test controller, addressing the need for better test coverage of this critical function. ## Changes Made Added 10 comprehensive test cases to `src/test/testing/testController/utils.unit.test.ts` covering: ### Core Functionality - **Root creation**: Tests that a new root `TestItem` is created when `testRoot` is undefined, with correct properties (id, name, path, tags, canResolveChildren) - **Recursive tree population**: Validates that all children in `testTreeData` are processed recursively with proper hierarchy - **Test item creation**: Ensures leaf nodes are created as `TestItem` objects with correct properties - **Node creation**: Tests non-leaf node creation and reuse of existing nodes ### Edge Cases & Error Handling - **Line number handling**: Tests correct `Range` creation for both zero and non-zero line numbers - **Cancellation support**: Verifies that processing stops when cancellation token is triggered - **Empty children**: Ensures graceful handling of nodes with no children - **Existing node reuse**: Tests that existing nodes are reused instead of creating duplicates ### Data Integrity - **Mapping updates**: Validates that `resultResolver` mappings (`runIdToTestItem`, `runIdToVSid`, `vsIdToRunId`) are updated correctly - **Tag assignment**: Confirms all created items have proper `RunTestTag` and `DebugTestTag` tags ## Test Implementation Details The tests follow existing patterns in the codebase: - Uses **Mocha** test framework with `suite`/`test` TDD structure - Leverages **sinon** for stubbing and mocking VSCode APIs - Uses **assert** for validation - Properly mocks `TestController`, `TestItem`, and `ITestResultResolver` interfaces - Includes setup/teardown for clean test isolation All tests are passing and provide comprehensive coverage of the `populateTestTree` function's behavior, ensuring robustness and maintainability of this critical testing infrastructure component. Fixes #25272. --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- .../testing/testController/common/types.ts | 2 +- .../testing/testController/utils.unit.test.ts | 664 +++++++++++++++++- 2 files changed, 664 insertions(+), 2 deletions(-) diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 282379abdb85..b4d95af6c30e 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -192,7 +192,7 @@ export type DiscoveredTestCommon = { }; export type DiscoveredTestItem = DiscoveredTestCommon & { - lineno: number; + lineno: number | string; runID: string; }; diff --git a/src/test/testing/testController/utils.unit.test.ts b/src/test/testing/testController/utils.unit.test.ts index 4d2af9da3d5a..c6d9a70831a9 100644 --- a/src/test/testing/testController/utils.unit.test.ts +++ b/src/test/testing/testController/utils.unit.test.ts @@ -2,8 +2,15 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import * as fs from 'fs'; import * as path from 'path'; -import { writeTestIdsFile } from '../../../client/testing/testController/common/utils'; +import { CancellationToken, TestController, TestItem, Uri, Range, Position } from 'vscode'; +import { writeTestIdsFile, populateTestTree } from '../../../client/testing/testController/common/utils'; import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { + DiscoveredTestNode, + DiscoveredTestItem, + ITestResultResolver, +} from '../../../client/testing/testController/common/types'; +import { RunTestTag, DebugTestTag } from '../../../client/testing/testController/common/testItemUtilities'; suite('writeTestIdsFile tests', () => { let sandbox: sinon.SinonSandbox; @@ -87,3 +94,658 @@ suite('getTempDir tests', () => { assert.ok(result.startsWith('/xdg/runtime/dir')); }); }); + +suite('populateTestTree tests', () => { + let sandbox: sinon.SinonSandbox; + let testController: TestController; + let resultResolver: ITestResultResolver; + let cancelationToken: CancellationToken; + let createTestItemStub: sinon.SinonStub; + let itemsAddStub: sinon.SinonStub; + let itemsGetStub: sinon.SinonStub; + + setup(() => { + sandbox = sinon.createSandbox(); + + // Create stubs for TestController methods + createTestItemStub = sandbox.stub(); + itemsAddStub = sandbox.stub(); + itemsGetStub = sandbox.stub(); + + // Create mock TestController + testController = { + createTestItem: createTestItemStub, + items: { + add: itemsAddStub, + get: itemsGetStub, + delete: sandbox.stub(), + replace: sandbox.stub(), + forEach: sandbox.stub(), + size: 0, + [Symbol.iterator]: sandbox.stub(), + }, + } as any; + + // Create mock result resolver + resultResolver = { + runIdToTestItem: new Map(), + runIdToVSid: new Map(), + vsIdToRunId: new Map(), + detailedCoverageMap: new Map(), + resolveDiscovery: sandbox.stub(), + resolveExecution: sandbox.stub(), + _resolveDiscovery: sandbox.stub(), + _resolveExecution: sandbox.stub(), + _resolveCoverage: sandbox.stub(), + }; + + // Mock cancellation token + cancelationToken = { + isCancellationRequested: false, + onCancellationRequested: sandbox.stub(), + } as any; + }); + + teardown(() => { + sandbox.restore(); + }); + + test('should create a root node if testRoot is undefined', () => { + // Arrange + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [], + }; + + const mockRootItem: TestItem = { + id: '/test/path/root', + label: 'RootTest', + uri: Uri.file('/test/path/root'), + canResolveChildren: true, + tags: [RunTestTag, DebugTestTag], + children: { + add: sandbox.stub(), + get: sandbox.stub(), + delete: sandbox.stub(), + replace: sandbox.stub(), + forEach: sandbox.stub(), + size: 0, + [Symbol.iterator]: sandbox.stub(), + }, + } as any; + + createTestItemStub.returns(mockRootItem); + + // Act + populateTestTree(testController, testTreeData, undefined, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledOnce); + // Check the args manually - function uses testTreeData.path as id + const call = createTestItemStub.firstCall; + assert.strictEqual(call.args[0], '/test/path/root'); + assert.strictEqual(call.args[1], 'RootTest'); + // Don't check Uri.file since it's complex to compare + assert.ok(itemsAddStub.calledOnceWith(mockRootItem)); + assert.strictEqual(mockRootItem.canResolveChildren, true); + assert.deepStrictEqual(mockRootItem.tags, [RunTestTag, DebugTestTag]); + }); + + test('should recursively add children as TestItems', () => { + // Arrange + // Tree structure: + // RootWorkspaceFolder (folder) + // └── test_example (test) + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootWorkspaceFolder', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const childrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + id: 'root-id', + children: { + add: childrenAddStub, + }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-id', + label: 'test_example', + uri: Uri.file('/test/path/test.py'), + canResolveChildren: false, + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledOnceWith('test-id', 'test_example', sinon.match.any)); + assert.ok(childrenAddStub.calledOnceWith(mockTestItem)); + assert.strictEqual(mockTestItem.canResolveChildren, false); + assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]); + }); + + test('should create TestItem with correct range when lineno is provided', () => { + // Arrange + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 5, + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const mockRootItem: TestItem = { + children: { add: sandbox.stub() }, + } as any; + + const mockTestItem: TestItem = { + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + const expectedRange = new Range(new Position(4, 0), new Position(5, 0)); + assert.deepStrictEqual(mockTestItem.range, expectedRange); + }); + + test('should handle lineno = 0 correctly', () => { + // Arrange + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: '0', + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const mockRootItem: TestItem = { + children: { add: sandbox.stub() }, + } as any; + + const mockTestItem: TestItem = { + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert- if lineno is '0', range should be defined but at the top + const expectedRange = new Range(new Position(0, 0), new Position(0, 0)); + + assert.deepStrictEqual(mockTestItem.range, expectedRange); + }); + + test('should update resultResolver mappings correctly for test items', () => { + // Arrange + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const mockRootItem: TestItem = { + children: { add: sandbox.stub() }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-id', + tags: [], + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.strictEqual(resultResolver.runIdToTestItem.get('run-id-123'), mockTestItem); + assert.strictEqual(resultResolver.runIdToVSid.get('run-id-123'), 'test-id'); + assert.strictEqual(resultResolver.vsIdToRunId.get('test-id'), 'run-id-123'); + }); + + test('should create nodes for non-leaf items and recurse', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── NestedFolder (folder) + // └── nested_test (test) + const nestedTestItem: DiscoveredTestItem = { + path: '/test/path/nested_test.py', + name: 'nested_test', + type_: 'test', + id_: 'nested-test-id', + lineno: 5, + runID: 'nested-run-id', + }; + + const nestedNode: DiscoveredTestNode = { + path: '/test/path/nested', + name: 'NestedFolder', + type_: 'folder', + id_: 'nested-id', + children: [nestedTestItem], + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [nestedNode], + }; + + const rootChildrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub }, + } as any; + + const nestedChildrenAddStub = sandbox.stub(); + const mockNestedNode: TestItem = { + id: 'nested-id', + canResolveChildren: true, + tags: [], + children: { add: nestedChildrenAddStub }, + } as any; + + const mockNestedTestItem: TestItem = { + id: 'nested-test-id', + tags: [], + } as any; + + createTestItemStub.onFirstCall().returns(mockNestedNode); + createTestItemStub.onSecondCall().returns(mockNestedTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + // Should create nested node - uses child.id_ for non-leaf nodes + assert.ok(createTestItemStub.calledWith('nested-id', 'NestedFolder', sinon.match.any)); + assert.ok(rootChildrenAddStub.calledWith(mockNestedNode)); + assert.strictEqual(mockNestedNode.canResolveChildren, true); + assert.deepStrictEqual(mockNestedNode.tags, [RunTestTag, DebugTestTag]); + + // Should create nested test item - uses child.id_ for test items too + assert.ok(createTestItemStub.calledWith('nested-test-id', 'nested_test', sinon.match.any)); + assert.ok(nestedChildrenAddStub.calledWith(mockNestedTestItem)); + }); + + test('should reuse existing nodes when they already exist', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── ExistingFolder (folder, already exists) + // └── test_example (test) + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const nestedNode: DiscoveredTestNode = { + path: '/test/path/existing', + name: 'ExistingFolder', + type_: 'folder', + id_: 'existing-id', + children: [testItem], + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [nestedNode], + }; + + const rootChildrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub }, + } as any; + + const existingChildrenAddStub = sandbox.stub(); + const existingNode: TestItem = { + id: 'existing-id', + children: { add: existingChildrenAddStub }, + } as any; + + const mockTestItem: TestItem = { + tags: [], + } as any; + + // Mock existing node in testController.items + itemsGetStub.withArgs('/test/path/existing').returns(existingNode); + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + // Should not create a new node, should reuse existing one + assert.ok(createTestItemStub.calledOnceWith('test-id', 'test_example', sinon.match.any)); + // Should not create a new node for the existing folder + assert.ok(createTestItemStub.neverCalledWith('existing-id', 'ExistingFolder', sinon.match.any)); + assert.ok(existingChildrenAddStub.calledWith(mockTestItem)); + // Should not add existing node to root children again + assert.ok(rootChildrenAddStub.notCalled); + }); + + test('should respect cancellation token and stop processing', () => { + // Arrange + const testItem1: DiscoveredTestItem = { + path: '/test/path/test1.py', + name: 'test1', + type_: 'test', + id_: 'test1-id', + lineno: 10, + runID: 'run-id-1', + }; + + const testItem2: DiscoveredTestItem = { + path: '/test/path/test2.py', + name: 'test2', + type_: 'test', + id_: 'test2-id', + lineno: 20, + runID: 'run-id-2', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem1, testItem2], + }; + + const rootChildrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub }, + } as any; + + // Set cancellation token to be cancelled + const cancelledToken = { + isCancellationRequested: true, + onCancellationRequested: sandbox.stub(), + } as any; + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelledToken); + + // Assert - no test items should be created when cancelled + assert.ok(createTestItemStub.notCalled); + assert.ok(rootChildrenAddStub.notCalled); + assert.strictEqual(resultResolver.runIdToTestItem.size, 0); + }); + + test('should handle empty children array gracefully', () => { + // Arrange + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [], + }; + + const rootChildrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub }, + } as any; + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert - should complete without errors + assert.ok(createTestItemStub.notCalled); + assert.ok(rootChildrenAddStub.notCalled); + }); + + test('should add correct tags to all created items', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── NestedFolder (folder) + // └── test_example (test) + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const nestedNode: DiscoveredTestNode = { + path: '/test/path/nested', + name: 'NestedFolder', + type_: 'folder', + id_: 'nested-id', + children: [testItem], + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [nestedNode], + }; + + const mockRootItem: TestItem = { + id: 'root-id', + tags: [], + canResolveChildren: true, + children: { add: sandbox.stub() }, + } as any; + + const mockNestedNode: TestItem = { + id: 'nested-id', + tags: [], + canResolveChildren: true, + children: { add: sandbox.stub() }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-id', + tags: [], + canResolveChildren: false, + } as any; + + createTestItemStub.onCall(0).returns(mockRootItem); + createTestItemStub.onCall(1).returns(mockNestedNode); + createTestItemStub.onCall(2).returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, undefined, resultResolver, cancelationToken); + + // Assert - All items should have RunTestTag and DebugTestTag + assert.deepStrictEqual(mockRootItem.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockNestedNode.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]); + }); + test('should handle a test node with no lineno property', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── test_without_lineno (test, no lineno) + const testItem = { + path: '/test/path/test.py', + name: 'test_without_lineno', + type_: 'test', + id_: 'test-no-lineno-id', + runID: 'run-id-no-lineno', + } as DiscoveredTestItem; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const childrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + id: 'root-id', + children: { + add: childrenAddStub, + }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-no-lineno-id', + label: 'test_without_lineno', + uri: Uri.file('/test/path/test.py'), + canResolveChildren: false, + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledOnceWith('test-no-lineno-id', 'test_without_lineno', sinon.match.any)); + assert.ok(childrenAddStub.calledOnceWith(mockTestItem)); + // range is undefined since lineno is not provided + assert.strictEqual(mockTestItem.range, undefined); + assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]); + }); + + test('should handle a node with multiple children', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // ├── test_one (test) + // └── test_two (test) + const testItem1: DiscoveredTestItem = { + path: '/test/path/test1.py', + name: 'test_one', + type_: 'test', + id_: 'test-one-id', + lineno: 3, + runID: 'run-id-one', + }; + const testItem2: DiscoveredTestItem = { + path: '/test/path/test2.py', + name: 'test_two', + type_: 'test', + id_: 'test-two-id', + lineno: 7, + runID: 'run-id-two', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem1, testItem2], + }; + + const childrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + id: 'root-id', + children: { + add: childrenAddStub, + }, + } as any; + + const mockTestItem1: TestItem = { + id: 'test-one-id', + label: 'test_one', + uri: Uri.file('/test/path/test1.py'), + canResolveChildren: false, + tags: [], + range: new Range(new Position(2, 0), new Position(3, 0)), + } as any; + const mockTestItem2: TestItem = { + id: 'test-two-id', + label: 'test_two', + uri: Uri.file('/test/path/test2.py'), + canResolveChildren: false, + tags: [], + range: new Range(new Position(6, 0), new Position(7, 0)), + } as any; + + createTestItemStub.onFirstCall().returns(mockTestItem1); + createTestItemStub.onSecondCall().returns(mockTestItem2); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledWith('test-one-id', 'test_one', sinon.match.any)); + assert.ok(createTestItemStub.calledWith('test-two-id', 'test_two', sinon.match.any)); + // two test items called with mockRootItem's method childrenAddStub + assert.strictEqual(childrenAddStub.callCount, 2); + assert.deepStrictEqual(mockTestItem1.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockTestItem2.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockTestItem1.range, new Range(new Position(2, 0), new Position(3, 0))); + assert.deepStrictEqual(mockTestItem2.range, new Range(new Position(6, 0), new Position(7, 0))); + }); +}); From aff035a371ac519a80c398064322c9fa9c52a4f9 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:52:26 -0700 Subject: [PATCH 0991/1136] Return early in activate from env var collection (#25286) Resolves: https://github.com/microsoft/vscode-python/issues/25275 https://github.com/microsoft/vscode-python/issues/25284 It seems like python env var experiment is interfering when env extension is being used. We should not implicit activate with env var experiment if environment extension is installed and being used. --------- Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- src/client/terminals/envCollectionActivation/service.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts index bd2ce1c6f717..2ce8d5d5d86a 100644 --- a/src/client/terminals/envCollectionActivation/service.ts +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -97,6 +97,14 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ public async activate(resource: Resource): Promise { try { + if (useEnvExtension()) { + traceVerbose('Ignoring environment variable experiment since env extension is being used'); + this.context.environmentVariableCollection.clear(); + // Needed for shell integration + await registerPythonStartup(this.context); + return; + } + if (!inTerminalEnvVarExperiment(this.experimentService)) { this.context.environmentVariableCollection.clear(); await this.handleMicroVenv(resource); From 97741300fb96c106cab57a3d53dfab0abf158efb Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:08:46 -0700 Subject: [PATCH 0992/1136] fix: use latest pet (#25287) --- build/azure-pipeline.stable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 11de6b0806d4..37c63cf067fb 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -128,7 +128,7 @@ extends: project: 'Monaco' definition: 593 buildVersionToDownload: 'latestFromBranch' - branchName: 'refs/heads/release/2025.8' + branchName: 'refs/heads/release/2025.10' targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' artifactName: 'bin-$(buildTarget)' itemPattern: | From ba56b217b2c6e24aba02defd2c142f12489f4bf3 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:07:43 -0700 Subject: [PATCH 0993/1136] Resolve REPL regression on indentation, disable PyREPL only when shell integration is enabled (#25296) Resolves: https://github.com/microsoft/vscode-python/issues/25295 https://github.com/microsoft/vscode-python/issues/25240 https://github.com/microsoft/vscode-python/issues/25242 --- package.nls.json | 2 +- python_files/pythonrc.py | 3 +-- src/client/common/configSettings.ts | 4 +++- src/client/common/types.ts | 4 +++- src/client/extensionActivation.ts | 3 +-- src/client/terminals/codeExecution/helper.ts | 12 ++++++++++++ src/client/terminals/pythonStartup.ts | 8 +++----- .../terminals/shellIntegration/pythonStartup.test.ts | 7 ++++--- 8 files changed, 28 insertions(+), 15 deletions(-) diff --git a/package.nls.json b/package.nls.json index 37a9ce435f2f..57f2ed95b2c0 100644 --- a/package.nls.json +++ b/package.nls.json @@ -73,7 +73,7 @@ "python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.", "python.tensorBoard.logDirectory.markdownDeprecationMessage": "Tensorboard support has been moved to the extension [Tensorboard extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.tensorboard). Instead use the setting `tensorBoard.logDirectory`.", "python.tensorBoard.logDirectory.deprecationMessage": "Tensorboard support has been moved to the extension Tensorboard extension. Instead use the setting `tensorBoard.logDirectory`.", - "python.terminal.shellIntegration.enabled.description": "Enable [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) for the terminals running python. Shell integration enhances the terminal experience by enabling command decorations, run recent command, improving accessibility among other things.", + "python.terminal.shellIntegration.enabled.description": "Enable [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) for the terminals running python. Shell integration enhances the terminal experience by enabling command decorations, run recent command, improving accessibility among other things. Note: PyREPL (available in Python 3.13+) is automatically disabled when shell integration is enabled to avoid cursor indentation issues.", "python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.", "python.terminal.activateEnvironment.description": "Activate Python Environment in all Terminals created.", "python.terminal.executeInFileDir.description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.", diff --git a/python_files/pythonrc.py b/python_files/pythonrc.py index afd32520cf01..005b06bcdd15 100644 --- a/python_files/pythonrc.py +++ b/python_files/pythonrc.py @@ -5,7 +5,6 @@ import readline original_ps1 = ">>> " -use_shell_integration = sys.version_info < (3, 13) is_wsl = "microsoft-standard-WSL" in platform.release() @@ -75,7 +74,7 @@ def __str__(self): return result -if sys.platform != "win32" and (not is_wsl) and use_shell_integration: +if sys.platform != "win32" and (not is_wsl): sys.ps1 = PS1() if sys.platform == "darwin": diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 634e0106fe7b..91c06d9331fd 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -377,7 +377,9 @@ export class PythonSettings implements IPythonSettings { launchArgs: [], activateEnvironment: true, activateEnvInCurrentTerminal: false, - enableShellIntegration: false, + shellIntegration: { + enabled: false, + }, }; this.REPL = pythonSettings.get('REPL')!; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 2cb393d89bdf..c30ad704b6c1 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -188,7 +188,9 @@ export interface ITerminalSettings { readonly launchArgs: string[]; readonly activateEnvironment: boolean; readonly activateEnvInCurrentTerminal: boolean; - readonly enableShellIntegration: boolean; + readonly shellIntegration: { + enabled: boolean; + }; } export interface IREPLSettings { diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 8fae9d5131ff..8330d5010f7a 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -53,7 +53,7 @@ import { DebuggerTypeName } from './debugger/constants'; import { StopWatch } from './common/utils/stopWatch'; import { registerReplCommands, registerReplExecuteOnEnter, registerStartNativeReplCommand } from './repl/replCommands'; import { registerTriggerForTerminalREPL } from './terminals/codeExecution/terminalReplWatcher'; -import { registerBasicRepl, registerPythonStartup } from './terminals/pythonStartup'; +import { registerPythonStartup } from './terminals/pythonStartup'; import { registerPixiFeatures } from './pythonEnvironments/common/environmentManagers/pixi'; import { registerCustomTerminalLinkProvider } from './terminals/pythonStartupLinkProvider'; import { registerEnvExtFeatures } from './envExt/api.internal'; @@ -184,7 +184,6 @@ async function activateLegacy(ext: ExtensionState, startupStopWatch: StopWatch): serviceManager.get(ITerminalAutoActivation).register(); await registerPythonStartup(ext.context); - await registerBasicRepl(ext.context); serviceManager.get(ICodeExecutionManager).registerCommands(); diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index 26ebe35aae43..4efad5ee174e 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -99,10 +99,12 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const endLineVal = activeEditor?.selection?.end.line ?? 0; const emptyHighlightVal = activeEditor?.selection?.isEmpty ?? true; let smartSendSettingsEnabledVal = true; + let shellIntegrationEnabled = false; const configuration = this.serviceContainer.get(IConfigurationService); if (configuration) { const pythonSettings = configuration.getSettings(this.activeResourceService.getActiveResource()); smartSendSettingsEnabledVal = pythonSettings.REPL.enableREPLSmartSend; + shellIntegrationEnabled = pythonSettings.terminal.shellIntegration.enabled; } const input = JSON.stringify({ @@ -125,6 +127,16 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { await this.moveToNextBlock(lineOffset, activeEditor); } + // For new _pyrepl for Python3.13+ && !shellIntegration, we need to send code via bracketed paste mode. + if (object.attach_bracket_paste && !shellIntegrationEnabled && _replType === ReplType.terminal) { + let trimmedNormalized = object.normalized.replace(/\n$/, ''); + if (trimmedNormalized.endsWith(':\n')) { + // In case where statement is unfinished via :, truncate so auto-indentation lands nicely. + trimmedNormalized = trimmedNormalized.replace(/\n$/, ''); + } + return `\u001b[200~${trimmedNormalized}\u001b[201~`; + } + return parse(object.normalized); } catch (ex) { traceError(ex, 'Python: Failed to normalize code for execution in terminal'); diff --git a/src/client/terminals/pythonStartup.ts b/src/client/terminals/pythonStartup.ts index 1a2576dce772..f0c3bf89c3b4 100644 --- a/src/client/terminals/pythonStartup.ts +++ b/src/client/terminals/pythonStartup.ts @@ -21,8 +21,11 @@ async function applyPythonStartupSetting(context: ExtensionContext): Promise { - // TODO: Configurable by setting - context.environmentVariableCollection.replace('PYTHON_BASIC_REPL', '1'); -} diff --git a/src/test/terminals/shellIntegration/pythonStartup.test.ts b/src/test/terminals/shellIntegration/pythonStartup.test.ts index 3c755adf0d9b..833a4f29e972 100644 --- a/src/test/terminals/shellIntegration/pythonStartup.test.ts +++ b/src/test/terminals/shellIntegration/pythonStartup.test.ts @@ -16,7 +16,7 @@ import { } from 'vscode'; import { assert } from 'chai'; import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; -import { registerBasicRepl, registerPythonStartup } from '../../../client/terminals/pythonStartup'; +import { registerPythonStartup } from '../../../client/terminals/pythonStartup'; import { IExtensionContext } from '../../../client/common/types'; import * as pythonStartupLinkProvider from '../../../client/terminals/pythonStartupLinkProvider'; import { CustomTerminalLinkProvider } from '../../../client/terminals/pythonStartupLinkProvider'; @@ -135,8 +135,9 @@ suite('Terminal - Shell Integration with PYTHONSTARTUP', () => { globalEnvironmentVariableCollection.verify((c) => c.delete('PYTHONSTARTUP'), TypeMoq.Times.once()); }); - test('PYTHON_BASIC_REPL is set when registerBasicRepl is called', async () => { - await registerBasicRepl(context.object); + test('PYTHON_BASIC_REPL is set when shell integration is enabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + await registerPythonStartup(context.object); globalEnvironmentVariableCollection.verify( (c) => c.replace('PYTHON_BASIC_REPL', '1', TypeMoq.It.isAny()), TypeMoq.Times.once(), From cce890fd47c2a3e9c2235097d0a7c932ab9fc73d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 09:15:33 -0700 Subject: [PATCH 0994/1136] Bump microvenv from 2023.5.post1 to 2025.0 (#25307) [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=microvenv&package-manager=pip&previous-version=2023.5.post1&new-version=2025.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index e3563501e309..2e6b6ee07783 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +4,9 @@ importlib-metadata==8.7.0 \ --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd # via -r requirements.in -microvenv==2023.5.post1 \ - --hash=sha256:32c46afea874e300f69f1add0806eb0795fd02b5fb251092fba0b73c059a7d1f \ - --hash=sha256:fd79b3dfea7860e2e84c87dd0aa8a135075f7fa2284174842b7bdeb077a0d8ac +microvenv==2025.0 \ + --hash=sha256:568155ec18af01c89f270d35d123ab803b09672b480c3702d15fd69e9cc5bd1e \ + --hash=sha256:8a2568a8390a4ffb5af2f05e7642454e03b887e582d192b6316326974eab5d0f # via -r requirements.in packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ From 583f2d3229561d2cd0f9fb38772595b15e276562 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 21 Jul 2025 09:38:15 -0700 Subject: [PATCH 0995/1136] bundle envs ext with stable (#25309) reverts https://github.com/microsoft/vscode-python/commit/8c3a49fe6ec4aabef26f10feda9452aa259811bc which switched to only bundling with pre-release so now it is bundled for stable and pre-release --- .github/actions/build-vsix/action.yml | 9 +-------- build/azure-pipeline.pre-release.yml | 2 +- gulpfile.js | 19 ++++++------------- package.json | 1 - 4 files changed, 8 insertions(+), 23 deletions(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index 1922f6196ee5..eaabe5141e8b 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -70,15 +70,8 @@ runs: shell: bash - name: Update optional extension dependencies - run: | - if [[ "${VSIX_NAME}" == *"insiders"* ]]; then - npm run addExtensionPackDependenciesPreRelease - else - npm run addExtensionPackDependencies - fi + run: npm run addExtensionPackDependencies shell: bash - env: - VSIX_NAME: ${{ inputs.vsix_name }} - name: Build Webpack run: | diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index 82c991189a82..ab087673f1e7 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -91,7 +91,7 @@ extends: - script: python ./build/update_package_file.py displayName: Update telemetry in package.json - - script: npm run addExtensionPackDependenciesPreRelease + - script: npm run addExtensionPackDependencies displayName: Update optional extension dependencies - script: npx gulp prePublishBundle diff --git a/gulpfile.js b/gulpfile.js index 3a03332c4f5c..0b919f16572a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -93,23 +93,16 @@ gulp.task('addExtensionPackDependencies', async () => { await addExtensionPackDependencies(); }); -// This task adds 'ms-python.vscode-python-envs' as required deps only for pre-release builds. -gulp.task('addExtensionPackDependenciesPreRelease', async () => { - await buildLicense(); - await addExtensionPackDependencies(true); -}); - -async function addExtensionPackDependencies(isPreRelease = false) { +async function addExtensionPackDependencies() { // Update the package.json to add extension pack dependencies at build time so that // extension dependencies need not be installed during development const packageJsonContents = await fsExtra.readFile('package.json', 'utf-8'); const packageJson = JSON.parse(packageJsonContents); - let deps = ['ms-python.vscode-pylance', 'ms-python.debugpy']; - if (isPreRelease) { - deps.push('ms-python.vscode-python-envs'); - } - packageJson.extensionPack = deps.concat(packageJson.extensionPack ? packageJson.extensionPack : []); - + packageJson.extensionPack = [ + 'ms-python.vscode-pylance', + 'ms-python.debugpy', + 'ms-python.vscode-python-envs', + ].concat(packageJson.extensionPack ? packageJson.extensionPack : []); // Remove potential duplicates. packageJson.extensionPack = packageJson.extensionPack.filter( (item, index) => packageJson.extensionPack.indexOf(item) === index, diff --git a/package.json b/package.json index b407c68caa35..226b52093b25 100644 --- a/package.json +++ b/package.json @@ -1689,7 +1689,6 @@ "format-fix": "prettier --write 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", "clean": "gulp clean", "addExtensionPackDependencies": "gulp addExtensionPackDependencies", - "addExtensionPackDependenciesPreRelease": "gulp addExtensionPackDependenciesPreRelease", "updateBuildNumber": "gulp updateBuildNumber", "verifyBundle": "gulp verifyBundle", "webpack": "webpack" From 4f0272543f2f509fc964a12edec71ad297985dd5 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 25 Jul 2025 09:49:46 +1000 Subject: [PATCH 0996/1136] Pompt model to use tool before running any Python command in terminal (#25321) For https://github.com/microsoft/vscode/issues/256114 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 226b52093b25..66eceab34d69 100644 --- a/package.json +++ b/package.json @@ -1585,7 +1585,7 @@ { "name": "configure_python_environment", "displayName": "Configure Python Environment", - "modelDescription": "This tool configures a Python environment in the given workspace. ALWAYS Use this tool to set up the user's chosen environment and ALWAYS call this tool before using any other Python related tools.", + "modelDescription": "This tool configures a Python environment in the given workspace. ALWAYS Use this tool to set up the user's chosen environment and ALWAYS call this tool before using any other Python related tools or running any Python command in the terminal.", "userDescription": "%python.languageModelTools.configure_python_environment.userDescription%", "toolReferenceName": "configurePythonEnvironment", "tags": [ From 3a729b4addafb44ffb874c721fd22565e183c310 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 18:27:17 +0000 Subject: [PATCH 0997/1136] Bump form-data (#25312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps and [form-data](https://github.com/form-data/form-data). These dependencies needed to be updated together. Updates `form-data` from 4.0.0 to 4.0.4
Release notes

Sourced from form-data's releases.

v4.0.1

Fixes

  • npmignore temporary build files (#532)
  • move util.isArray to Array.isArray (#564)

Tests

  • migrate from travis to GHA
Changelog

Sourced from form-data's changelog.

v4.0.4 - 2025-07-16

Commits

  • [meta] add auto-changelog 811f682
  • [Tests] handle predict-v8-randomness failures in node < 17 and node > 23 1d11a76
  • [Fix] Switch to using crypto random for boundary values 3d17230
  • [Tests] fix linting errors 5e34080
  • [meta] actually ensure the readme backup isn’t published 316c82b
  • [Dev Deps] update @ljharb/eslint-config 58c25d7
  • [meta] fix readme capitalization 2300ca1

v4.0.3 - 2025-06-05

Fixed

Commits

  • [eslint] use a shared config 426ba9a
  • [eslint] fix some spacing issues 2094191
  • [Refactor] use hasown 81ab41b
  • [Fix] validate boundary type in setBoundary() method 8d8e469
  • [Tests] add tests to check the behavior of getBoundary with non-strings 837b8a1
  • [Dev Deps] remove unused deps 870e4e6
  • [meta] remove local commit hooks e6e83cc
  • [Dev Deps] update eslint 4066fd6
  • [meta] fix scripts to use prepublishOnly c4bbb13

v4.0.2 - 2025-02-14

Merged

Fixed

Commits

  • Merge tags v2.5.3 and v3.0.3 92613b9
  • [Tests] migrate from travis to GHA 806eda7
  • [Tests] migrate from travis to GHA 8fdb3bc

... (truncated)

Commits
  • 41996f5 v4.0.4
  • 316c82b [meta] actually ensure the readme backup isn’t published
  • 2300ca1 [meta] fix readme capitalization
  • 811f682 [meta] add auto-changelog
  • 5e34080 [Tests] fix linting errors
  • 1d11a76 [Tests] handle predict-v8-randomness failures in node < 17 and node > 23
  • 58c25d7 [Dev Deps] update @ljharb/eslint-config
  • 3d17230 [Fix] Switch to using crypto random for boundary values
  • d8d67dc v4.0.3
  • e6e83cc [meta] remove local commit hooks
  • Additional commits viewable in compare view
Maintainer changes

This version was pushed to npm by ljharb, a new releaser for form-data since your current version.


Updates `form-data` from 2.5.1 to 4.0.4
Release notes

Sourced from form-data's releases.

v4.0.1

Fixes

  • npmignore temporary build files (#532)
  • move util.isArray to Array.isArray (#564)

Tests

  • migrate from travis to GHA
Changelog

Sourced from form-data's changelog.

v4.0.4 - 2025-07-16

Commits

  • [meta] add auto-changelog 811f682
  • [Tests] handle predict-v8-randomness failures in node < 17 and node > 23 1d11a76
  • [Fix] Switch to using crypto random for boundary values 3d17230
  • [Tests] fix linting errors 5e34080
  • [meta] actually ensure the readme backup isn’t published 316c82b
  • [Dev Deps] update @ljharb/eslint-config 58c25d7
  • [meta] fix readme capitalization 2300ca1

v4.0.3 - 2025-06-05

Fixed

Commits

  • [eslint] use a shared config 426ba9a
  • [eslint] fix some spacing issues 2094191
  • [Refactor] use hasown 81ab41b
  • [Fix] validate boundary type in setBoundary() method 8d8e469
  • [Tests] add tests to check the behavior of getBoundary with non-strings 837b8a1
  • [Dev Deps] remove unused deps 870e4e6
  • [meta] remove local commit hooks e6e83cc
  • [Dev Deps] update eslint 4066fd6
  • [meta] fix scripts to use prepublishOnly c4bbb13

v4.0.2 - 2025-02-14

Merged

Fixed

Commits

  • Merge tags v2.5.3 and v3.0.3 92613b9
  • [Tests] migrate from travis to GHA 806eda7
  • [Tests] migrate from travis to GHA 8fdb3bc

... (truncated)

Commits
  • 41996f5 v4.0.4
  • 316c82b [meta] actually ensure the readme backup isn’t published
  • 2300ca1 [meta] fix readme capitalization
  • 811f682 [meta] add auto-changelog
  • 5e34080 [Tests] fix linting errors
  • 1d11a76 [Tests] handle predict-v8-randomness failures in node < 17 and node > 23
  • 58c25d7 [Dev Deps] update @ljharb/eslint-config
  • 3d17230 [Fix] Switch to using crypto random for boundary values
  • d8d67dc v4.0.3
  • e6e83cc [meta] remove local commit hooks
  • Additional commits viewable in compare view
Maintainer changes

This version was pushed to npm by ljharb, a new releaser for form-data since your current version.


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 124 ++++++++++++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index a5805f351472..99f39a10d6f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1818,19 +1818,42 @@ } }, "node_modules/@types/got/node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", "dev": true, "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" }, "engines": { "node": ">= 0.12" } }, + "node_modules/@types/got/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3822,7 +3845,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5301,7 +5323,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5533,7 +5554,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5543,7 +5563,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -5558,7 +5577,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5568,14 +5586,14 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -6961,12 +6979,14 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -7135,7 +7155,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -7181,7 +7200,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -7434,7 +7452,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7958,7 +7975,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7984,7 +8000,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -9772,7 +9787,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -16535,15 +16549,24 @@ }, "dependencies": { "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", "dev": true, "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true } } }, @@ -18055,7 +18078,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "requires": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -19174,7 +19196,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -19375,14 +19396,12 @@ "es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, "es-module-lexer": { "version": "1.5.4", @@ -19394,20 +19413,19 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "requires": { "es-errors": "^1.3.0" } }, "es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "requires": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" } }, "es-shim-unscopables": { @@ -20427,12 +20445,14 @@ } }, "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, @@ -20560,7 +20580,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -20590,7 +20609,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "requires": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -20790,8 +20808,7 @@ "gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "got": { "version": "8.3.2", @@ -21185,8 +21202,7 @@ "has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-to-string-tag-x": { "version": "1.4.1", @@ -21201,7 +21217,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "requires": { "has-symbols": "^1.0.3" } @@ -22535,8 +22550,7 @@ "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" }, "md5": { "version": "2.3.0", From 6827afb50ab5326d051c7c50e39dd26fc2c0e15a Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 28 Jul 2025 14:01:50 -0700 Subject: [PATCH 0998/1136] add workspace folder for debugpy launch.json config (#25338) fixes https://github.com/microsoft/vscode-python-environments/issues/645 --- src/client/interpreter/interpreterPathCommand.ts | 12 ++++++++++-- .../interpreters/interpreterPathCommand.unit.test.ts | 10 +++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/client/interpreter/interpreterPathCommand.ts b/src/client/interpreter/interpreterPathCommand.ts index 0dce208c9bfc..12f6756dafeb 100644 --- a/src/client/interpreter/interpreterPathCommand.ts +++ b/src/client/interpreter/interpreterPathCommand.ts @@ -4,12 +4,13 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; +import { Uri, workspace } from 'vscode'; import { IExtensionSingleActivationService } from '../activation/types'; import { Commands } from '../common/constants'; import { IDisposable, IDisposableRegistry } from '../common/types'; import { registerCommand } from '../common/vscodeApis/commandApis'; import { IInterpreterService } from './contracts'; +import { useEnvExtension } from '../envExt/api.internal'; @injectable() export class InterpreterPathCommand implements IExtensionSingleActivationService { @@ -26,7 +27,9 @@ export class InterpreterPathCommand implements IExtensionSingleActivationService ); } - public async _getSelectedInterpreterPath(args: { workspaceFolder: string } | string[]): Promise { + public async _getSelectedInterpreterPath( + args: { workspaceFolder: string; type: string } | string[], + ): Promise { // If `launch.json` is launching this command, `args.workspaceFolder` carries the workspaceFolder // If `tasks.json` is launching this command, `args[1]` carries the workspaceFolder let workspaceFolder; @@ -35,6 +38,11 @@ export class InterpreterPathCommand implements IExtensionSingleActivationService } else if (args[1]) { const [, second] = args; workspaceFolder = second; + } else if (useEnvExtension() && 'type' in args && args.type === 'debugpy') { + // If using the envsExt and the type is debugpy, we need to add the workspace folder to get the interpreter path. + if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + workspaceFolder = workspace.workspaceFolders[0].uri.fsPath; + } } else { workspaceFolder = undefined; } diff --git a/src/test/interpreters/interpreterPathCommand.unit.test.ts b/src/test/interpreters/interpreterPathCommand.unit.test.ts index be94be654861..8d45ad82577c 100644 --- a/src/test/interpreters/interpreterPathCommand.unit.test.ts +++ b/src/test/interpreters/interpreterPathCommand.unit.test.ts @@ -13,15 +13,19 @@ import * as commandApis from '../../client/common/vscodeApis/commandApis'; import { InterpreterPathCommand } from '../../client/interpreter/interpreterPathCommand'; import { IInterpreterService } from '../../client/interpreter/contracts'; import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +import * as workspaceApis from '../../client/common/vscodeApis/workspaceApis'; suite('Interpreter Path Command', () => { let interpreterService: IInterpreterService; let interpreterPathCommand: InterpreterPathCommand; let registerCommandStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + setup(() => { interpreterService = mock(); registerCommandStub = sinon.stub(commandApis, 'registerCommand'); interpreterPathCommand = new InterpreterPathCommand(instance(interpreterService), []); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); }); teardown(() => { @@ -43,7 +47,7 @@ suite('Interpreter Path Command', () => { }); test('If `workspaceFolder` property exists in `args`, it is used to retrieve setting from config', async () => { - const args = { workspaceFolder: 'folderPath' }; + const args = { workspaceFolder: 'folderPath', type: 'debugpy' }; when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { assert.deepEqual(arg, Uri.file('folderPath')); @@ -76,6 +80,10 @@ suite('Interpreter Path Command', () => { }); test('If neither of these exists, value of workspace folder is `undefined`', async () => { + getConfigurationStub.withArgs('python').returns({ + get: sinon.stub().returns(false), + }); + const args = ['command']; when(interpreterService.getActiveInterpreter(undefined)).thenReturn( From 3fb2381a3f4593cb9ee26e14f90de76e962aca54 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:34:11 -0700 Subject: [PATCH 0999/1136] bump version 2025.12.0 (#25354) --- build/azure-pipeline.stable.yml | 40 ++++++++++++++++----------------- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 37c63cf067fb..c68dacc7db80 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -92,8 +92,8 @@ extends: displayName: Build - bash: | - mkdir -p $(Build.SourcesDirectory)/python-env-tools/bin - chmod +x $(Build.SourcesDirectory)/python-env-tools/bin + mkdir -p $(Build.SourcesDirectory)/python-env-tools/bin + chmod +x $(Build.SourcesDirectory)/python-env-tools/bin displayName: Make Directory for python-env-tool binary - bash: | @@ -124,30 +124,30 @@ extends: - task: DownloadPipelineArtifact@2 inputs: - buildType: 'specific' - project: 'Monaco' - definition: 593 - buildVersionToDownload: 'latestFromBranch' - branchName: 'refs/heads/release/2025.10' - targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' - artifactName: 'bin-$(buildTarget)' - itemPattern: | - pet.exe - pet - ThirdPartyNotices.txt + buildType: 'specific' + project: 'Monaco' + definition: 593 + buildVersionToDownload: 'latestFromBranch' + branchName: 'refs/heads/release/2025.12' + targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' + artifactName: 'bin-$(buildTarget)' + itemPattern: | + pet.exe + pet + ThirdPartyNotices.txt - bash: | - ls -lf ./python-env-tools/bin - chmod +x ./python-env-tools/bin/pet* - ls -lf ./python-env-tools/bin + ls -lf ./python-env-tools/bin + chmod +x ./python-env-tools/bin/pet* + ls -lf ./python-env-tools/bin displayName: Set chmod for pet binary - script: python -c "import shutil; shutil.rmtree('.nox', ignore_errors=True)" displayName: Clean up Nox tsa: - config: - areaPath: 'Visual Studio Code Python Extensions' - serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' - enabled: true + config: + areaPath: 'Visual Studio Code Python Extensions' + serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' + enabled: true apiScanDependentPipelineId: '593' # python-environment-tools apiScanSoftwareVersion: '2024' diff --git a/package-lock.json b/package-lock.json index 99f39a10d6f4..4345047b8e3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.11.0-dev", + "version": "2025.12.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.11.0-dev", + "version": "2025.12.0", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 66eceab34d69..2f138943d9e0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.11.0-dev", + "version": "2025.12.0", "featureFlags": { "usingNewInterpreterStorage": true }, From 78b5d75debb5918074b622213cd9ba878c6437e8 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 5 Aug 2025 14:18:52 -0700 Subject: [PATCH 1000/1136] bump to 2025.13.0-dev (#25355) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4345047b8e3b..86e84f48b311 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.12.0", + "version": "2025.13.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.12.0", + "version": "2025.13.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 2f138943d9e0..961ba8263195 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.12.0", + "version": "2025.13.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From f3f22937ff73c37c1a4c8d60c158161103051790 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Wed, 6 Aug 2025 12:58:03 -0700 Subject: [PATCH 1001/1136] Default Python shell integration to true (#25359) Resolves: https://github.com/microsoft/vscode-python/issues/24141 /cc @cwebster-99 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 961ba8263195..c6d1595ac181 100644 --- a/package.json +++ b/package.json @@ -640,7 +640,7 @@ "type": "array" }, "python.terminal.shellIntegration.enabled": { - "default": false, + "default": true, "markdownDescription": "%python.terminal.shellIntegration.enabled.description%", "scope": "resource", "type": "boolean", From fec1b06ec8e2d98469861ceac9a86aaaaf244cb2 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 7 Aug 2025 10:53:12 -0700 Subject: [PATCH 1002/1136] Add verbose logging for conda activation commands and service methods (#25365) to assist with transition to python-environments extension, add in verbose logging to help track existing steps while debugging --- .../condaActivationProvider.ts | 26 ++++++++++++++++--- .../environmentManagers/condaService.ts | 5 ++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts index d209550e04a4..42bb8f38fc9e 100644 --- a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts @@ -8,6 +8,7 @@ import '../../extensions'; import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; +import { traceInfo, traceVerbose, traceWarn } from '../../../logging'; import { IComponentAdapter, ICondaService } from '../../../interpreter/contracts'; import { IPlatformService } from '../../platform/types'; @@ -53,16 +54,21 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman pythonPath: string, targetShell: TerminalShellType, ): Promise { + traceVerbose(`Getting conda activation commands for interpreter ${pythonPath} with shell ${targetShell}`); const envInfo = await this.pyenvs.getCondaEnvironment(pythonPath); if (!envInfo) { + traceWarn(`No conda environment found for interpreter ${pythonPath}`); return undefined; } + traceVerbose(`Found conda environment: ${JSON.stringify(envInfo)}`); const condaEnv = envInfo.name.length > 0 ? envInfo.name : envInfo.path; // New version. const interpreterPath = await this.condaService.getInterpreterPathForEnvironment(envInfo); + traceInfo(`Using interpreter path: ${interpreterPath}`); const activatePath = await this.condaService.getActivationScriptFromInterpreter(interpreterPath, envInfo.name); + traceVerbose(`Got activation script: ${activatePath?.path}} with type: ${activatePath?.type}`); // eslint-disable-next-line camelcase if (activatePath?.path) { if ( @@ -70,11 +76,14 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman targetShell !== TerminalShellType.bash && targetShell !== TerminalShellType.gitbash ) { - return [activatePath.path, `conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; + const commands = [activatePath.path, `conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; + traceInfo(`Using Windows-specific commands: ${commands.join(', ')}`); + return commands; } const condaInfo = await this.condaService.getCondaInfo(); + traceVerbose(`Conda shell level: ${condaInfo?.conda_shlvl}`); if ( activatePath.type !== 'global' || // eslint-disable-next-line camelcase @@ -84,27 +93,36 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman // activatePath is not the global activate path, or we don't have a shlvl, or it's -1(conda never sourced). // and we need to source the activate path. if (activatePath.path === 'activate') { - return [ + const commands = [ `source ${activatePath.path}`, `conda activate ${condaEnv.toCommandArgumentForPythonExt()}`, ]; + traceInfo(`Using source activate commands: ${commands.join(', ')}`); + return commands; } - return [`source ${activatePath.path} ${condaEnv.toCommandArgumentForPythonExt()}`]; + const command = [`source ${activatePath.path} ${condaEnv.toCommandArgumentForPythonExt()}`]; + traceInfo(`Using single source command: ${command}`); + return command; } - return [`conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; + const command = [`conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; + traceInfo(`Using direct conda activate command: ${command}`); + return command; } switch (targetShell) { case TerminalShellType.powershell: case TerminalShellType.powershellCore: + traceVerbose('Using PowerShell-specific activation'); return _getPowershellCommands(condaEnv); // TODO: Do we really special-case fish on Windows? case TerminalShellType.fish: + traceVerbose('Using Fish shell-specific activation'); return getFishCommands(condaEnv, await this.condaService.getCondaFile()); default: if (this.platform.isWindows) { + traceVerbose('Using Windows shell-specific activation fallback option.'); return this.getWindowsCommands(condaEnv); } return getUnixCommands(condaEnv, await this.condaService.getCondaFile()); diff --git a/src/client/pythonEnvironments/common/environmentManagers/condaService.ts b/src/client/pythonEnvironments/common/environmentManagers/condaService.ts index 0739993dad37..0aa91bdbfb45 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/condaService.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/condaService.ts @@ -2,6 +2,7 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { SemVer } from 'semver'; import { IFileSystem, IPlatformService } from '../../../common/platform/types'; +import { traceVerbose } from '../../../logging'; import { cache } from '../../../common/utils/decorators'; import { ICondaService } from '../../../interpreter/contracts'; import { traceDecoratorVerbose } from '../../../logging'; @@ -23,12 +24,15 @@ export class CondaService implements ICondaService { interpreterPath?: string, envName?: string, ): Promise<{ path: string | undefined; type: 'local' | 'global' } | undefined> { + traceVerbose(`Getting activation script for interpreter ${interpreterPath}, env ${envName}`); const condaPath = await this.getCondaFileFromInterpreter(interpreterPath, envName); + traceVerbose(`Found conda path: ${condaPath}`); const activatePath = (condaPath ? path.join(path.dirname(condaPath), 'activate') : 'activate' ).fileToCommandArgumentForPythonExt(); // maybe global activate? + traceVerbose(`Using activate path: ${activatePath}`); // try to find the activate script in the global conda root prefix. if (this.platform.isLinux || this.platform.isMac) { @@ -41,6 +45,7 @@ export class CondaService implements ICondaService { .fileToCommandArgumentForPythonExt(); if (activatePath === globalActivatePath || !(await this.fileSystem.fileExists(activatePath))) { + traceVerbose(`Using global activate path: ${globalActivatePath}`); return { path: globalActivatePath, type: 'global', From 11fa35dcc741da1fa0610bae6afc301a7efb2085 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 7 Aug 2025 11:01:35 -0700 Subject: [PATCH 1003/1136] Add onTerminalShellIntegration:python (#25364) Resolves: https://github.com/microsoft/vscode-python/issues/25363 --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c6d1595ac181..f113890ad97d 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,8 @@ "onLanguageModelTool:get_python_executable_details", "onLanguageModelTool:install_python_packages", "onLanguageModelTool:configure_python_environment", - "onLanguageModelTool:create_virtual_environment" + "onLanguageModelTool:create_virtual_environment", + "onTerminalShellIntegration:python" ], "main": "./out/client/extension", "browser": "./dist/extension.browser.js", From fffb30f6d8bebb51758b62fa3a76ef31b783344a Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:49:00 -0700 Subject: [PATCH 1004/1136] update to node 22 and fix tests (#25379) update to use node 22 update to use a higher version of vscode in the python api fix tests which began failing with node 22 --- .github/workflows/build.yml | 2 +- .github/workflows/pr-check.yml | 2 +- build/azure-pipeline.pre-release.yml | 2 +- build/azure-pipeline.stable.yml | 2 +- build/azure-pipelines/pipeline.yml | 6 +-- pythonExtensionApi/package-lock.json | 2 +- pythonExtensionApi/package.json | 4 +- src/client/common/process/logger.ts | 7 ++++ src/test/common/process/logger.unit.test.ts | 43 ++++++++++++++++----- 9 files changed, 50 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca75f6ef7727..16f398f5a166 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: permissions: {} env: - NODE_VERSION: 20.18.1 + NODE_VERSION: 22.17.0 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 # Force a path with spaces and to test extension works in these scenarios # Unicode characters are causing 2.7 failures so skip that for now. diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 65e80e1f6280..368d9d0dbb73 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -10,7 +10,7 @@ on: permissions: {} env: - NODE_VERSION: 20.18.1 + NODE_VERSION: 22.17.0 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). Also enables a reporter which exits the process running the tests if it haven't already. ARTIFACT_NAME_VSIX: ms-python-insiders-vsix diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index ab087673f1e7..e7159618d3ae 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -66,7 +66,7 @@ extends: buildSteps: - task: NodeTool@0 inputs: - versionSpec: '20.18.1' + versionSpec: '22.17.0' displayName: Select Node version - task: UsePythonVersion@0 diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index c68dacc7db80..ce67c69a3df4 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -60,7 +60,7 @@ extends: buildSteps: - task: NodeTool@0 inputs: - versionSpec: '20.18.1' + versionSpec: '22.17.0' displayName: Select Node version - task: UsePythonVersion@0 diff --git a/build/azure-pipelines/pipeline.yml b/build/azure-pipelines/pipeline.yml index ebb8a141d9d3..46302aa6ff90 100644 --- a/build/azure-pipelines/pipeline.yml +++ b/build/azure-pipelines/pipeline.yml @@ -37,13 +37,13 @@ extends: testPlatforms: - name: Linux nodeVersions: - - 20.18.1 + - 22.17.0 - name: MacOS nodeVersions: - - 20.18.1 + - 22.17.0 - name: Windows nodeVersions: - - 20.18.1 + - 22.17.0 testSteps: - template: /build/azure-pipelines/templates/test-steps.yml@self parameters: diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json index ec175f1aaa5d..e462fc1c888a 100644 --- a/pythonExtensionApi/package-lock.json +++ b/pythonExtensionApi/package-lock.json @@ -14,7 +14,7 @@ "typescript": "~5.2" }, "engines": { - "node": ">=20.18.1", + "node": ">=22.17.0", "vscode": "^1.93.0" } }, diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index 9a27e5a09b0a..e4e956ff6065 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -13,7 +13,7 @@ "main": "./out/main.js", "types": "./out/main.d.ts", "engines": { - "node": ">=20.18.1", + "node": ">=22.17.0", "vscode": "^1.93.0" }, "license": "MIT", @@ -27,7 +27,7 @@ }, "devDependencies": { "typescript": "~5.2", - "@types/vscode": "^1.93.0", + "@types/vscode": "^1.102.0", "source-map": "^0.8.0-beta.0" }, "scripts": { diff --git a/src/client/common/process/logger.ts b/src/client/common/process/logger.ts index 47e9ef88fa4f..b65da8dc81e5 100644 --- a/src/client/common/process/logger.ts +++ b/src/client/common/process/logger.ts @@ -41,6 +41,13 @@ export class ProcessLogger implements IProcessLogger { }); } + /** + * Formats command strings for display by replacing common paths with symbols. + * - Replaces the workspace folder path with '.' if there's exactly one workspace folder + * - Replaces the user's home directory path with '~' + * @param command The command string to format + * @returns The formatted command string with paths replaced by symbols + */ private getDisplayCommands(command: string): string { if (this.workspaceService.workspaceFolders && this.workspaceService.workspaceFolders.length === 1) { command = replaceMatchesWithCharacter(command, this.workspaceService.workspaceFolders[0].uri.fsPath, '.'); diff --git a/src/test/common/process/logger.unit.test.ts b/src/test/common/process/logger.unit.test.ts index f1421ea58b85..366a7056e89e 100644 --- a/src/test/common/process/logger.unit.test.ts +++ b/src/test/common/process/logger.unit.test.ts @@ -109,32 +109,55 @@ suite('ProcessLogger suite', () => { test('Logger replaces the path/to/home with ~ in the command path where the home path IS NOT at the beginning of the path', async () => { const options = { cwd: path.join('debug', 'path') }; - logger.logProcess(path.join('net', untildify('~'), 'test'), ['--foo', '--bar'], options); + const untildifyStr = untildify('~'); + + let p1 = path.join('net', untildifyStr, 'test'); + if (p1.startsWith('.')) { + if (getOSType() === OSType.Windows) { + p1 = p1.replace(/^\.\\+/, ''); + } else { + p1 = p1.replace(/^\.\\/, ''); + } + } + logger.logProcess(p1, ['--foo', '--bar'], options); - sinon.assert.calledWithExactly(traceLogStub, `> ${path.join('net', '~', 'test')} --foo --bar`); + const path1 = path.join('.', 'net', '~', 'test'); + sinon.assert.calledWithExactly(traceLogStub, `> ${path1} --foo --bar`); sinon.assert.calledWithExactly(traceLogStub, `cwd: ${options.cwd}`); }); test('Logger replaces the path/to/home with ~ in the command path where the home path IS NOT at the beginning of the path but another arg contains other ref to home folder', async () => { const options = { cwd: path.join('debug', 'path') }; - logger.logProcess( - path.join('net', untildify('~'), 'test'), - ['--foo', path.join(untildify('~'), 'boo')], - options, - ); + let p1 = path.join('net', untildify('~'), 'test'); + if (p1.startsWith('.')) { + if (getOSType() === OSType.Windows) { + p1 = p1.replace(/^\.\\+/, ''); + } else { + p1 = p1.replace(/^\.\\/, ''); + } + } + logger.logProcess(p1, ['--foo', path.join(untildify('~'), 'boo')], options); sinon.assert.calledWithExactly( traceLogStub, - `> ${path.join('net', '~', 'test')} --foo ${path.join('~', 'boo')}`, + `> ${path.join('.', 'net', '~', 'test')} --foo ${path.join('~', 'boo')}`, ); sinon.assert.calledWithExactly(traceLogStub, `cwd: ${options.cwd}`); }); test('Logger replaces the path/to/home with ~ in the command path where the home path IS NOT at the beginning of the path between doble quotes', async () => { const options = { cwd: path.join('debug', 'path') }; - logger.logProcess(`"${path.join('net', untildify('~'), 'test')}" "--foo" "--bar"`, undefined, options); + let p1 = path.join('net', untildify('~'), 'test'); + if (p1.startsWith('.')) { + if (getOSType() === OSType.Windows) { + p1 = p1.replace(/^\.\\+/, ''); + } else { + p1 = p1.replace(/^\.\\/, ''); + } + } + logger.logProcess(`"${p1}" "--foo" "--bar"`, undefined, options); - sinon.assert.calledWithExactly(traceLogStub, `> "${path.join('net', '~', 'test')}" "--foo" "--bar"`); + sinon.assert.calledWithExactly(traceLogStub, `> "${path.join('.', 'net', '~', 'test')}" "--foo" "--bar"`); sinon.assert.calledWithExactly(traceLogStub, `cwd: ${options.cwd}`); }); From 5b59591b5c3dce164bb66a0c951f4dbd6e9c4132 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:33:41 -0700 Subject: [PATCH 1005/1136] Bump actions/checkout from 4 to 5 (#25381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
Release notes

Sourced from actions/checkout's releases.

v5.0.0

What's Changed

⚠️ Minimum Compatible Runner Version

v2.327.1
Release Notes

Make sure your runner is updated to this version or newer to use this release.

Full Changelog: https://github.com/actions/checkout/compare/v4...v5.0.0

v4.3.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v4...v4.3.0

v4.2.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v4.2.1...v4.2.2

v4.2.1

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v4.2.0...v4.2.1

... (truncated)

Changelog

Sourced from actions/checkout's changelog.

Changelog

V5.0.0

V4.3.0

v4.2.2

v4.2.1

v4.2.0

v4.1.7

v4.1.6

v4.1.5

v4.1.4

v4.1.3

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- .github/workflows/build.yml | 18 ++++++------ .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/gen-issue-velocity.yml | 2 +- .github/workflows/info-needed-closer.yml | 2 +- .github/workflows/issue-labels.yml | 2 +- .github/workflows/pr-check.yml | 28 +++++++++---------- .../workflows/test-plan-item-validator.yml | 2 +- .github/workflows/triage-info-needed.yml | 4 +-- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 16f398f5a166..6d122b77288b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -84,12 +84,12 @@ jobs: # vsix-target: alpine-arm64 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'microsoft/python-environment-tools' path: 'python-env-tools' @@ -115,7 +115,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false @@ -135,7 +135,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false @@ -178,7 +178,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: path: ${{ env.special-working-directory-relative }} persist-credentials: false @@ -218,13 +218,13 @@ jobs: test-suite: [ts-unit, venv, single-workspace, multi-workspace, debugger, functional] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: path: ${{ env.special-working-directory-relative }} persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'microsoft/python-environment-tools' path: ${{ env.special-working-directory-relative }}/python-env-tools @@ -426,12 +426,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'microsoft/python-environment-tools' path: ${{ env.special-working-directory-relative }}/python-env-tools diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cfd7c393e3ed..84de97c4dc9a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/gen-issue-velocity.yml b/.github/workflows/gen-issue-velocity.yml index 344fa161f02e..c28c6c368562 100644 --- a/.github/workflows/gen-issue-velocity.yml +++ b/.github/workflows/gen-issue-velocity.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/info-needed-closer.yml b/.github/workflows/info-needed-closer.yml index d7efbd199451..33f53fc20dca 100644 --- a/.github/workflows/info-needed-closer.yml +++ b/.github/workflows/info-needed-closer.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index ec7d14d96cda..a78ca03d5ee9 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 368d9d0dbb73..b40c2c6946bf 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -57,12 +57,12 @@ jobs: # vsix-target: alpine-arm64 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'microsoft/python-environment-tools' path: 'python-env-tools' @@ -87,7 +87,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false @@ -106,12 +106,12 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'microsoft/python-environment-tools' path: 'python-env-tools' @@ -162,7 +162,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: path: ${{ env.special-working-directory-relative }} persist-credentials: false @@ -215,13 +215,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: path: ${{ env.special-working-directory-relative }} persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'microsoft/python-environment-tools' path: ${{ env.special-working-directory-relative }}/python-env-tools @@ -412,13 +412,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: path: ${{ env.special-working-directory-relative }} persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'microsoft/python-environment-tools' path: ${{ env.special-working-directory-relative }}/python-env-tools @@ -452,12 +452,12 @@ jobs: steps: # Need the source to have the tests available. - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'microsoft/python-environment-tools' path: python-env-tools @@ -488,12 +488,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'microsoft/python-environment-tools' path: python-env-tools diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml index 91e8948cc784..6e62058b04c6 100644 --- a/.github/workflows/test-plan-item-validator.yml +++ b/.github/workflows/test-plan-item-validator.yml @@ -12,7 +12,7 @@ jobs: if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') steps: - name: Checkout Actions - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml index f468fb293acd..f61d17c033d7 100644 --- a/.github/workflows/triage-info-needed.yml +++ b/.github/workflows/triage-info-needed.yml @@ -15,7 +15,7 @@ jobs: issues: write steps: - name: Checkout Actions - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable @@ -39,7 +39,7 @@ jobs: issues: write steps: - name: Checkout Actions - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable From cc8b3de11cb4bb3f4709f60cc2008b0b72dc54f9 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:48:33 -0700 Subject: [PATCH 1006/1136] msg for doctest unsupported (#25387) fixes https://github.com/microsoft/vscode-python/issues/25380 --- python_files/tests/unittestadapter/test_utils.py | 5 ++--- python_files/unittestadapter/pvsc_utils.py | 10 +++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/python_files/tests/unittestadapter/test_utils.py b/python_files/tests/unittestadapter/test_utils.py index b0341ce37b63..390b0779a44d 100644 --- a/python_files/tests/unittestadapter/test_utils.py +++ b/python_files/tests/unittestadapter/test_utils.py @@ -284,11 +284,10 @@ def test_build_empty_tree() -> None: start_dir = os.fsdecode(TEST_DATA_PATH) pattern = "does_not_exist*" - expected = None - loader = unittest.TestLoader() suite = loader.discover(start_dir, pattern) tests, errors = build_test_tree(suite, start_dir) - assert expected == tests + assert tests is not None + assert tests.get("children") == [] assert not errors diff --git a/python_files/unittestadapter/pvsc_utils.py b/python_files/unittestadapter/pvsc_utils.py index 017bad38966a..09a5ec9f3be5 100644 --- a/python_files/unittestadapter/pvsc_utils.py +++ b/python_files/unittestadapter/pvsc_utils.py @@ -3,6 +3,7 @@ import argparse import atexit +import doctest import enum import inspect import json @@ -202,6 +203,12 @@ def build_test_tree( root = build_test_node(top_level_directory, directory_path.name, TestNodeTypeEnum.folder) for test_case in get_test_case(suite): + if isinstance(test_case, doctest.DocTestCase): + print( + "Skipping doctest as it is not supported for the extension. Test case: ", test_case + ) + error = ["Skipping doctest as it is not supported for the extension."] + continue test_id = test_case.id() if test_id.startswith("unittest.loader._FailedTest"): error.append(str(test_case._exception)) # type: ignore # noqa: SLF001 @@ -255,9 +262,6 @@ def build_test_tree( } # concatenate class name and function test name current_node["children"].append(test_node) - if not root["children"]: - root = None - return root, error From 167e6e764763e7df88879e7afaa06cb9b21816b6 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 13 Aug 2025 15:17:58 -0700 Subject: [PATCH 1007/1136] update readme to include python-envs ext (#25388) fixes https://github.com/microsoft/vscode-python/issues/25384 --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9fe3669ce6b0..929bdef48892 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,21 @@ The Python extension does offer [some support](https://github.com/microsoft/vsco The Python extension will automatically install the following extensions by default to provide the best Python development experience in VS Code: -- [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) - to provide performant Python language support -- [Python Debugger](https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy) - to provide a seamless debug experience with debugpy +- [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) – performant Python language support +- [Python Debugger](https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy) – seamless debug experience with debugpy +- [Python Environments](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-python-envs) – dedicated environment management (see below) These extensions are optional dependencies, meaning the Python extension will remain fully functional if they fail to be installed. Any or all of these extensions can be [disabled](https://code.visualstudio.com/docs/editor/extension-marketplace#_disable-an-extension) or [uninstalled](https://code.visualstudio.com/docs/editor/extension-marketplace#_uninstall-an-extension) at the expense of some features. Extensions installed through the marketplace are subject to the [Marketplace Terms of Use](https://cdn.vsassets.io/v/M146_20190123.39/_content/Microsoft-Visual-Studio-Marketplace-Terms-of-Use.pdf). +### About the Python Environments Extension + +You may now see that the **Python Environments Extension** is installed for you, but it may or may not be "enabled" in your VS Code experience. Enablement is controlled by the setting `"python.useEnvironmentsExtension": true` (or `false`). + +- If you set this setting to `true`, you will manually opt in to using the Python Environments Extension for environment management. +- If you do not have this setting specified, you may be randomly assigned to have it turned on as we roll it out until it becomes the default experience for all users. + +The Python Environments Extension is still under active development and experimentation. Its goal is to provide a dedicated view and improved workflows for creating, deleting, and switching between Python environments, as well as managing packages. If you have feedback, please let us know via [issues](https://github.com/microsoft/vscode-python/issues). + ## Extensibility The Python extension provides pluggable access points for extensions that extend various feature areas to further improve your Python development experience. These extensions are all optional and depend on your project configuration and preferences. From 3df36ecb26e4b3c3d3bed982c34e5d1a2498f3b9 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:01:05 -0700 Subject: [PATCH 1008/1136] bug: use executeInFileDir and launchArgs settings with envs ext (#25405) helps with a part of https://github.com/microsoft/vscode-python-environments/issues/709 --- .../codeExecution/codeExecutionManager.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index 740256ca78b3..30e5b7facd2d 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -5,7 +5,7 @@ import { inject, injectable } from 'inversify'; import { Disposable, EventEmitter, Terminal, Uri } from 'vscode'; - +import * as path from 'path'; import { ICommandManager, IDocumentManager } from '../../common/application/types'; import { Commands } from '../../common/constants'; import '../../common/extensions'; @@ -130,6 +130,15 @@ export class CodeExecutionManager implements ICodeExecutionManager { if (!fileToExecute) { return; } + + // Check on setting terminal.executeInFileDir + const pythonSettings = this.configSettings.getSettings(file); + let cwd = pythonSettings.terminal.executeInFileDir ? path.dirname(fileToExecute.fsPath) : undefined; + + // Check on setting terminal.launchArgs + const launchArgs = pythonSettings.terminal.launchArgs; + const totalArgs = [...launchArgs, fileToExecute.fsPath.fileToCommandArgumentForPythonExt()]; + const fileAfterSave = await codeExecutionHelper.saveFileIfDirty(fileToExecute); if (fileAfterSave) { fileToExecute = fileAfterSave; @@ -138,19 +147,9 @@ export class CodeExecutionManager implements ICodeExecutionManager { const show = this.shouldTerminalFocusOnStart(fileToExecute); let terminal: Terminal | undefined; if (dedicated) { - terminal = await runInDedicatedTerminal( - fileToExecute, - [fileToExecute.fsPath.fileToCommandArgumentForPythonExt()], - undefined, - show, - ); + terminal = await runInDedicatedTerminal(fileToExecute, totalArgs, cwd, show); } else { - terminal = await runInTerminal( - fileToExecute, - [fileToExecute.fsPath.fileToCommandArgumentForPythonExt()], - undefined, - show, - ); + terminal = await runInTerminal(fileToExecute, totalArgs, cwd, show); } if (terminal) { From ebc683abd492a73fe9c707094e39ab18ae8f6bd4 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:06:58 -0700 Subject: [PATCH 1009/1136] chore: update tmp (#25404) --- package-lock.json | 57 +++---------------- package.json | 2 +- src/client/common/platform/fs-temp.ts | 24 +++----- src/test/common/platform/fs-temp.unit.test.ts | 17 +++--- 4 files changed, 28 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index 86e84f48b311..9a52111abc5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "semver": "^7.5.2", "stack-trace": "0.0.10", "sudo-prompt": "^9.2.1", - "tmp": "^0.0.33", + "tmp": "^0.2.5", "uint64be": "^3.0.0", "unicode": "^14.0.0", "vscode-debugprotocol": "^1.28.0", @@ -2514,18 +2514,6 @@ "node": "*" } }, - "node_modules/@vscode/vsce/node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -11155,14 +11143,6 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-cancelable": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", @@ -13374,14 +13354,12 @@ } }, "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", "engines": { - "node": ">=0.6.0" + "node": ">=14.14" } }, "node_modules/to-absolute-glob": { @@ -16951,15 +16929,6 @@ "requires": { "brace-expansion": "^1.1.7" } - }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } } } }, @@ -23574,11 +23543,6 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, "p-cancelable": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", @@ -25244,12 +25208,9 @@ } }, "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "~1.0.2" - } + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==" }, "to-absolute-glob": { "version": "2.0.2", diff --git a/package.json b/package.json index f113890ad97d..e07b3fc7983e 100644 --- a/package.json +++ b/package.json @@ -1713,7 +1713,7 @@ "semver": "^7.5.2", "stack-trace": "0.0.10", "sudo-prompt": "^9.2.1", - "tmp": "^0.0.33", + "tmp": "^0.2.5", "uint64be": "^3.0.0", "unicode": "^14.0.0", "vscode-debugprotocol": "^1.28.0", diff --git a/src/client/common/platform/fs-temp.ts b/src/client/common/platform/fs-temp.ts index 32b57df15387..60dde040f454 100644 --- a/src/client/common/platform/fs-temp.ts +++ b/src/client/common/platform/fs-temp.ts @@ -5,14 +5,7 @@ import * as tmp from 'tmp'; import { ITempFileSystem, TemporaryFile } from './types'; interface IRawTempFS { - // TODO (https://github.com/microsoft/vscode/issues/84517) - // This functionality has been requested for the - // VS Code FS API (vscode.workspace.fs.*). - file( - config: tmp.Options, - - callback?: (err: any, path: string, fd: number, cleanupCallback: () => void) => void, - ): void; + fileSync(config?: tmp.Options): tmp.SynchrounousResult; } // Operations related to temporary files and directories. @@ -35,14 +28,13 @@ export class TemporaryFileSystem implements ITempFileSystem { mode, }; return new Promise((resolve, reject) => { - this.raw.file(opts, (err, filename, _fd, cleanUp) => { - if (err) { - return reject(err); - } - resolve({ - filePath: filename, - dispose: cleanUp, - }); + const { name, removeCallback } = this.raw.fileSync(opts); + if (!name) { + return reject(new Error('Failed to create temp file')); + } + resolve({ + filePath: name, + dispose: removeCallback, }); }); } diff --git a/src/test/common/platform/fs-temp.unit.test.ts b/src/test/common/platform/fs-temp.unit.test.ts index bfc8284b33d6..29b4e5f42b12 100644 --- a/src/test/common/platform/fs-temp.unit.test.ts +++ b/src/test/common/platform/fs-temp.unit.test.ts @@ -7,11 +7,14 @@ import { TemporaryFileSystem } from '../../../client/common/platform/fs-temp'; interface IDeps { // tmp module - file( - config: { postfix?: string; mode?: number }, - - callback?: (err: any, path: string, fd: number, cleanupCallback: () => void) => void, - ): void; + fileSync(config: { + postfix?: string; + mode?: number; + }): { + name: string; + fd: number; + removeCallback(): void; + }; } suite('FileSystem - temp files', () => { @@ -28,7 +31,7 @@ suite('FileSystem - temp files', () => { suite('createFile', () => { test(`fails if the raw call fails`, async () => { const failure = new Error('oops'); - deps.setup((d) => d.file({ postfix: '.tmp', mode: undefined }, TypeMoq.It.isAny())) + deps.setup((d) => d.fileSync({ postfix: '.tmp', mode: undefined })) // fail with an arbitrary error .throws(failure); @@ -40,7 +43,7 @@ suite('FileSystem - temp files', () => { test(`fails if the raw call "returns" an error`, async () => { const failure = new Error('oops'); - deps.setup((d) => d.file({ postfix: '.tmp', mode: undefined }, TypeMoq.It.isAny())).callback((_cfg, cb) => + deps.setup((d) => d.fileSync({ postfix: '.tmp', mode: undefined })).callback((_cfg, cb) => cb(failure, '...', -1, () => {}), ); From 6181f44c38c92ee25b2bfd6364233de9c015fddd Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 25 Aug 2025 11:47:25 -0700 Subject: [PATCH 1010/1136] fix interpreter conversion to legacy api (#25417) fixes https://github.com/microsoft/vscode-python-environments/issues/508 --- src/client/envExt/api.legacy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/envExt/api.legacy.ts b/src/client/envExt/api.legacy.ts index 0f942a13eea2..d1a9f404d541 100644 --- a/src/client/envExt/api.legacy.ts +++ b/src/client/envExt/api.legacy.ts @@ -77,13 +77,13 @@ function toLegacyType(env: PythonEnvironment): PythonEnvironmentLegacy { const ver = parseVersion(env.version); const envType = toEnvironmentType(env); return { - id: env.environmentPath.fsPath, + id: env.execInfo.run.executable, displayName: env.displayName, detailedDisplayName: env.name, envType, envPath: env.sysPrefix, type: getEnvType(envType), - path: env.environmentPath.fsPath, + path: env.execInfo.run.executable, version: { raw: env.version, major: ver.major, From c102bbd49a3d9438cfbd49068cfd3a7ed84f4254 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 29 Aug 2025 11:14:47 -0700 Subject: [PATCH 1011/1136] add additional msgs for python envs ext add (#25426) assist with making it more visible that the environments extension is now installed by default --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 929bdef48892..ffc73d3232ed 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Python extension for Visual Studio Code -A [Visual Studio Code](https://code.visualstudio.com/) [extension](https://marketplace.visualstudio.com/VSCode) with rich support for the [Python language](https://www.python.org/) (for all [actively supported Python versions](https://devguide.python.org/versions/#supported-versions)), providing access points for extensions to seamlessly integrate and offer support for IntelliSense (Pylance), debugging (Python Debugger), formatting, linting, code navigation, refactoring, variable explorer, test explorer, and more! +A [Visual Studio Code](https://code.visualstudio.com/) [extension](https://marketplace.visualstudio.com/VSCode) with rich support for the [Python language](https://www.python.org/) (for all [actively supported Python versions](https://devguide.python.org/versions/#supported-versions)), providing access points for extensions to seamlessly integrate and offer support for IntelliSense (Pylance), debugging (Python Debugger), formatting, linting, code navigation, refactoring, variable explorer, test explorer, environment management (**NEW** Python Environments Extension). ## Support for [vscode.dev](https://vscode.dev/) @@ -13,7 +13,7 @@ The Python extension will automatically install the following extensions by defa - [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) – performant Python language support - [Python Debugger](https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy) – seamless debug experience with debugpy -- [Python Environments](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-python-envs) – dedicated environment management (see below) +- **(NEW)** [Python Environments](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-python-envs) – dedicated environment management (see below) These extensions are optional dependencies, meaning the Python extension will remain fully functional if they fail to be installed. Any or all of these extensions can be [disabled](https://code.visualstudio.com/docs/editor/extension-marketplace#_disable-an-extension) or [uninstalled](https://code.visualstudio.com/docs/editor/extension-marketplace#_uninstall-an-extension) at the expense of some features. Extensions installed through the marketplace are subject to the [Marketplace Terms of Use](https://cdn.vsassets.io/v/M146_20190123.39/_content/Microsoft-Visual-Studio-Marketplace-Terms-of-Use.pdf). From b9248c46317a5855f14cd83aa508fe49631b313d Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 2 Sep 2025 09:38:49 +1000 Subject: [PATCH 1012/1136] Be explicit about the fact that Tools are specific to Python (#25403) Fixes https://github.com/microsoft/vscode/issues/262069 --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e07b3fc7983e..cc8bed68ddb5 100644 --- a/package.json +++ b/package.json @@ -1502,7 +1502,7 @@ "name": "get_python_environment_details", "displayName": "Get Python Environment Info", "userDescription": "%python.languageModelTools.get_python_environment_details.userDescription%", - "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Environment (conda, venv, etec), 2. Version of Python, 3. List of all installed packages with their versions. ALWAYS call configure_python_environment before using this tool.", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Python Environment (conda, venv, etec), 2. Version of Python, 3. List of all installed Python packages with their versions. ALWAYS call configure_python_environment before using this tool.", "toolReferenceName": "getPythonEnvironmentInfo", "tags": [ "python", @@ -1552,7 +1552,7 @@ "name": "install_python_packages", "displayName": "Install Python Package", "userDescription": "%python.languageModelTools.install_python_packages.userDescription%", - "modelDescription": "Installs Python packages in the given workspace. Use this tool to install packages in the user's chosen environment. ALWAYS call configure_python_environment before using this tool.", + "modelDescription": "Installs Python packages in the given workspace. Use this tool to install Python packages in the user's chosen Python environment. ALWAYS call configure_python_environment before using this tool.", "toolReferenceName": "installPythonPackage", "tags": [ "python", @@ -1571,7 +1571,7 @@ "items": { "type": "string" }, - "description": "The list of packages to install." + "description": "The list of Python packages to install." }, "resourcePath": { "type": "string", From 6235a0226cd4e005d6fe7a5da31da2bef88fa9d0 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Fri, 5 Sep 2025 09:43:18 -0700 Subject: [PATCH 1013/1136] bump 2025.14.0, update pet (#25444) --- build/azure-pipeline.stable.yml | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index ce67c69a3df4..71399281efd9 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -128,7 +128,7 @@ extends: project: 'Monaco' definition: 593 buildVersionToDownload: 'latestFromBranch' - branchName: 'refs/heads/release/2025.12' + branchName: 'refs/heads/release/2025.14' targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' artifactName: 'bin-$(buildTarget)' itemPattern: | diff --git a/package-lock.json b/package-lock.json index 9a52111abc5c..3c8d361e0897 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.13.0-dev", + "version": "2025.14.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.13.0-dev", + "version": "2025.14.0", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index cc8bed68ddb5..dd89ca4b5685 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.13.0-dev", + "version": "2025.14.0", "featureFlags": { "usingNewInterpreterStorage": true }, From 4e0633b63952b6d6e11fd79e26739868b97a52b6 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Fri, 5 Sep 2025 10:22:22 -0700 Subject: [PATCH 1014/1136] bump to 2025.15 (#25445) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c8d361e0897..be908b0b0f11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.14.0", + "version": "2025.15.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.14.0", + "version": "2025.15.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index dd89ca4b5685..0530726310f2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.14.0", + "version": "2025.15.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 8c21493b92f15842faf8e1616d960a8ac8a0e7d4 Mon Sep 17 00:00:00 2001 From: fourdim <59462000+fourdim@users.noreply.github.com> Date: Tue, 9 Sep 2025 03:44:49 -0400 Subject: [PATCH 1015/1136] Fix native REPL search path (#25372) Resolves: https://github.com/microsoft/vscode-python/issues/24361 --- python_files/python_server.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/python_files/python_server.py b/python_files/python_server.py index 1689d9b8f7f9..77b43c692dc3 100644 --- a/python_files/python_server.py +++ b/python_files/python_server.py @@ -5,6 +5,7 @@ import sys import traceback import uuid +from pathlib import Path from typing import Dict, List, Optional, Union STDIN = sys.stdin @@ -172,6 +173,16 @@ def get_headers(): if __name__ == "__main__": + # https://docs.python.org/3/tutorial/modules.html#the-module-search-path + # The directory containing the input script (or the current directory when no file is specified). + # Here we emulate the same behavior like no file is specified. + input_script_dir = Path(__file__).parent + script_dir_str = str(input_script_dir) + if script_dir_str in sys.path: + sys.path.remove(script_dir_str) + while "" in sys.path: + sys.path.remove("") + sys.path.insert(0, "") while not STDIN.closed: try: headers = get_headers() From 5eb18d3a225e48ec23feadfdb987c3cd7e2c50e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:20:05 -0700 Subject: [PATCH 1016/1136] Bump actions/setup-node from 4 to 5 (#25439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
Release notes

Sourced from actions/setup-node's releases.

v5.0.0

What's Changed

Breaking Changes

Make sure your runner is updated to this version or newer to use this release. v2.327.1 Release Notes

Dependency Upgrades

Enhancement:

New Contributors

Full Changelog: https://github.com/actions/setup-node/compare/v4...v5.0.0

v4.4.0

What's Changed

Bug fixes:

Enhancement:

Dependency update:

New Contributors

Full Changeloghttps://github.com/actions/setup-node/compare/v4...v4.4.0

v4.3.0

What's Changed

Dependency updates

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/pr-check.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d122b77288b..dc95df9d0bfc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -236,7 +236,7 @@ jobs: sparse-checkout-cone-mode: false - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index b40c2c6946bf..bfa12e1724fc 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -233,7 +233,7 @@ jobs: sparse-checkout-cone-mode: false - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' @@ -505,7 +505,7 @@ jobs: sparse-checkout-cone-mode: false - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' From c171088c4e773522e93c458fc4f02c43ae5f0041 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:20:33 -0700 Subject: [PATCH 1017/1136] Bump actions/github-script from 7 to 8 (#25443) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
Release notes

Sourced from actions/github-script's releases.

v8.0.0

What's Changed

⚠️ Minimum Compatible Runner Version

v2.327.1
Release Notes

Make sure your runner is updated to this version or newer to use this release.

New Contributors

Full Changelog: https://github.com/actions/github-script/compare/v7.1.0...v8.0.0

v7.1.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/github-script/compare/v7...v7.1.0

... (truncated)

Commits
  • ed59741 Merge pull request #653 from actions/sneha-krip/readme-for-v8
  • 2dc352e Bold minimum Actions Runner version in README
  • 01e118c Update README for Node 24 runtime requirements
  • 8b222ac Apply suggestion from @​salmanmkc
  • adc0eea README for updating actions/github-script from v7 to v8
  • 20fe497 Merge pull request #637 from actions/node24
  • e7b7f22 update licenses
  • 2c81ba0 Update Node.js version support to 24.x
  • f28e40c Merge pull request #610 from actions/nebuk89-patch-1
  • 1ae9958 Update README.md
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/github-script&package-manager=github_actions&previous-version=7&new-version=8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-file-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index 688c48d865d8..da5d6fa8f696 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -44,7 +44,7 @@ jobs: failure-message: 'TypeScript code was edited without also editing a ${file-pattern} file; see the Testing page in our wiki on testing guidelines (the ${skip-label} label can be used to pass this check)' - name: 'Ensure PR has an associated issue' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const labels = context.payload.pull_request.labels.map(label => label.name); From b1d4e61c27063321ef69171aab2fed86e26c0d3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:21:26 -0700 Subject: [PATCH 1018/1136] Bump actions/setup-python from 5 to 6 in /.github/actions/lint (#25437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
Release notes

Sourced from actions/setup-python's releases.

v6.0.0

What's Changed

Breaking Changes

Make sure your runner is on version v2.327.1 or later to ensure compatibility with this release. See Release Notes

Enhancements:

Bug fixes:

Dependency updates:

New Contributors

Full Changelog: https://github.com/actions/setup-python/compare/v5...v6.0.0

v5.6.0

What's Changed

Full Changelog: https://github.com/actions/setup-python/compare/v5...v5.6.0

v5.5.0

What's Changed

Enhancements:

Bug fixes:

... (truncated)

Commits
  • e797f83 Upgrade to node 24 (#1164)
  • 3d1e2d2 Revert "Enhance cache-dependency-path handling to support files outside the w...
  • 65b0712 Clarify pythonLocation behavior for PyPy and GraalPy in environment variables...
  • 5b668cf Bump actions/checkout from 4 to 5 (#1181)
  • f62a0e2 Change missing cache directory error to warning (#1182)
  • 9322b3c Upgrade setuptools to 78.1.1 to fix path traversal vulnerability in PackageIn...
  • fbeb884 Bump form-data to fix critical vulnerabilities #182 & #183 (#1163)
  • 03bb615 Bump idna from 2.9 to 3.7 in /tests/data (#843)
  • 36da51d Add version parsing from Pipfile (#1067)
  • 3c6f142 update documentation (#1156)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/lint/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml index 9992b442c276..3a989fddb982 100644 --- a/.github/actions/lint/action.yml +++ b/.github/actions/lint/action.yml @@ -36,7 +36,7 @@ runs: shell: bash - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' cache: 'pip' From 3ab937e8ae313fd8ef2265fc97df881987054308 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:21:47 -0700 Subject: [PATCH 1019/1136] Bump typing-extensions from 4.14.1 to 4.15.0 (#25422) Bumps [typing-extensions](https://github.com/python/typing_extensions) from 4.14.1 to 4.15.0.
Release notes

Sourced from typing-extensions's releases.

4.15.0

No user-facing changes since 4.15.0rc1.

New features since 4.14.1:

  • Add the @typing_extensions.disjoint_base decorator, as specified in PEP 800. Patch by Jelle Zijlstra.
  • Add typing_extensions.type_repr, a backport of annotationlib.type_repr, introduced in Python 3.14 (CPython PR #124551, originally by Jelle Zijlstra). Patch by Semyon Moroz.
  • Fix behavior of type params in typing_extensions.evaluate_forward_ref. Backport of CPython PR #137227 by Jelle Zijlstra.

4.15.0rc1

  • Add the @typing_extensions.disjoint_base decorator, as specified in PEP 800. Patch by Jelle Zijlstra.
  • Add typing_extensions.type_repr, a backport of annotationlib.type_repr, introduced in Python 3.14 (CPython PR #124551, originally by Jelle Zijlstra). Patch by Semyon Moroz.
  • Fix behavior of type params in typing_extensions.evaluate_forward_ref. Backport of CPython PR #137227 by Jelle Zijlstra.
Changelog

Sourced from typing-extensions's changelog.

Release 4.15.0 (August 25, 2025)

No user-facing changes since 4.15.0rc1.

Release 4.15.0rc1 (August 18, 2025)

  • Add the @typing_extensions.disjoint_base decorator, as specified in PEP 800. Patch by Jelle Zijlstra.
  • Add typing_extensions.type_repr, a backport of annotationlib.type_repr, introduced in Python 3.14 (CPython PR #124551, originally by Jelle Zijlstra). Patch by Semyon Moroz.
  • Fix behavior of type params in typing_extensions.evaluate_forward_ref. Backport of CPython PR #137227 by Jelle Zijlstra.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typing-extensions&package-manager=pip&previous-version=4.14.1&new-version=4.15.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.in | 2 +- requirements.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.in b/requirements.in index ba2339b1e966..8bbc9a0f3728 100644 --- a/requirements.in +++ b/requirements.in @@ -4,7 +4,7 @@ # 2) uv pip compile --generate-hashes --upgrade requirements.in -o requirements.txt # Unittest test adapter -typing-extensions==4.14.1 +typing-extensions==4.15.0 # Fallback env creator for debian microvenv diff --git a/requirements.txt b/requirements.txt index 2e6b6ee07783..dddc2ee9691c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,9 +46,9 @@ tomli==2.2.1 \ --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 # via -r requirements.in -typing-extensions==4.14.1 \ - --hash=sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36 \ - --hash=sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76 +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via -r requirements.in zipp==3.21.0 \ --hash=sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4 \ From 30cd4e38b24043b7f32fa2194550dc6a1270028d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:22:12 -0700 Subject: [PATCH 1020/1136] Bump actions/setup-node from 4 to 5 in /.github/actions/lint (#25434) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
Release notes

Sourced from actions/setup-node's releases.

v5.0.0

What's Changed

Breaking Changes

Make sure your runner is updated to this version or newer to use this release. v2.327.1 Release Notes

Dependency Upgrades

Enhancement:

New Contributors

Full Changelog: https://github.com/actions/setup-node/compare/v4...v5.0.0

v4.4.0

What's Changed

Bug fixes:

Enhancement:

Dependency update:

New Contributors

Full Changeloghttps://github.com/actions/setup-node/compare/v4...v4.4.0

v4.3.0

What's Changed

Dependency updates

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/lint/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml index 3a989fddb982..9971c0fbcf96 100644 --- a/.github/actions/lint/action.yml +++ b/.github/actions/lint/action.yml @@ -10,7 +10,7 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ inputs.node_version }} cache: 'npm' From 3747439cf43fcde2b85a405320ed0bf4999d8109 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:22:48 -0700 Subject: [PATCH 1021/1136] Bump jakebailey/pyright-action from 2.3.2 to 2.3.3 (#25401) Bumps [jakebailey/pyright-action](https://github.com/jakebailey/pyright-action) from 2.3.2 to 2.3.3.
Release notes

Sourced from jakebailey/pyright-action's releases.

v2.3.3

  • Fix lint (4599f31)
  • Replace jest-path-serializer (1349f1a)
  • Fix deps (f701448)
  • fmt (ec50111)
  • Update engines (41972b7)
  • Update github actions (#180) (86e183a)
  • Update actions/checkout action to v5 (#190) (8b711b9)
  • Update deps (9631dc2)
  • Update deps (fa0d678)
  • Update github actions (#163) (623784a)
  • Fix eslint (73a65bd)
  • Update deps (dee7200)
  • Update deps (ea37d1c)
  • Update nvmrc (fb32d81)
  • Update eslint (b0c5af5)
  • Update deps (f4851c1)
  • Update actions/cache action to v4.2.0 (#159) (57f6678)
  • Update codecov/codecov-action action to v5 (#154) (f572338)
  • Update github actions (#146) (b7d7f8e)
  • Update deps (b721321)
  • Update deps (4156862)
  • Update github actions (#121) (ec480a0)
  • Update deps (bfe39b3)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=jakebailey/pyright-action&package-manager=github_actions&previous-version=2.3.2&new-version=2.3.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/pr-check.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dc95df9d0bfc..51aff82bfc6f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -155,7 +155,7 @@ jobs: python -m pip install --upgrade -r build/test-requirements.txt - name: Run Pyright - uses: jakebailey/pyright-action@b5d50e5cde6547546a5c4ac92e416a8c2c1a1dfe # v2.3.2 + uses: jakebailey/pyright-action@6cabc0f01c4994be48fd45cd9dbacdd6e1ee6e5e # v2.3.3 with: version: 1.1.308 working-directory: 'python_files' diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index bfa12e1724fc..07fb9c19ec67 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -138,7 +138,7 @@ jobs: python -m pip install --upgrade -r build/test-requirements.txt - name: Run Pyright - uses: jakebailey/pyright-action@b5d50e5cde6547546a5c4ac92e416a8c2c1a1dfe # v2.3.2 + uses: jakebailey/pyright-action@6cabc0f01c4994be48fd45cd9dbacdd6e1ee6e5e # v2.3.3 with: version: 1.1.308 working-directory: 'python_files' From ed276d08bbbac9890d66596253298ea28acd5257 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Thu, 11 Sep 2025 23:29:48 +0200 Subject: [PATCH 1022/1136] Upgrade jedi-language-server to 0.45.1 (#25450) 0.45.0 was added with https://github.com/microsoft/vscode-python/pull/25006 but also brought https://github.com/pappasam/jedi-language-server/issues/340 image v0.45.1 should workaround it. --- .../jedilsp_requirements/requirements.txt | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/python_files/jedilsp_requirements/requirements.txt b/python_files/jedilsp_requirements/requirements.txt index 0fc5cd76810f..e2599e7bbce4 100644 --- a/python_files/jedilsp_requirements/requirements.txt +++ b/python_files/jedilsp_requirements/requirements.txt @@ -6,32 +6,32 @@ attrs==25.3.0 \ # via # cattrs # lsprotocol -cattrs==24.1.3 \ - --hash=sha256:981a6ef05875b5bb0c7fb68885546186d306f10f0f6718fe9b96c226e68821ff \ - --hash=sha256:adf957dddd26840f27ffbd060a6c4dd3b2192c5b7c2c0525ef1bd8131d8a83f5 +cattrs==25.2.0 \ + --hash=sha256:539d7eedee7d2f0706e4e109182ad096d608ba84633c32c75ef3458f1d11e8f1 \ + --hash=sha256:f46c918e955db0177be6aa559068390f71988e877c603ae2e56c71827165cc06 # via # jedi-language-server # lsprotocol # pygls -docstring-to-markdown==0.16 \ - --hash=sha256:097bf502fdf040b0d019688a7cc1abb89b98196801448721740e8aa3e5075627 \ - --hash=sha256:f92cc42357b2c932f70ca2ebc79f7805039a34011ad381c1b6ac3481e81596ce +docstring-to-markdown==0.17 \ + --hash=sha256:df72a112294c7492487c9da2451cae0faeee06e86008245c188c5761c9590ca3 \ + --hash=sha256:fd7d5094aa83943bf5f9e1a13701866b7c452eac19765380dead666e36d3711c # via jedi-language-server -exceptiongroup==1.2.2 \ - --hash=sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b \ - --hash=sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc +exceptiongroup==1.3.0 \ + --hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \ + --hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88 # via cattrs -importlib-metadata==8.6.1 \ - --hash=sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e \ - --hash=sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580 +importlib-metadata==8.7.0 \ + --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ + --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd # via docstring-to-markdown jedi==0.19.2 \ --hash=sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0 \ --hash=sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9 # via jedi-language-server -jedi-language-server==0.45.0 \ - --hash=sha256:b44eb380f87c37935b91e4399f048dc935eb7d85829130fdbcecfdad61e1362b \ - --hash=sha256:f9ffd662877324ff28720c770197514184801b049a2d2c43190a7708b061f397 +jedi-language-server==0.45.1 \ + --hash=sha256:8c0c6b4eaeffdbb87be79e9897c9929ffeddf875dff7c1c36dd67768e294942b \ + --hash=sha256:a1fcfba8008f2640e921937fcf1933c3961d74249341eba8b3ef9a0c3f817102 # via -r python_files/jedilsp_requirements/requirements.in lsprotocol==2023.0.1 \ --hash=sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2 \ @@ -39,9 +39,9 @@ lsprotocol==2023.0.1 \ # via # jedi-language-server # pygls -parso==0.8.4 \ - --hash=sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18 \ - --hash=sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d +parso==0.8.5 \ + --hash=sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a \ + --hash=sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887 # via jedi pygls==1.3.1 \ --hash=sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018 \ @@ -49,14 +49,15 @@ pygls==1.3.1 \ # via # -r python_files/jedilsp_requirements/requirements.in # jedi-language-server -typing-extensions==4.13.2 \ - --hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \ - --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via # cattrs # docstring-to-markdown + # exceptiongroup # jedi-language-server -zipp==3.21.0 \ - --hash=sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4 \ - --hash=sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931 +zipp==3.23.0 \ + --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ + --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 # via importlib-metadata From 85ccdd74d40a574b38fcde3d9adeedb1b4d21314 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:41:58 -0700 Subject: [PATCH 1023/1136] Bump actions/setup-python from 5 to 6 (#25438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
Release notes

Sourced from actions/setup-python's releases.

v6.0.0

What's Changed

Breaking Changes

Make sure your runner is on version v2.327.1 or later to ensure compatibility with this release. See Release Notes

Enhancements:

Bug fixes:

Dependency updates:

New Contributors

Full Changelog: https://github.com/actions/setup-python/compare/v5...v6.0.0

v5.6.0

What's Changed

Full Changelog: https://github.com/actions/setup-python/compare/v5...v5.6.0

v5.5.0

What's Changed

Enhancements:

Bug fixes:

... (truncated)

Commits
  • e797f83 Upgrade to node 24 (#1164)
  • 3d1e2d2 Revert "Enhance cache-dependency-path handling to support files outside the w...
  • 65b0712 Clarify pythonLocation behavior for PyPy and GraalPy in environment variables...
  • 5b668cf Bump actions/checkout from 4 to 5 (#1181)
  • f62a0e2 Change missing cache directory error to warning (#1182)
  • 9322b3c Upgrade setuptools to 78.1.1 to fix path traversal vulnerability in PackageIn...
  • fbeb884 Bump form-data to fix critical vulnerabilities #182 & #183 (#1163)
  • 03bb615 Bump idna from 2.9 to 3.7 in /tests/data (#843)
  • 36da51d Add version parsing from Pipfile (#1067)
  • 3c6f142 update documentation (#1156)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 6 +++--- .github/workflows/gen-issue-velocity.yml | 2 +- .github/workflows/pr-check.yml | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 51aff82bfc6f..88b88ebc2876 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -130,7 +130,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Use Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} @@ -184,7 +184,7 @@ jobs: persist-credentials: false - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} @@ -252,7 +252,7 @@ jobs: run: npx @vscode/l10n-dev@latest export ./src - name: Install Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} diff --git a/.github/workflows/gen-issue-velocity.yml b/.github/workflows/gen-issue-velocity.yml index c28c6c368562..fdcb41cdaba9 100644 --- a/.github/workflows/gen-issue-velocity.yml +++ b/.github/workflows/gen-issue-velocity.yml @@ -19,7 +19,7 @@ jobs: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 07fb9c19ec67..c932c314682d 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -101,7 +101,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Use Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} @@ -168,7 +168,7 @@ jobs: persist-credentials: false - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} @@ -249,7 +249,7 @@ jobs: run: npx @vscode/l10n-dev@latest export ./src - name: Use Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} @@ -520,7 +520,7 @@ jobs: run: npx @vscode/l10n-dev@latest export ./src - name: Use Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} cache: 'pip' From b5787a44311a662568142138cf25cc97d8c36ca6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:42:25 -0700 Subject: [PATCH 1024/1136] Bump actions/setup-python from 5 to 6 in /.github/actions/build-vsix (#25433) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
Release notes

Sourced from actions/setup-python's releases.

v6.0.0

What's Changed

Breaking Changes

Make sure your runner is on version v2.327.1 or later to ensure compatibility with this release. See Release Notes

Enhancements:

Bug fixes:

Dependency updates:

New Contributors

Full Changelog: https://github.com/actions/setup-python/compare/v5...v6.0.0

v5.6.0

What's Changed

Full Changelog: https://github.com/actions/setup-python/compare/v5...v5.6.0

v5.5.0

What's Changed

Enhancements:

Bug fixes:

... (truncated)

Commits
  • e797f83 Upgrade to node 24 (#1164)
  • 3d1e2d2 Revert "Enhance cache-dependency-path handling to support files outside the w...
  • 65b0712 Clarify pythonLocation behavior for PyPy and GraalPy in environment variables...
  • 5b668cf Bump actions/checkout from 4 to 5 (#1181)
  • f62a0e2 Change missing cache directory error to warning (#1182)
  • 9322b3c Upgrade setuptools to 78.1.1 to fix path traversal vulnerability in PackageIn...
  • fbeb884 Bump form-data to fix critical vulnerabilities #182 & #183 (#1163)
  • 03bb615 Bump idna from 2.9 to 3.7 in /tests/data (#843)
  • 36da51d Add version parsing from Pipfile (#1067)
  • 3c6f142 update documentation (#1156)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- .github/actions/build-vsix/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index eaabe5141e8b..bfe90fc940e8 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -32,7 +32,7 @@ runs: # Jedi LS depends on dataclasses which is not in the stdlib in Python 3.7. - name: Use Python 3.9 for JediLSP - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.9 cache: 'pip' From 6712746df8aae1b1b8d5e5264f7b4ad7a9bd9bd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:42:49 -0700 Subject: [PATCH 1025/1136] Bump sha.js from 2.4.11 to 2.4.12 (#25409) Bumps [sha.js](https://github.com/crypto-browserify/sha.js) from 2.4.11 to 2.4.12.
Changelog

Sourced from sha.js's changelog.

v2.4.12 - 2025-07-01

Commits

  • [eslint] switch to eslint 7acadfb
  • [meta] add auto-changelog b46e711
  • [eslint] fix package.json indentation df9d521
  • [Tests] migrate from travis to GHA c43c64a
  • [Fix] support multi-byte wide typed arrays f2a258e
  • [meta] reorder package.json d8d77c0
  • [meta] add npmignore 35aec35
  • [Tests] avoid console logs 73e33ae
  • [Tests] fix tests run in batch 2629130
  • [Tests] drop node requirement to 0.10 00c7f23
  • [Dev Deps] update buffer, hash-test-vectors, standard, tape, typedarray 92b5de5
  • [Tests] drop node requirement to v3 9b5eca8
  • [meta] set engines to &gt;= 4 807084c
  • Only apps should have lockfiles c72789c
  • [Deps] update inherits, safe-buffer 5428cfc
  • [Dev Deps] update @ljharb/eslint-config 2dbe0aa
  • update README to reflect LICENSE 8938256
  • [Dev Deps] add missing peer dep d528896
  • [Dev Deps] remove unused buffer dep 94ca724
Commits
  • eb4ea2f v2.4.12
  • d8d77c0 [meta] reorder package.json
  • df9d521 [eslint] fix package.json indentation
  • 35aec35 [meta] add npmignore
  • d528896 [Dev Deps] add missing peer dep
  • b46e711 [meta] add auto-changelog
  • 94ca724 [Dev Deps] remove unused buffer dep
  • 2dbe0aa [Dev Deps] update @ljharb/eslint-config
  • 73e33ae [Tests] avoid console logs
  • f2a258e [Fix] support multi-byte wide typed arrays
  • Additional commits viewable in compare view
Maintainer changes

This version was pushed to npm by ljharb, a new releaser for sha.js since your current version.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=sha.js&package-manager=npm_and_yarn&previous-version=2.4.11&new-version=2.4.12)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- package-lock.json | 56 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index be908b0b0f11..2775ed2d88cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12444,18 +12444,45 @@ "dev": true }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "dev": true, "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sha.js/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -24525,13 +24552,22 @@ "dev": true }, "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "dev": true, "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "shallow-clone": { From 2968abd26e2c9e196ee393793ecd3911d6d6307e Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 18 Sep 2025 15:51:03 -0700 Subject: [PATCH 1026/1136] Optimize test result processing in PythonResultResolver to improve performance and reduce complexity (#25471) fixes https://github.com/microsoft/vscode-python/issues/25366 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../testController/common/resultResolver.ts | 425 ++++++++++++------ .../testing/common/testingAdapter.test.ts | 208 ++++++++- 2 files changed, 485 insertions(+), 148 deletions(-) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 82856627e0c9..b92e7a870f20 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -99,6 +99,11 @@ export class PythonResultResolver implements ITestResultResolver { // if any tests exist, they should be populated in the test tree, regardless of whether there were errors or not. // parse and insert test data. + // Clear existing mappings before rebuilding test tree + this.runIdToTestItem.clear(); + this.runIdToVSid.clear(); + this.vsIdToRunId.clear(); + // If the test root for this folder exists: Workspace refresh, update its children. // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. populateTestTree(this.testController, rawTestData.tests, undefined, this, token); @@ -173,165 +178,291 @@ export class PythonResultResolver implements ITestResultResolver { } } + /** + * Collect all test case items from the test controller tree. + * Note: This performs full tree traversal - use cached lookups when possible. + */ + private collectAllTestCases(): TestItem[] { + const testCases: TestItem[] = []; + + this.testController.items.forEach((i) => { + const tempArr: TestItem[] = getTestCaseNodes(i); + testCases.push(...tempArr); + }); + + return testCases; + } + + /** + * Find a test item efficiently using cached maps with fallback strategies. + * Uses a three-tier approach: direct lookup, ID mapping, then tree search. + */ + private findTestItemByIdEfficient(keyTemp: string): TestItem | undefined { + // Try direct O(1) lookup first + const directItem = this.runIdToTestItem.get(keyTemp); + if (directItem) { + // Validate the item is still in the test tree + if (this.isTestItemValid(directItem)) { + return directItem; + } else { + // Clean up stale reference + this.runIdToTestItem.delete(keyTemp); + } + } + + // Try vsId mapping as fallback + const vsId = this.runIdToVSid.get(keyTemp); + if (vsId) { + // Search by VS Code ID in the controller + let foundItem: TestItem | undefined; + this.testController.items.forEach((item) => { + if (item.id === vsId) { + foundItem = item; + return; + } + if (!foundItem) { + item.children.forEach((child) => { + if (child.id === vsId) { + foundItem = child; + } + }); + } + }); + + if (foundItem) { + // Cache for future lookups + this.runIdToTestItem.set(keyTemp, foundItem); + return foundItem; + } else { + // Clean up stale mapping + this.runIdToVSid.delete(keyTemp); + this.vsIdToRunId.delete(vsId); + } + } + + // Last resort: full tree search + traceError(`Falling back to tree search for test: ${keyTemp}`); + const testCases = this.collectAllTestCases(); + return testCases.find((item) => item.id === vsId); + } + + /** + * Check if a TestItem is still valid (exists in the TestController tree) + * + * Time Complexity: O(depth) where depth is the maximum nesting level of the test tree. + * In most cases this is O(1) to O(3) since test trees are typically shallow. + */ + private isTestItemValid(testItem: TestItem): boolean { + // Simple validation: check if the item's parent chain leads back to the controller + let current: TestItem | undefined = testItem; + while (current?.parent) { + current = current.parent; + } + + // If we reached a root item, check if it's in the controller + if (current) { + return this.testController.items.get(current.id) === current; + } + + // If no parent chain, check if it's directly in the controller + return this.testController.items.get(testItem.id) === testItem; + } + + /** + * Clean up stale test item references from the cache maps. + * Validates cached items and removes any that are no longer in the test tree. + */ + public cleanupStaleReferences(): void { + const staleRunIds: string[] = []; + + // Check all runId->TestItem mappings + this.runIdToTestItem.forEach((testItem, runId) => { + if (!this.isTestItemValid(testItem)) { + staleRunIds.push(runId); + } + }); + + // Remove stale entries + staleRunIds.forEach((runId) => { + const vsId = this.runIdToVSid.get(runId); + this.runIdToTestItem.delete(runId); + this.runIdToVSid.delete(runId); + if (vsId) { + this.vsIdToRunId.delete(vsId); + } + }); + + if (staleRunIds.length > 0) { + traceVerbose(`Cleaned up ${staleRunIds.length} stale test item references`); + } + } + + /** + * Handle test items that errored during execution. + * Extracts error details, finds the corresponding TestItem, and reports the error to VS Code's Test Explorer. + */ + private handleTestError(keyTemp: string, testItem: any, runInstance: TestRun): void { + const rawTraceback = testItem.traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + const text = `${testItem.test} failed with error: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + const message = new TestMessage(text); + + const foundItem = this.findTestItemByIdEfficient(keyTemp); + + if (foundItem?.uri) { + if (foundItem.range) { + message.location = new Location(foundItem.uri, foundItem.range); + } + runInstance.errored(foundItem, message); + } + } + + /** + * Handle test items that failed during execution + */ + private handleTestFailure(keyTemp: string, testItem: any, runInstance: TestRun): void { + const rawTraceback = testItem.traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + + const text = `${testItem.test} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + const message = new TestMessage(text); + + const foundItem = this.findTestItemByIdEfficient(keyTemp); + + if (foundItem?.uri) { + if (foundItem.range) { + message.location = new Location(foundItem.uri, foundItem.range); + } + runInstance.failed(foundItem, message); + } + } + + /** + * Handle test items that passed during execution + */ + private handleTestSuccess(keyTemp: string, runInstance: TestRun): void { + const grabTestItem = this.runIdToTestItem.get(keyTemp); + + if (grabTestItem !== undefined) { + const foundItem = this.findTestItemByIdEfficient(keyTemp); + if (foundItem?.uri) { + runInstance.passed(grabTestItem); + } + } + } + + /** + * Handle test items that were skipped during execution + */ + private handleTestSkipped(keyTemp: string, runInstance: TestRun): void { + const grabTestItem = this.runIdToTestItem.get(keyTemp); + + if (grabTestItem !== undefined) { + const foundItem = this.findTestItemByIdEfficient(keyTemp); + if (foundItem?.uri) { + runInstance.skipped(grabTestItem); + } + } + } + + /** + * Handle subtest failures + */ + private handleSubtestFailure(keyTemp: string, testItem: any, runInstance: TestRun): void { + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); + const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); + + if (parentTestItem) { + const subtestStats = this.subTestStats.get(parentTestCaseId); + if (subtestStats) { + subtestStats.failed += 1; + } else { + this.subTestStats.set(parentTestCaseId, { + failed: 1, + passed: 0, + }); + clearAllChildren(parentTestItem); + } + + const subTestItem = this.testController?.createTestItem(subtestId, subtestId, parentTestItem.uri); + + if (subTestItem) { + const traceback = testItem.traceback ?? ''; + const text = `${testItem.subtest} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + const message = new TestMessage(text); + if (parentTestItem.uri && parentTestItem.range) { + message.location = new Location(parentTestItem.uri, parentTestItem.range); + } + runInstance.failed(subTestItem, message); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } + + /** + * Handle subtest successes + */ + private handleSubtestSuccess(keyTemp: string, runInstance: TestRun): void { + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); + const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); + + if (parentTestItem) { + const subtestStats = this.subTestStats.get(parentTestCaseId); + if (subtestStats) { + subtestStats.passed += 1; + } else { + this.subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); + clearAllChildren(parentTestItem); + } + + const subTestItem = this.testController?.createTestItem(subtestId, subtestId, parentTestItem.uri); + + if (subTestItem) { + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + runInstance.passed(subTestItem); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } + + /** + * Process test execution results and update VS Code's Test Explorer with outcomes. + * Uses efficient lookup methods to handle large numbers of test results. + */ public _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): void { const rawTestExecData = payload as ExecutionTestPayload; if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { - // Map which holds the subtest information for each test item. - - // iterate through payload and update the UI accordingly. for (const keyTemp of Object.keys(rawTestExecData.result)) { - const testCases: TestItem[] = []; - - // grab leaf level test items - this.testController.items.forEach((i) => { - const tempArr: TestItem[] = getTestCaseNodes(i); - testCases.push(...tempArr); - }); const testItem = rawTestExecData.result[keyTemp]; + // Delegate to specific outcome handlers using efficient lookups if (testItem.outcome === 'error') { - const rawTraceback = testItem.traceback ?? ''; - const traceback = splitLines(rawTraceback, { - trim: false, - removeEmptyEntries: true, - }).join('\r\n'); - const text = `${testItem.test} failed with error: ${ - testItem.message ?? testItem.outcome - }\r\n${traceback}`; - const message = new TestMessage(text); - - const grabVSid = this.runIdToVSid.get(keyTemp); - // search through freshly built array of testItem to find the failed test and update UI. - testCases.forEach((indiItem) => { - if (indiItem.id === grabVSid) { - if (indiItem.uri) { - if (indiItem.range) { - message.location = new Location(indiItem.uri, indiItem.range); - } - runInstance.errored(indiItem, message); - } - } - }); + this.handleTestError(keyTemp, testItem, runInstance); } else if (testItem.outcome === 'failure' || testItem.outcome === 'passed-unexpected') { - const rawTraceback = testItem.traceback ?? ''; - const traceback = splitLines(rawTraceback, { - trim: false, - removeEmptyEntries: true, - }).join('\r\n'); - - const text = `${testItem.test} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; - const message = new TestMessage(text); - - // note that keyTemp is a runId for unittest library... - const grabVSid = this.runIdToVSid.get(keyTemp); - // search through freshly built array of testItem to find the failed test and update UI. - testCases.forEach((indiItem) => { - if (indiItem.id === grabVSid) { - if (indiItem.uri) { - if (indiItem.range) { - message.location = new Location(indiItem.uri, indiItem.range); - } - runInstance.failed(indiItem, message); - } - } - }); + this.handleTestFailure(keyTemp, testItem, runInstance); } else if (testItem.outcome === 'success' || testItem.outcome === 'expected-failure') { - const grabTestItem = this.runIdToTestItem.get(keyTemp); - const grabVSid = this.runIdToVSid.get(keyTemp); - if (grabTestItem !== undefined) { - testCases.forEach((indiItem) => { - if (indiItem.id === grabVSid) { - if (indiItem.uri) { - runInstance.passed(grabTestItem); - } - } - }); - } + this.handleTestSuccess(keyTemp, runInstance); } else if (testItem.outcome === 'skipped') { - const grabTestItem = this.runIdToTestItem.get(keyTemp); - const grabVSid = this.runIdToVSid.get(keyTemp); - if (grabTestItem !== undefined) { - testCases.forEach((indiItem) => { - if (indiItem.id === grabVSid) { - if (indiItem.uri) { - runInstance.skipped(grabTestItem); - } - } - }); - } + this.handleTestSkipped(keyTemp, runInstance); } else if (testItem.outcome === 'subtest-failure') { - // split on [] or () based on how the subtest is setup. - const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); - const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); - const data = testItem; - // find the subtest's parent test item - if (parentTestItem) { - const subtestStats = this.subTestStats.get(parentTestCaseId); - if (subtestStats) { - subtestStats.failed += 1; - } else { - this.subTestStats.set(parentTestCaseId, { - failed: 1, - passed: 0, - }); - // clear since subtest items don't persist between runs - clearAllChildren(parentTestItem); - } - const subTestItem = this.testController?.createTestItem( - subtestId, - subtestId, - parentTestItem.uri, - ); - // create a new test item for the subtest - if (subTestItem) { - const traceback = data.traceback ?? ''; - const text = `${data.subtest} failed: ${ - testItem.message ?? testItem.outcome - }\r\n${traceback}`; - parentTestItem.children.add(subTestItem); - runInstance.started(subTestItem); - const message = new TestMessage(text); - if (parentTestItem.uri && parentTestItem.range) { - message.location = new Location(parentTestItem.uri, parentTestItem.range); - } - runInstance.failed(subTestItem, message); - } else { - throw new Error('Unable to create new child node for subtest'); - } - } else { - throw new Error('Parent test item not found'); - } + this.handleSubtestFailure(keyTemp, testItem, runInstance); } else if (testItem.outcome === 'subtest-success') { - // split on [] or () based on how the subtest is setup. - const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); - const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); - - // find the subtest's parent test item - if (parentTestItem) { - const subtestStats = this.subTestStats.get(parentTestCaseId); - if (subtestStats) { - subtestStats.passed += 1; - } else { - this.subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); - // clear since subtest items don't persist between runs - clearAllChildren(parentTestItem); - } - const subTestItem = this.testController?.createTestItem( - subtestId, - subtestId, - parentTestItem.uri, - ); - // create a new test item for the subtest - if (subTestItem) { - parentTestItem.children.add(subTestItem); - runInstance.started(subTestItem); - runInstance.passed(subTestItem); - } else { - throw new Error('Unable to create new child node for subtest'); - } - } else { - throw new Error('Parent test item not found'); - } + this.handleSubtestSuccess(keyTemp, runInstance); } } } diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index dcd78dc23dba..97c04d5dfdf1 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -9,7 +9,11 @@ import * as fs from 'fs'; import * as os from 'os'; import * as sinon from 'sinon'; import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; -import { ITestController, ITestResultResolver } from '../../../client/testing/testController/common/types'; +import { + ITestController, + ITestResultResolver, + ExecutionTestPayload, +} from '../../../client/testing/testController/common/types'; import { IPythonExecutionFactory } from '../../../client/common/process/types'; import { IConfigurationService } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; @@ -1033,4 +1037,206 @@ suite('End to End Tests: test adapters', () => { assert.strictEqual(failureOccurred, false, failureMsg); }); }); + + test('_resolveExecution performance test: validates efficient test result processing', async () => { + // This test validates that _resolveExecution processes test results efficiently + // without expensive tree rebuilding or linear searching operations. + // + // The test ensures that processing many test results (like parameterized tests) + // remains fast and doesn't cause performance issues or stack overflow. + + // ================================================================ + // SETUP: Initialize test environment and tracking variables + // ================================================================ + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + + // Performance tracking variables + let totalCallTime = 0; + let callCount = 0; + const callTimes: number[] = []; + let treeRebuildCount = 0; + let totalSearchOperations = 0; + + // Test configuration - Moderate scale to validate efficiency + const numTestFiles = 5; // Multiple test files + const testFunctionsPerFile = 10; // Test functions per file + const totalTestItems = numTestFiles * testFunctionsPerFile; // Total test items in mock tree + const numParameterizedResults = 15; // Number of parameterized test results to process + + // ================================================================ + // MOCK: Set up spies and function wrapping to track performance + // ================================================================ + + // Mock getTestCaseNodes to track expensive tree operations + const originalGetTestCaseNodes = require('../../../client/testing/testController/common/testItemUtilities') + .getTestCaseNodes; + const getTestCaseNodesSpy = sinon.stub().callsFake((item) => { + treeRebuildCount++; + const result = originalGetTestCaseNodes(item); + // Track search operations through tree items + // Safely handle undefined results + if (result && Array.isArray(result)) { + totalSearchOperations += result.length; + } + return result || []; // Return empty array if undefined + }); + + // Replace the real function with our spy + const testItemUtilities = require('../../../client/testing/testController/common/testItemUtilities'); + testItemUtilities.getTestCaseNodes = getTestCaseNodesSpy; + + // Wrap the _resolveExecution function to measure performance + const original_resolveExecution = resultResolver._resolveExecution.bind(resultResolver); + resultResolver._resolveExecution = async (payload, runInstance) => { + const startTime = performance.now(); + callCount++; + + // Call the actual implementation + await original_resolveExecution(payload, runInstance); + + const endTime = performance.now(); + const callTime = endTime - startTime; + callTimes.push(callTime); + totalCallTime += callTime; + + return Promise.resolve(); + }; + + // ================================================================ + // SETUP: Create test data that simulates realistic test scenarios + // ================================================================ + + // Create a mock TestController with the methods we need + const mockTestController = { + items: new Map(), + createTestItem: (id: string, label: string, uri?: Uri) => { + const childrenMap = new Map(); + // Add forEach method to children map to simulate TestItemCollection + (childrenMap as any).forEach = function (callback: (item: any) => void) { + Map.prototype.forEach.call(this, callback); + }; + + const mockTestItem = { + id, + label, + uri, + children: childrenMap, + parent: undefined, + canResolveChildren: false, + tags: [{ id: 'python-run' }, { id: 'python-debug' }], + }; + return mockTestItem; + }, + // Add a forEach method to simulate the problematic iteration + forEach: function (callback: (item: any) => void) { + this.items.forEach(callback); + }, + }; // Replace the testController in our resolver + (resultResolver as any).testController = mockTestController; + + // Create test controller with many test items (simulates real workspace) + for (let i = 0; i < numTestFiles; i++) { + const testItem = mockTestController.createTestItem( + `test_file_${i}`, + `Test File ${i}`, + Uri.file(`/test_${i}.py`), + ); + mockTestController.items.set(`test_file_${i}`, testItem); + + // Add child test items to each file + for (let j = 0; j < testFunctionsPerFile; j++) { + const childItem = mockTestController.createTestItem( + `test_${i}_${j}`, + `test_method_${j}`, + Uri.file(`/test_${i}.py`), + ); + testItem.children.set(`test_${i}_${j}`, childItem); + + // Set up the ID mappings that the resolver uses + resultResolver.runIdToTestItem.set(`test_${i}_${j}`, childItem as any); + resultResolver.runIdToVSid.set(`test_${i}_${j}`, `test_${i}_${j}`); + resultResolver.vsIdToRunId.set(`test_${i}_${j}`, `test_${i}_${j}`); + } + } // Create payload with multiple test results (simulates real test execution) + const testResults: Record = {}; + for (let i = 0; i < numParameterizedResults; i++) { + testResults[`test_0_${i % 20}`] = { + test: `test_method[${i}]`, + outcome: 'success', + message: null, + traceback: null, + subtest: null, + }; + } + + const payload: ExecutionTestPayload = { + cwd: '/test', + status: 'success' as const, + error: '', + result: testResults, + }; + + const mockRunInstance = { + passed: sinon.stub(), + failed: sinon.stub(), + errored: sinon.stub(), + skipped: sinon.stub(), + }; + + // ================================================================ + // EXECUTION: Run the performance test + // ================================================================ + + const overallStartTime = performance.now(); + + // Run the _resolveExecution function with test data + await resultResolver._resolveExecution(payload, mockRunInstance as any); + + const overallEndTime = performance.now(); + const totalTime = overallEndTime - overallStartTime; + + // ================================================================ + // CLEANUP: Restore original functions + // ================================================================ + testItemUtilities.getTestCaseNodes = originalGetTestCaseNodes; + + // ================================================================ + // ASSERT: Verify efficient performance characteristics + // ================================================================ + console.log(`\n=== PERFORMANCE RESULTS ===`); + console.log( + `Test setup: ${numTestFiles} files × ${testFunctionsPerFile} test functions = ${totalTestItems} total items`, + ); + console.log(`Total execution time: ${totalTime.toFixed(2)}ms`); + console.log(`Tree operations performed: ${treeRebuildCount}`); + console.log(`Search operations: ${totalSearchOperations}`); + console.log(`Average time per call: ${(totalCallTime / callCount).toFixed(2)}ms`); + console.log(`Results processed: ${numParameterizedResults}`); + + // Basic function call verification + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + + // EFFICIENCY VERIFICATION: Ensure minimal expensive operations + assert.strictEqual( + treeRebuildCount, + 0, + 'Expected ZERO tree rebuilds - efficient implementation should use cached lookups', + ); + + assert.strictEqual( + totalSearchOperations, + 0, + 'Expected ZERO linear search operations - efficient implementation should use direct lookups', + ); + + // Performance threshold verification - should be fast + assert.ok(totalTime < 100, `Function should complete quickly, took ${totalTime}ms (should be under 100ms)`); + + // Scalability check - time should not grow significantly with more results + const timePerResult = totalTime / numParameterizedResults; + assert.ok( + timePerResult < 10, + `Time per result should be minimal: ${timePerResult.toFixed(2)}ms per result (should be under 10ms)`, + ); + }); }); From f3675b0526405a01e8f6889824aa6cab2a2d1f85 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Sat, 20 Sep 2025 11:13:42 -0700 Subject: [PATCH 1027/1136] Add meta-instruction files for learnings and testing feature area (#25473) --- .github/instructions/learning.instructions.md | 34 ++++ .../testing_feature_area.instructions.md | 181 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 .github/instructions/learning.instructions.md create mode 100644 .github/instructions/testing_feature_area.instructions.md diff --git a/.github/instructions/learning.instructions.md b/.github/instructions/learning.instructions.md new file mode 100644 index 000000000000..28b085f486ce --- /dev/null +++ b/.github/instructions/learning.instructions.md @@ -0,0 +1,34 @@ +--- +applyTo: '**' +description: This document describes how to deal with learnings that you make. (meta instruction) +--- + +This document describes how to deal with learnings that you make. +It is a meta-instruction file. + +Structure of learnings: + +- Each instruction file has a "Learnings" section. +- Each learning has a counter that indicates how often that learning was useful (initially 1). +- Each learning has a 1 sentence description of the learning that is clear and concise. + +Example: + +```markdown +## Learnings + +- Prefer `const` over `let` whenever possible (1) +- Avoid `any` type (3) +``` + +When the user tells you "learn!", you should: + +- extract a learning from the recent conversation + _ identify the problem that you created + _ identify why it was a problem + _ identify how you were told to fix it/how the user fixed it + _ generate only one learning (1 sentence) that helps to summarize the insight gained +- then, add the reflected learning to the "Learnings" section of the most appropriate instruction file + +Important: Whenever a learning was really useful, increase the counter!! +When a learning was not useful and just caused more problems, decrease the counter. diff --git a/.github/instructions/testing_feature_area.instructions.md b/.github/instructions/testing_feature_area.instructions.md new file mode 100644 index 000000000000..9036ab4e3acd --- /dev/null +++ b/.github/instructions/testing_feature_area.instructions.md @@ -0,0 +1,181 @@ +--- +applyTo: 'src/client/testing/**' +--- + +# Testing feature area — Discovery, Run, Debug, and Results + +This document maps the testing support in the extension: discovery, execution (run), debugging, result reporting and how those pieces connect to the codebase. It's written for contributors and agents who need to navigate, modify, or extend test support (both `unittest` and `pytest`). + +## Overview + +- Purpose: expose Python tests in the VS Code Test Explorer (TestController), support discovery, run, debug, and surface rich results and outputs. +- Scope: provider-agnostic orchestration + provider-specific adapters, TestController mapping, IPC with Python-side scripts, debug launch integration, and configuration management. + +## High-level architecture + +- Controller / UI bridge: orchestrates TestController requests and routes them to workspace adapters. +- Workspace adapter: provider-agnostic coordinator that translates TestController requests to provider adapters and maps payloads back into TestItems/TestRuns. +- Provider adapters: implement discovery/run/debug for `unittest` and `pytest` by launching Python scripts and wiring named-pipe IPC. +- Result resolver: translates Python-side JSON/IPCPayloads into TestController updates (start/pass/fail/output/attachments). +- Debug launcher: prepares debug sessions and coordinates the debugger attach flow with the Python runner. + +## Key components (files and responsibilities) + +- Entrypoints + - `src/client/testing/testController/controller.ts` — `PythonTestController` (main orchestrator). + - `src/client/testing/serviceRegistry.ts` — DI/wiring for testing services. +- Workspace orchestration + - `src/client/testing/testController/workspaceTestAdapter.ts` — `WorkspaceTestAdapter` (provider-agnostic entry used by controller). +- Provider adapters + - Unittest + - `src/client/testing/testController/unittest/testDiscoveryAdapter.ts` + - `src/client/testing/testController/unittest/testExecutionAdapter.ts` + - Pytest + - `src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts` + - `src/client/testing/testController/pytest/pytestExecutionAdapter.ts` +- Result resolution and helpers + - `src/client/testing/testController/common/resultResolver.ts` — `PythonResultResolver` (maps payload -> TestController updates). + - `src/client/testing/testController/common/testItemUtilities.ts` — helpers for TestItem lifecycle. + - `src/client/testing/testController/common/types.ts` — `ITestDiscoveryAdapter`, `ITestExecutionAdapter`, `ITestResultResolver`, `ITestDebugLauncher`. + - `src/client/testing/testController/common/debugLauncher.ts` — debug session creation helper. + - `src/client/testing/testController/common/utils.ts` — named-pipe helpers and command builders (`startDiscoveryNamedPipe`, etc.). +- Configuration + - `src/client/testing/common/testConfigurationManager.ts` — per-workspace test settings. + - `src/client/testing/configurationFactory.ts` — configuration service factory. +- Utilities & glue + - `src/client/testing/utils.ts` — assorted helpers used by adapters. + - Python-side scripts: `python_files/unittestadapter/*`, `python_files/pytestadapter/*` — discovery/run code executed by adapters. + +## Python subprocess runners (what runs inside Python) + +The adapters in the extension don't implement test discovery/run logic themselves — they spawn a Python subprocess that runs small helper scripts located under `python_files/` and stream structured events back to the extension over the named-pipe IPC. This is a central part of the feature area; changes here usually require coordinated edits in both the TypeScript adapters and the Python scripts. + +- Unittest helpers (folder: `python_files/unittestadapter`) + + - `discovery.py` — performs `unittest` discovery and emits discovery payloads (test suites, cases, locations) on the IPC channel. + - `execution.py` / `django_test_runner.py` — run tests for `unittest` and, where applicable, Django test runners; emit run events (start, stdout/stderr, pass, fail, skip, teardown) and attachment info. + - `pvsc_utils.py`, `django_handler.py` — utility helpers used by the runners for environment handling and Django-specific wiring. + - The adapter TypeScript files (`testDiscoveryAdapter.ts`, `testExecutionAdapter.ts`) construct the command line, start a named-pipe listener, and spawn these Python scripts using the extension's ExecutionFactory (activated interpreter) so the scripts execute inside the user's selected environment. + +- Pytest helpers (folder: `python_files/vscode_pytest`) + + - `_common.py` — shared helpers for pytest runner scripts. + - `run_pytest_script.py` — the primary pytest runner used for discovery and execution; emits the same structured IPC payloads the extension expects (discovery events and run events). + - The `pytest` execution adapter (`pytestExecutionAdapter.ts`) and discovery adapter build the CLI to run `run_pytest_script.py`, start the pipe, and translate incoming payloads via `PythonResultResolver`. + +- IPC contract and expectations + + - Adapters rely on a stable JSON payload contract emitted by the Python scripts: identifiers for tests, event types (discovered, collected, started, passed, failed, skipped), timings, error traces, and optional attachments (logs, captured stdout/stderr, file links). + - The extension maps these payloads to `TestItem`/`TestRun` updates via `PythonResultResolver` (`src/client/testing/testController/common/resultResolver.ts`). If you change payload shape, update the resolver and tests concurrently. + +- How the subprocess is started + - Execution adapters use the extension's `ExecutionFactory` (preferred) to get an activated interpreter and then spawn a child process that runs the helper script. The adapter will set up environment variables and command-line args (including the pipe name / run-id) so the Python runner knows where to send events and how to behave (discovery vs run vs debug). + - For debug sessions a debug-specific entry argument/port is passed and `common/debugLauncher.ts` coordinates starting a VS Code debug session that will attach to the Python process. + +## Core functionality (what to change where) + +- Discovery + - Entry: `WorkspaceTestAdapter.discoverTests` → provider discovery adapter. Adapter starts a named-pipe listener, spawns the discovery script in an activated interpreter, forwards discovery events to `PythonResultResolver` which creates/updates TestItems. + - Files: `workspaceTestAdapter.ts`, `*DiscoveryAdapter.ts`, `resultResolver.ts`, `testItemUtilities.ts`. +- Run / Execution + - Entry: `WorkspaceTestAdapter.executeTests` → provider execution adapter. Adapter spawns runner in an activated env, runner streams run events to the pipe, `PythonResultResolver` updates a `TestRun` with start/pass/fail and attachments. + - Files: `workspaceTestAdapter.ts`, `*ExecutionAdapter.ts`, `resultResolver.ts`. +- Debugging + - Flow: debug request flows like a run but goes through `debugLauncher.ts` to create a VS Code debug session with prepared ports/pipes. The Python runner coordinates attach/continue with the debugger. + - Files: `*ExecutionAdapter.ts`, `common/debugLauncher.ts`, `common/types.ts`. +- Result reporting + - `resultResolver.ts` is the canonical place to change how JSON payloads map to TestController constructs (messages, durations, error traces, attachments). + +## Typical workflows (short) + +- Full discovery + + 1. `PythonTestController` triggers discovery -> `WorkspaceTestAdapter.discoverTests`. + 2. Provider discovery adapter starts pipe and launches Python discovery script. + 3. Discovery events -> `PythonResultResolver` -> TestController tree updated. + +- Run tests + + 1. Controller collects TestItems -> creates `TestRun`. + 2. `WorkspaceTestAdapter.executeTests` delegates to execution adapter which launches the runner. + 3. Runner events arrive via pipe -> `PythonResultResolver` updates `TestRun`. + 4. On process exit the run is finalized. + +- Debug a test + 1. Debug request flows to execution adapter. + 2. Adapter prepares ports and calls `debugLauncher` to start a VS Code debug session with the run ID. + 3. Runner coordinates with the debugger; `PythonResultResolver` still receives and applies run events. + +## Tests and examples to inspect + +- Unit/integration tests for adapters and orchestration under `src/test/` (examples): + - `src/test/testing/common/testingAdapter.test.ts` + - `src/test/testing/testController/workspaceTestAdapter.unit.test.ts` + - `src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts` + - Adapter tests demonstrate expected telemetry, debug-launch payloads and result resolution. + +## History & evolution (brief) + +- Migration to TestController API: the code organizes around VS Code TestController, mapping legacy adapter behaviour into TestItems/TestRuns. +- Named-pipe IPC: discovery/run use named-pipe IPC to stream events from Python runner scripts (`python_files/*`) which enables richer, incremental updates and debug coordination. +- Environment activation: adapters prefer the extension ExecutionFactory (activated interpreter) to run discovery and test scripts. + +## Pointers for contributors (practical) + +- To extend discovery output: update the Python discovery script in `python_files/*` and `resultResolver.ts` to parse new payload fields. +- To change run behaviour (args/env/timouts): update the provider execution adapter (`*ExecutionAdapter.ts`) and add/update tests under `src/test/`. +- To change debug flow: edit `common/debugLauncher.ts` and adapters' debug paths; update tests that assert launch argument shapes. + +## Django support (how it works) + +- The extension supports Django projects by delegating discovery and execution to Django-aware Python helpers under `python_files/unittestadapter`. + - `python_files/unittestadapter/django_handler.py` contains helpers that invoke `manage.py` for discovery or execute Django test runners inside the project context. + - `python_files/unittestadapter/django_test_runner.py` provides `CustomDiscoveryTestRunner` and `CustomExecutionTestRunner` which integrate with the extension by using the same IPC contract (they use `UnittestTestResult` and `send_post_request` to emit discovery/run payloads). +- How adapters pass Django configuration: + - Execution adapters set environment variables (e.g. `MANAGE_PY_PATH`) and modify `PYTHONPATH` so Django code and the custom test runner are importable inside the spawned subprocess. + - For discovery the adapter may run the discovery helper which calls `manage.py test` with a custom test runner that emits discovery payloads instead of executing tests. +- Practical notes for contributors: + - Changes to Django discovery/execution often require edits in both `django_test_runner.py`/`django_handler.py` and the TypeScript adapters (`testDiscoveryAdapter.ts` / `testExecutionAdapter.ts`). + - The Django test runner expects `TEST_RUN_PIPE` environment variable to be present to send IPC events (see `django_test_runner.py`). + +## Settings referenced by this feature area + +- The extension exposes several `python.testing.*` settings used by adapters and configuration code (declared in `package.json`): + - `python.testing.pytestEnabled`, `python.testing.unittestEnabled` — enable/disable frameworks. + - `python.testing.pytestPath`, `python.testing.pytestArgs`, `python.testing.unittestArgs` — command path and CLI arguments used when spawning helper scripts. + - `python.testing.cwd` — optional working directory used when running discovery/runs. + - `python.testing.autoTestDiscoverOnSaveEnabled`, `python.testing.autoTestDiscoverOnSavePattern` — control automatic discovery on save. + - `python.testing.debugPort` — default port used for debug runs. + - `python.testing.promptToConfigure` — whether to prompt users to configure tests when potential test folders are found. +- Where to look in the code: + - Settings are consumed by `src/client/testing/common/testConfigurationManager.ts`, `src/client/testing/configurationFactory.ts`, and adapters under `src/client/testing/testController/*` which read settings to build CLI args and env for subprocesses. + - The setting definitions and descriptions are in `package.json` and localized strings in `package.nls.json`. + +## Coverage support (how it works) + +- Coverage is supported by running the Python helper scripts with coverage enabled and then collecting a coverage payload from the runner. + - Pytest-side coverage logic lives in `python_files/vscode_pytest/__init__.py` (checks `COVERAGE_ENABLED`, imports `coverage`, computes per-file metrics and emits a `CoveragePayloadDict`). + - Unittest adapters enable coverage by setting environment variable(s) (e.g. `COVERAGE_ENABLED`) when launching the subprocess; adapters and `resultResolver.ts` handle the coverage profile kind (`TestRunProfileKind.Coverage`). +- Flow summary: + 1. User starts a Coverage run via Test Explorer (profile kind `Coverage`). + 2. Controller/adapters set `COVERAGE_ENABLED` (or equivalent) in the subprocess env and invoke the runner script. + 3. The Python runner collects coverage (using `coverage` or `pytest-cov`), builds a file-level coverage map, and sends a coverage payload back over the IPC. + 4. `PythonResultResolver` (`src/client/testing/testController/common/resultResolver.ts`) receives the coverage payload and stores `detailedCoverageMap` used by the TestController profile to show file-level coverage details. +- Tests that exercise coverage flows are under `src/test/testing/*` and `python_files/tests/*` (see `testingAdapter.test.ts` and adapter unit tests that assert `COVERAGE_ENABLED` is set appropriately). + +## Interaction with the VS Code API + +- TestController API + - The feature area is built on VS Code's TestController/TestItem/TestRun APIs (`vscode.tests.createTestController` / `tests.createTestController` in the code). The controller creates a `TestController` in `src/client/testing/testController/controller.ts` and synchronizes `TestItem` trees with discovery payloads. + - `PythonResultResolver` maps incoming JSON events to VS Code API calls: `testRun.appendOutput`, `testRun.passed/failed/skipped`, `testRun.end`, and `TestItem` updates (labels, locations, children). +- Debug API + - Debug runs use the Debug API to start an attach/launch session. The debug launcher implementation is in `src/client/testing/testController/common/debugLauncher.ts` which constructs a debug configuration and calls the VS Code debug API to start a session (e.g. `vscode.debug.startDebugging`). + - Debug adapter/resolver code in the extension's debugger modules may also be used when attaching to Django or test subprocesses. +- Commands and configuration + - The Test Controller wires commands that appear in the Test Explorer and editor context menus (see `package.json` contributes `commands`) and listens to configuration changes filtered by `python.testing` in `src/client/testing/main.ts`. +- Execution factory & activated environments + - Adapters use the extension `ExecutionFactory` to spawn subprocesses in an activated interpreter (so the user's venv/conda is used). This involves the extension's internal environment execution APIs and sometimes `envExt` helpers when the external environment extension is present. + +## Learnings + +- Never await `showErrorMessage()` calls in test execution adapters as it blocks the test UI thread and freezes the Test Explorer (1) From 89c7e7224b86fc3833cf1889bd028501136d0ee9 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Sat, 20 Sep 2025 11:42:07 -0700 Subject: [PATCH 1028/1136] add "copy test id" to menu (#25475) fixes https://github.com/microsoft/vscode-python/issues/25476 --- .github/instructions/testing_feature_area.instructions.md | 6 ++++++ package.json | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/.github/instructions/testing_feature_area.instructions.md b/.github/instructions/testing_feature_area.instructions.md index 9036ab4e3acd..038dc1025ea5 100644 --- a/.github/instructions/testing_feature_area.instructions.md +++ b/.github/instructions/testing_feature_area.instructions.md @@ -173,9 +173,15 @@ The adapters in the extension don't implement test discovery/run logic themselve - Debug adapter/resolver code in the extension's debugger modules may also be used when attaching to Django or test subprocesses. - Commands and configuration - The Test Controller wires commands that appear in the Test Explorer and editor context menus (see `package.json` contributes `commands`) and listens to configuration changes filtered by `python.testing` in `src/client/testing/main.ts`. +- The "Copy Test ID" command (`python.copyTestId`) can be accessed from both the Test Explorer context menu (`testing/item/context`) and the editor gutter icon context menu (`testing/item/gutter`). This command copies test identifiers to the clipboard in the appropriate format for the active test framework (pytest path format or unittest module.class.method format). - Execution factory & activated environments - Adapters use the extension `ExecutionFactory` to spawn subprocesses in an activated interpreter (so the user's venv/conda is used). This involves the extension's internal environment execution APIs and sometimes `envExt` helpers when the external environment extension is present. ## Learnings - Never await `showErrorMessage()` calls in test execution adapters as it blocks the test UI thread and freezes the Test Explorer (1) +- VS Code test-related context menus are contributed to using both `testing/item/context` and `testing/item/gutter` menu locations in package.json for full coverage (1) + +``` + +``` diff --git a/package.json b/package.json index 0530726310f2..9bb3886d4fdc 100644 --- a/package.json +++ b/package.json @@ -1244,6 +1244,13 @@ "when": "controllerId == 'python-tests'" } ], + "testing/item/gutter": [ + { + "command": "python.copyTestId", + "group": "navigation", + "when": "controllerId == 'python-tests'" + } + ], "commandPalette": [ { "category": "Python", From df1e567f233871d2f333248583418c844c67bc06 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Sat, 20 Sep 2025 11:48:45 -0700 Subject: [PATCH 1029/1136] Add implementation and usage instruction prompts for VS Code components (#25477) --- .../extract-impl-instructions.prompt.md | 79 +++++++++++++++++++ .../extract-usage-instructions.prompt.md | 30 +++++++ 2 files changed, 109 insertions(+) create mode 100644 .github/prompts/extract-impl-instructions.prompt.md create mode 100644 .github/prompts/extract-usage-instructions.prompt.md diff --git a/.github/prompts/extract-impl-instructions.prompt.md b/.github/prompts/extract-impl-instructions.prompt.md new file mode 100644 index 000000000000..c2fb08b443c7 --- /dev/null +++ b/.github/prompts/extract-impl-instructions.prompt.md @@ -0,0 +1,79 @@ +--- +mode: edit +--- + +Analyze the specified part of the VS Code Python Extension codebase to generate or update implementation instructions in `.github/instructions/.instructions.md`. + +## Task + +Create concise developer guidance focused on: + +### Implementation Essentials + +- **Core patterns**: How this component is typically implemented and extended +- **Key interfaces**: Essential classes, services, and APIs with usage examples +- **Integration points**: How this component interacts with other extension parts +- **Common tasks**: Typical development scenarios with step-by-step guidance + +### Content Structure + +````markdown +--- +description: 'Implementation guide for the part of the Python Extension' +--- + +# Implementation Guide + +## Overview + +Brief description of the component's purpose and role in VS Code Python Extension. + +## Key Concepts + +- Main abstractions and their responsibilities +- Important interfaces and base classes + +## Common Implementation Patterns + +### Pattern 1: [Specific Use Case] + +```typescript +// Code example showing typical implementation +``` +```` + +### Pattern 2: [Another Use Case] + +```typescript +// Another practical example +``` + +## Integration Points + +- How this component connects to other VS Code Python Extension systems +- Required services and dependencies +- Extension points and contribution models + +## Essential APIs + +- Key methods and interfaces developers need +- Common parameters and return types + +## Gotchas and Best Practices + +- Non-obvious behaviors to watch for +- Performance considerations +- Common mistakes to avoid + +``` + +## Guidelines +- **Be specific**: Use actual class names, method signatures, and file paths +- **Show examples**: Include working code snippets from the codebase +- **Target implementation**: Focus on how to build with/extend this component +- **Keep it actionable**: Every section should help developers accomplish tasks + +Source conventions from existing `.github/instructions/*.instructions.md`, `CONTRIBUTING.md`, and codebase patterns. + +If `.github/instructions/.instructions.md` exists, intelligently merge new insights with existing content. +``` diff --git a/.github/prompts/extract-usage-instructions.prompt.md b/.github/prompts/extract-usage-instructions.prompt.md new file mode 100644 index 000000000000..ea48f162a220 --- /dev/null +++ b/.github/prompts/extract-usage-instructions.prompt.md @@ -0,0 +1,30 @@ +--- +mode: edit +--- + +Analyze the user requested part of the codebase (use a suitable ) to generate or update `.github/instructions/.instructions.md` for guiding developers and AI coding agents. + +Focus on practical usage patterns and essential knowledge: + +- How to use, extend, or integrate with this code area +- Key architectural patterns and conventions specific to this area +- Common implementation patterns with code examples +- Integration points and typical interaction patterns with other components +- Essential gotchas and non-obvious behaviors + +Source existing conventions from `.github/instructions/*.instructions.md`, `CONTRIBUTING.md`, and `README.md`. + +Guidelines: + +- Write concise, actionable instructions using markdown structure +- Document discoverable patterns with concrete examples +- If `.github/instructions/.instructions.md` exists, merge intelligently +- Target developers who need to work with or extend this code area + +Update `.github/instructions/.instructions.md` with header: + +``` +--- +description: "How to work with the part of the codebase" +--- +``` From 19a2f3468b6e114ddf219bbe9282b0e2d16e85e8 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:33:54 -0700 Subject: [PATCH 1030/1136] bump to v2025.16.0 (#25497) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2775ed2d88cb..fb32e01ddba6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.15.0-dev", + "version": "2025.16.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.15.0-dev", + "version": "2025.16.0", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 9bb3886d4fdc..516b35e31e18 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.15.0-dev", + "version": "2025.16.0", "featureFlags": { "usingNewInterpreterStorage": true }, From 9cb5ae71b07ad63feccf9ffe7f84a8162103548e Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:49:46 -0700 Subject: [PATCH 1031/1136] bump v2025.17.0-dev (#25498) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb32e01ddba6..d6391e017d88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.16.0", + "version": "2025.17.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.16.0", + "version": "2025.17.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 516b35e31e18..f54d359963a4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.16.0", + "version": "2025.17.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 9cba9a050a80f8a854444cb68013fe17ddef20eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 08:22:19 -0700 Subject: [PATCH 1032/1136] Bump actions/setup-node from 5 to 6 in /.github/actions/lint (#25528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6.
Release notes

Sourced from actions/setup-node's releases.

v6.0.0

What's Changed

Breaking Changes

Dependency Upgrades

Full Changelog: https://github.com/actions/setup-node/compare/v5...v6.0.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/lint/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml index 9971c0fbcf96..0bd5a2d8e1e2 100644 --- a/.github/actions/lint/action.yml +++ b/.github/actions/lint/action.yml @@ -10,7 +10,7 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ inputs.node_version }} cache: 'npm' From e6cd7aa2d11a257ceb5aab748c2f2e6d3743c5e0 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:00:22 -0700 Subject: [PATCH 1033/1136] support new env kind from pet `venvUv` (#25532) python extension will handle envs created by uv the same as any venvs, so just adding the `venvUv` kind to map to `venv`. The python environments extension will handle uv environments differently and not just bundle them by default --- .../base/locators/common/nativePythonUtils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts b/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts index 86135924537f..a4dfad724082 100644 --- a/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts +++ b/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts @@ -23,6 +23,7 @@ export enum NativePythonEnvironmentKind { VirtualEnvWrapper = 'VirtualEnvWrapper', WindowsStore = 'WindowsStore', WindowsRegistry = 'WindowsRegistry', + VenvUv = 'VenvUv', } const mapping = new Map([ @@ -36,6 +37,7 @@ const mapping = new Map([ [NativePythonEnvironmentKind.VirtualEnv, PythonEnvKind.VirtualEnv], [NativePythonEnvironmentKind.VirtualEnvWrapper, PythonEnvKind.VirtualEnvWrapper], [NativePythonEnvironmentKind.Venv, PythonEnvKind.Venv], + [NativePythonEnvironmentKind.VenvUv, PythonEnvKind.Venv], [NativePythonEnvironmentKind.WindowsRegistry, PythonEnvKind.System], [NativePythonEnvironmentKind.WindowsStore, PythonEnvKind.MicrosoftStore], [NativePythonEnvironmentKind.Homebrew, PythonEnvKind.System], From 900ae9c7ac772ce6296209290982d379e4cc9abb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:01:54 -0700 Subject: [PATCH 1034/1136] Bump actions/setup-node from 5 to 6 (#25527) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6.
Release notes

Sourced from actions/setup-node's releases.

v6.0.0

What's Changed

Breaking Changes

Dependency Upgrades

Full Changelog: https://github.com/actions/setup-node/compare/v5...v6.0.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/pr-check.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 88b88ebc2876..b4ac46558b63 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -236,7 +236,7 @@ jobs: sparse-checkout-cone-mode: false - name: Install Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index c932c314682d..2af03922411c 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -233,7 +233,7 @@ jobs: sparse-checkout-cone-mode: false - name: Install Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' @@ -505,7 +505,7 @@ jobs: sparse-checkout-cone-mode: false - name: Install Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' From 99d5dc27f79fa310b336e880f40cd1d1ef04fa17 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:36:15 -0700 Subject: [PATCH 1035/1136] switch to uv for tag (#25551) support https://github.com/microsoft/python-environment-tools/pull/263 --- .../base/locators/common/nativePythonUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts b/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts index a4dfad724082..716bdd444633 100644 --- a/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts +++ b/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts @@ -23,7 +23,7 @@ export enum NativePythonEnvironmentKind { VirtualEnvWrapper = 'VirtualEnvWrapper', WindowsStore = 'WindowsStore', WindowsRegistry = 'WindowsRegistry', - VenvUv = 'VenvUv', + VenvUv = 'Uv', } const mapping = new Map([ From 2ce21a028fa9fd967b630146374183f56f21d3ad Mon Sep 17 00:00:00 2001 From: iBug Date: Fri, 7 Nov 2025 16:14:16 +0800 Subject: [PATCH 1036/1136] Fix microsoft/vscode#232420: Python REPL cursor drifting (#25521) It seems `readline` is only imported if `sys.platform != "win32"`, so this fix is applied on that condition too. Context: microsoft/vscode#232420 --- python_files/pythonrc.py | 4 +++- python_files/tests/test_shell_integration.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/python_files/pythonrc.py b/python_files/pythonrc.py index 005b06bcdd15..63c52bc009da 100644 --- a/python_files/pythonrc.py +++ b/python_files/pythonrc.py @@ -52,7 +52,9 @@ def __str__(self): result = "" # For non-windows allow recent_command history. if sys.platform != "win32": - result = "{command_executed}{command_line}{command_finished}{prompt_started}{prompt}{command_start}".format( + result = "{soh}{command_executed}{command_line}{command_finished}{prompt_started}{stx}{prompt}{soh}{command_start}{stx}".format( + soh="\001", + stx="\002", command_executed="\x1b]633;C\x07", command_line="\x1b]633;E;" + str(get_last_command()) + "\x07", command_finished="\x1b]633;D;" + str(exit_code) + "\x07", diff --git a/python_files/tests/test_shell_integration.py b/python_files/tests/test_shell_integration.py index 574edfc056b4..7503a725b6d1 100644 --- a/python_files/tests/test_shell_integration.py +++ b/python_files/tests/test_shell_integration.py @@ -17,7 +17,7 @@ def test_decoration_success(): if sys.platform != "win32" and (not is_wsl): assert ( result - == "\x1b]633;C\x07\x1b]633;E;None\x07\x1b]633;D;0\x07\x1b]633;A\x07>>> \x1b]633;B\x07" + == "\x01\x1b]633;C\x07\x1b]633;E;None\x07\x1b]633;D;0\x07\x1b]633;A\x07\x02>>> \x01\x1b]633;B\x07\x02" ) else: pass @@ -32,7 +32,7 @@ def test_decoration_failure(): if sys.platform != "win32" and (not is_wsl): assert ( result - == "\x1b]633;C\x07\x1b]633;E;None\x07\x1b]633;D;1\x07\x1b]633;A\x07>>> \x1b]633;B\x07" + == "\x01\x1b]633;C\x07\x1b]633;E;None\x07\x1b]633;D;1\x07\x1b]633;A\x07\x02>>> \x01\x1b]633;B\x07\x02" ) else: pass From cd771ca8b34b36495cf1bf25c8441569164a6917 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:19:45 -0800 Subject: [PATCH 1037/1136] Refactor test and discovery methods to remove overload (#25572) --- .../testing/testController/common/types.ts | 12 +++---- .../testing/testController/controller.ts | 8 ++--- .../pytest/pytestDiscoveryAdapter.ts | 6 ++-- .../pytest/pytestExecutionAdapter.ts | 34 +++++++++--------- .../unittest/testDiscoveryAdapter.ts | 6 ++-- .../unittest/testExecutionAdapter.ts | 34 +++++++++--------- .../testController/workspaceTestAdapter.ts | 36 +++++++++---------- .../workspaceTestAdapter.unit.test.ts | 31 +++++++++------- 8 files changed, 83 insertions(+), 84 deletions(-) diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index b4d95af6c30e..5c6796905024 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -155,11 +155,9 @@ export interface ITestResultResolver { _resolveCoverage(payload: CoveragePayload, runInstance: TestRun): void; } export interface ITestDiscoveryAdapter { - // ** first line old method signature, second line new method signature - discoverTests(uri: Uri): Promise; discoverTests( uri: Uri, - executionFactory?: IPythonExecutionFactory, + executionFactory: IPythonExecutionFactory, token?: CancellationToken, interpreter?: PythonEnvironment, ): Promise; @@ -167,14 +165,12 @@ export interface ITestDiscoveryAdapter { // interface for execution/runner adapter export interface ITestExecutionAdapter { - // ** first line old method signature, second line new method signature - runTests(uri: Uri, testIds: string[], profileKind?: boolean | TestRunProfileKind): Promise; runTests( uri: Uri, testIds: string[], - profileKind?: boolean | TestRunProfileKind, - runInstance?: TestRun, - executionFactory?: IPythonExecutionFactory, + profileKind: boolean | TestRunProfileKind | undefined, + runInstance: TestRun, + executionFactory: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, interpreter?: PythonEnvironment, ): Promise; diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index aefc97117da5..b38c9b0bcee1 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -276,8 +276,8 @@ export class PythonTestController implements ITestController, IExtensionSingleAc } await testAdapter.discoverTests( this.testController, - this.refreshCancellation.token, this.pythonExecFactory, + this.refreshCancellation.token, await this.interpreterService.getActiveInterpreter(workspace.uri), ); } else { @@ -302,8 +302,8 @@ export class PythonTestController implements ITestController, IExtensionSingleAc } await testAdapter.discoverTests( this.testController, - this.refreshCancellation.token, this.pythonExecFactory, + this.refreshCancellation.token, await this.interpreterService.getActiveInterpreter(workspace.uri), ); } else { @@ -453,9 +453,9 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.testController, runInstance, testItems, + this.pythonExecFactory, token, request.profile?.kind, - this.pythonExecFactory, this.debugLauncher, await this.interpreterService.getActiveInterpreter(workspace.uri), ); @@ -470,9 +470,9 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.testController, runInstance, testItems, + this.pythonExecFactory, token, request.profile?.kind, - this.pythonExecFactory, this.debugLauncher, await this.interpreterService.getActiveInterpreter(workspace.uri), ); diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 04258ddbddf2..308c9ba1f9bc 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -38,7 +38,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { async discoverTests( uri: Uri, - executionFactory?: IPythonExecutionFactory, + executionFactory: IPythonExecutionFactory, token?: CancellationToken, interpreter?: PythonEnvironment, ): Promise { @@ -69,7 +69,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { uri: Uri, discoveryPipeName: string, cSource: CancellationTokenSource, - executionFactory?: IPythonExecutionFactory, + executionFactory: IPythonExecutionFactory, interpreter?: PythonEnvironment, token?: CancellationToken, ): Promise { @@ -170,7 +170,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { resource: uri, interpreter, }; - const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + const execService = await executionFactory.createActivatedEnvironment(creationOptions); const execInfo = await execService?.getExecutablePath(); traceVerbose(`Executable path for pytest discovery: ${execInfo}.`); diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 053c497c56e0..3b2f9f7de33a 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -32,9 +32,9 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { async runTests( uri: Uri, testIds: string[], - profileKind?: TestRunProfileKind, - runInstance?: TestRun, - executionFactory?: IPythonExecutionFactory, + profileKind: boolean | TestRunProfileKind | undefined, + runInstance: TestRun, + executionFactory: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, interpreter?: PythonEnvironment, ): Promise { @@ -49,14 +49,14 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } }; const cSource = new CancellationTokenSource(); - runInstance?.token.onCancellationRequested(() => cSource.cancel()); + runInstance.token.onCancellationRequested(() => cSource.cancel()); const name = await utils.startRunResultNamedPipe( dataReceivedCallback, // callback to handle data received deferredTillServerClose, // deferred to resolve when server closes cSource.token, // token to cancel ); - runInstance?.token.onCancellationRequested(() => { + runInstance.token.onCancellationRequested(() => { traceInfo(`Test run cancelled, resolving 'TillServerClose' deferred for ${uri.fsPath}.`); }); @@ -82,9 +82,9 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testIds: string[], resultNamedPipeName: string, serverCancel: CancellationTokenSource, - runInstance?: TestRun, - profileKind?: TestRunProfileKind, - executionFactory?: IPythonExecutionFactory, + runInstance: TestRun, + profileKind: boolean | TestRunProfileKind | undefined, + executionFactory: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, interpreter?: PythonEnvironment, ): Promise { @@ -114,7 +114,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { interpreter, }; // need to check what will happen in the exec service is NOT defined and is null - const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + const execService = await executionFactory.createActivatedEnvironment(creationOptions); const execInfo = await execService?.getExecutablePath(); traceVerbose(`Executable path for pytest execution: ${execInfo}.`); @@ -144,14 +144,14 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { cwd, throwOnStdErr: true, env: mutableEnv, - token: runInstance?.token, + token: runInstance.token, }; if (debugBool) { const launchOptions: LaunchOptions = { cwd, args: testArgs, - token: runInstance?.token, + token: runInstance.token, testProvider: PYTEST_PROVIDER, runTestIdsPort: testIdsFileName, pytestPort: resultNamedPipeName, @@ -181,7 +181,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { args: runArgs, env: (mutableEnv as unknown) as { [key: string]: string }, }); - runInstance?.token.onCancellationRequested(() => { + runInstance.token.onCancellationRequested(() => { traceInfo(`Test run cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); proc.kill(); deferredTillExecClose.resolve(); @@ -189,11 +189,11 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { }); proc.stdout.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); - runInstance?.appendOutput(out); + runInstance.appendOutput(out); }); proc.stderr.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); - runInstance?.appendOutput(out); + runInstance.appendOutput(out); }); proc.onExit((code, signal) => { if (code !== 0) { @@ -218,7 +218,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { let resultProc: ChildProcess | undefined; - runInstance?.token.onCancellationRequested(() => { + runInstance.token.onCancellationRequested(() => { traceInfo(`Test run cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. if (resultProc) { @@ -235,11 +235,11 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // Displays output to user and ensure the subprocess doesn't run into buffer overflow. result?.proc?.stdout?.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); - runInstance?.appendOutput(out); + runInstance.appendOutput(out); }); result?.proc?.stderr?.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); - runInstance?.appendOutput(out); + runInstance.appendOutput(out); }); result?.proc?.on('exit', (code, signal) => { if (code !== 0) { diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 23d70568687f..a40e25153fbc 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -38,7 +38,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { public async discoverTests( uri: Uri, - executionFactory?: IPythonExecutionFactory, + executionFactory: IPythonExecutionFactory, token?: CancellationToken, ): Promise { const settings = this.configSettings.getSettings(uri); @@ -89,7 +89,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { testRunPipeName: string, cwd: string, cSource: CancellationTokenSource, - executionFactory?: IPythonExecutionFactory, + executionFactory: IPythonExecutionFactory, ): Promise { // get and edit env vars const mutableEnv = { @@ -157,7 +157,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { allowEnvironmentFetchExceptions: false, resource: options.workspaceFolder, }; - const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + const execService = await executionFactory.createActivatedEnvironment(creationOptions); const execInfo = await execService?.getExecutablePath(); traceVerbose(`Executable path for unittest discovery: ${execInfo}.`); diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 74572ea5c63c..cbc1d2985f84 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -42,9 +42,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { public async runTests( uri: Uri, testIds: string[], - profileKind?: TestRunProfileKind, - runInstance?: TestRun, - executionFactory?: IPythonExecutionFactory, + profileKind: boolean | TestRunProfileKind | undefined, + runInstance: TestRun, + executionFactory: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, ): Promise { // deferredTillServerClose awaits named pipe server close @@ -59,13 +59,13 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { } }; const cSource = new CancellationTokenSource(); - runInstance?.token.onCancellationRequested(() => cSource.cancel()); + runInstance.token.onCancellationRequested(() => cSource.cancel()); const name = await utils.startRunResultNamedPipe( dataReceivedCallback, // callback to handle data received deferredTillServerClose, // deferred to resolve when server closes cSource.token, // token to cancel ); - runInstance?.token.onCancellationRequested(() => { + runInstance.token.onCancellationRequested(() => { console.log(`Test run cancelled, resolving 'till TillAllServerClose' deferred for ${uri.fsPath}.`); // if canceled, stop listening for results deferredTillServerClose.resolve(); @@ -93,9 +93,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { testIds: string[], resultNamedPipeName: string, serverCancel: CancellationTokenSource, - runInstance?: TestRun, - profileKind?: TestRunProfileKind, - executionFactory?: IPythonExecutionFactory, + runInstance: TestRun, + profileKind: boolean | TestRunProfileKind | undefined, + executionFactory: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, ): Promise { const settings = this.configSettings.getSettings(uri); @@ -119,9 +119,9 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { workspaceFolder: uri, command, cwd, - profileKind, + profileKind: typeof profileKind === 'boolean' ? undefined : profileKind, testIds, - token: runInstance?.token, + token: runInstance.token, }; traceLog(`Running UNITTEST execution for the following test ids: ${testIds}`); @@ -145,7 +145,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { allowEnvironmentFetchExceptions: false, resource: options.workspaceFolder, }; - const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + const execService = await executionFactory.createActivatedEnvironment(creationOptions); const execInfo = await execService?.getExecutablePath(); traceVerbose(`Executable path for unittest execution: ${execInfo}.`); @@ -193,7 +193,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { args, env: (mutableEnv as unknown) as { [key: string]: string }, }); - runInstance?.token.onCancellationRequested(() => { + runInstance.token.onCancellationRequested(() => { traceInfo(`Test run cancelled, killing unittest subprocess for workspace ${uri.fsPath}`); proc.kill(); deferredTillExecClose.resolve(); @@ -201,11 +201,11 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { }); proc.stdout.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); - runInstance?.appendOutput(out); + runInstance.appendOutput(out); }); proc.stderr.on('data', (data) => { const out = utils.fixLogLinesNoTrailing(data.toString()); - runInstance?.appendOutput(out); + runInstance.appendOutput(out); }); proc.onExit((code, signal) => { if (code !== 0) { @@ -228,7 +228,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { let resultProc: ChildProcess | undefined; - runInstance?.token.onCancellationRequested(() => { + runInstance.token.onCancellationRequested(() => { traceInfo(`Test run cancelled, killing unittest subprocess for workspace ${cwd}.`); // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. if (resultProc) { @@ -246,11 +246,11 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { result?.proc?.stdout?.on('data', (data) => { const out = fixLogLinesNoTrailing(data.toString()); - runInstance?.appendOutput(`${out}`); + runInstance.appendOutput(`${out}`); }); result?.proc?.stderr?.on('data', (data) => { const out = fixLogLinesNoTrailing(data.toString()); - runInstance?.appendOutput(`${out}`); + runInstance.appendOutput(`${out}`); }); result?.proc?.on('exit', (code, signal) => { diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index a73acdaba5f0..75b9489f708e 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -42,9 +42,9 @@ export class WorkspaceTestAdapter { testController: TestController, runInstance: TestRun, includes: TestItem[], + executionFactory: IPythonExecutionFactory, token?: CancellationToken, profileKind?: boolean | TestRunProfileKind, - executionFactory?: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, interpreter?: PythonEnvironment, ): Promise { @@ -73,20 +73,18 @@ export class WorkspaceTestAdapter { } }); const testCaseIds = Array.from(testCaseIdsSet); - // ** execution factory only defined for new rewrite way - if (executionFactory !== undefined) { - await this.executionAdapter.runTests( - this.workspaceUri, - testCaseIds, - profileKind, - runInstance, - executionFactory, - debugLauncher, - interpreter, - ); - } else { - await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, profileKind); + if (executionFactory === undefined) { + throw new Error('Execution factory is required for test execution'); } + await this.executionAdapter.runTests( + this.workspaceUri, + testCaseIds, + profileKind, + runInstance, + executionFactory, + debugLauncher, + interpreter, + ); deferred.resolve(); } catch (ex) { // handle token and telemetry here @@ -116,8 +114,8 @@ export class WorkspaceTestAdapter { public async discoverTests( testController: TestController, + executionFactory: IPythonExecutionFactory, token?: CancellationToken, - executionFactory?: IPythonExecutionFactory, interpreter?: PythonEnvironment, ): Promise { sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: this.testProvider }); @@ -132,12 +130,10 @@ export class WorkspaceTestAdapter { this.discovering = deferred; try { - // ** execution factory only defined for new rewrite way - if (executionFactory !== undefined) { - await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory, token, interpreter); - } else { - await this.discoveryAdapter.discoverTests(this.workspaceUri); + if (executionFactory === undefined) { + throw new Error('Execution factory is required for test discovery'); } + await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory, token, interpreter); deferred.resolve(); } catch (ex) { sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: true }); diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts index aac07793ca66..6d2895ca2979 100644 --- a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -147,7 +147,7 @@ suite('Workspace test adapter', () => { const testProvider = 'unittest'; execFactory = typemoq.Mock.ofType(); - await workspaceTestAdapter.discoverTests(testController, undefined, execFactory.object); + await workspaceTestAdapter.discoverTests(testController, execFactory.object); sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, uriFoo, sinon.match.any, testProvider); @@ -166,7 +166,7 @@ suite('Workspace test adapter', () => { stubResultResolver, ); - await workspaceTestAdapter.discoverTests(testController, undefined, execFactory.object); + await workspaceTestAdapter.discoverTests(testController, execFactory.object); sinon.assert.calledOnce(discoverTestsStub); }); @@ -193,8 +193,8 @@ suite('Workspace test adapter', () => { ); // Try running discovery twice - const one = workspaceTestAdapter.discoverTests(testController); - const two = workspaceTestAdapter.discoverTests(testController); + const one = workspaceTestAdapter.discoverTests(testController, execFactory.object); + const two = workspaceTestAdapter.discoverTests(testController, execFactory.object); Promise.all([one, two]); @@ -215,7 +215,7 @@ suite('Workspace test adapter', () => { stubResultResolver, ); - await workspaceTestAdapter.discoverTests(testController, undefined, execFactory.object); + await workspaceTestAdapter.discoverTests(testController, execFactory.object); sinon.assert.calledWith(sendTelemetryStub, EventName.UNITTEST_DISCOVERY_DONE); assert.strictEqual(telemetryEvent.length, 2); @@ -238,7 +238,7 @@ suite('Workspace test adapter', () => { stubResultResolver, ); - await workspaceTestAdapter.discoverTests(testController); + await workspaceTestAdapter.discoverTests(testController, execFactory.object); sinon.assert.calledWith(sendTelemetryStub, EventName.UNITTEST_DISCOVERY_DONE); assert.strictEqual(telemetryEvent.length, 2); @@ -256,6 +256,7 @@ suite('Workspace test adapter', () => { let testControllerMock: typemoq.IMock; let telemetryEvent: { eventName: EventName; properties: Record }[] = []; let resultResolver: ResultResolver.PythonResultResolver; + let execFactory: typemoq.IMock; // Stubbed test controller (see comment around L.40) let testController: TestController; @@ -328,6 +329,7 @@ suite('Workspace test adapter', () => { executionTestsStub = sandbox.stub(UnittestTestExecutionAdapter.prototype, 'runTests'); sendTelemetryStub = sandbox.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + execFactory = typemoq.Mock.ofType(); runInstance = typemoq.Mock.ofType(); const testProvider = 'pytest'; @@ -384,7 +386,12 @@ suite('Workspace test adapter', () => { testControllerMock = typemoq.Mock.ofType(); testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); - await workspaceTestAdapter.executeTests(testController, runInstance.object, [mockTestItem1, mockTestItem2]); + await workspaceTestAdapter.executeTests( + testController, + runInstance.object, + [mockTestItem1, mockTestItem2], + execFactory.object, + ); runInstance.verify((r) => r.started(typemoq.It.isAny()), typemoq.Times.exactly(2)); }); @@ -400,7 +407,7 @@ suite('Workspace test adapter', () => { stubResultResolver, ); - await workspaceTestAdapter.executeTests(testController, runInstance.object, []); + await workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); sinon.assert.calledOnce(executionTestsStub); }); @@ -427,8 +434,8 @@ suite('Workspace test adapter', () => { ); // Try running discovery twice - const one = workspaceTestAdapter.executeTests(testController, runInstance.object, []); - const two = workspaceTestAdapter.executeTests(testController, runInstance.object, []); + const one = workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + const two = workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); Promise.all([one, two]); @@ -467,7 +474,7 @@ suite('Workspace test adapter', () => { const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); const testProvider = 'unittest'; - await workspaceTestAdapter.executeTests(testController, runInstance.object, []); + await workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider); @@ -487,7 +494,7 @@ suite('Workspace test adapter', () => { stubResultResolver, ); - await workspaceTestAdapter.executeTests(testController, runInstance.object, []); + await workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); sinon.assert.calledWith(sendTelemetryStub, EventName.UNITTEST_RUN_ALL_FAILED); assert.strictEqual(telemetryEvent.length, 1); From 56d3615a70be949bb202679cf8d8da5de0101751 Mon Sep 17 00:00:00 2001 From: Itai Hay <3392524+itaihay@users.noreply.github.com> Date: Mon, 10 Nov 2025 20:44:24 +0200 Subject: [PATCH 1038/1136] Move pytest test IDs file deletion to finally block (#25540) If the vscode-pytest execution is wrapped and re-triggered then the deletion of the file causes the second run the fail. Deleting the file on the finally block ensures that the pytest execution will work even if re-run. - Move the deletion of the test IDs temp file from before pytest execution to a finally block. - This ensures the temp file is always cleaned up, even if pytest execution fails or an exception occurs. - Move ids_path initialization outside the try block so it's accessible in the finally block for cleanup. context: https://github.com/microsoft/vscode-python/issues/15669 https://github.com/microsoft/vscode-python/issues/24406#issuecomment-3478348972 Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- python_files/vscode_pytest/run_pytest_script.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/python_files/vscode_pytest/run_pytest_script.py b/python_files/vscode_pytest/run_pytest_script.py index c0f5114b375c..8d30ba7e4399 100644 --- a/python_files/vscode_pytest/run_pytest_script.py +++ b/python_files/vscode_pytest/run_pytest_script.py @@ -51,20 +51,22 @@ def run_pytest(args): run_test_ids_pipe = os.environ.get("RUN_TEST_IDS_PIPE") if run_test_ids_pipe: + ids_path = pathlib.Path(run_test_ids_pipe) try: - # Read the test ids from the file, delete file, and run pytest. - ids_path = pathlib.Path(run_test_ids_pipe) + # Read the test ids from the file and run pytest. ids = ids_path.read_text(encoding="utf-8").splitlines() - try: - ids_path.unlink() - except Exception as e: - print("Error[vscode-pytest]: unable to delete temp file" + str(e)) arg_array = ["-p", "vscode_pytest", *args, *ids] print("Running pytest with args: " + str(arg_array)) pytest.main(arg_array) except Exception as e: print("Error[vscode-pytest]: unable to read testIds from temp file" + str(e)) run_pytest(args) + finally: + # Delete the test ids temp file. + try: + ids_path.unlink() + except Exception as e: + print("Error[vscode-pytest]: unable to delete temp file" + str(e)) else: print("Error[vscode-pytest]: RUN_TEST_IDS_PIPE env var is not set.") run_pytest(args) From 2b564e82a7b9e08761714ac103b145bcebc76e35 Mon Sep 17 00:00:00 2001 From: Dhanika Botejue <156615547+Dhanika-Botejue@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:38:32 -0500 Subject: [PATCH 1039/1136] docs: add periods to feature descriptions for consistency (#25562) - Add terminal periods to all items in 'Feature details' section - Fix punctuation inconsistency where 'Refactoring' required a period but others had none - Maintains all existing content and formatting --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ffc73d3232ed..6a9af73ec708 100644 --- a/README.md +++ b/README.md @@ -90,13 +90,13 @@ To see all available Python commands, open the Command Palette and type `Python` Learn more about the rich features of the Python extension: -- [IntelliSense](https://code.visualstudio.com/docs/python/editing#_autocomplete-and-intellisense): Edit your code with auto-completion, code navigation, syntax checking and more -- [Linting](https://code.visualstudio.com/docs/python/linting): Get additional code analysis with Pylint, Flake8 and more -- [Code formatting](https://code.visualstudio.com/docs/python/formatting): Format your code with black, autopep or yapf -- [Debugging](https://code.visualstudio.com/docs/python/debugging): Debug your Python scripts, web apps, remote or multi-threaded processes +- [IntelliSense](https://code.visualstudio.com/docs/python/editing#_autocomplete-and-intellisense): Edit your code with auto-completion, code navigation, syntax checking and more. +- [Linting](https://code.visualstudio.com/docs/python/linting): Get additional code analysis with Pylint, Flake8 and more. +- [Code formatting](https://code.visualstudio.com/docs/python/formatting): Format your code with black, autopep or yapf. +- [Debugging](https://code.visualstudio.com/docs/python/debugging): Debug your Python scripts, web apps, remote or multi-threaded processes. - [Testing](https://code.visualstudio.com/docs/python/unit-testing): Run and debug tests through the Test Explorer with unittest or pytest. -- [Jupyter Notebooks](https://code.visualstudio.com/docs/python/jupyter-support): Create and edit Jupyter Notebooks, add and run code cells, render plots, visualize variables through the variable explorer, visualize dataframes with the data viewer, and more -- [Environments](https://code.visualstudio.com/docs/python/environments): Automatically activate and switch between virtualenv, venv, pipenv, conda and pyenv environments +- [Jupyter Notebooks](https://code.visualstudio.com/docs/python/jupyter-support): Create and edit Jupyter Notebooks, add and run code cells, render plots, visualize variables through the variable explorer, visualize dataframes with the data viewer, and more. +- [Environments](https://code.visualstudio.com/docs/python/environments): Automatically activate and switch between virtualenv, venv, pipenv, conda and pyenv environments. - [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring): Restructure your Python code with variable extraction and method extraction. Additionally, there is componentized support to enable additional refactoring, such as import sorting, through extensions including [isort](https://marketplace.visualstudio.com/items?itemName=ms-python.isort) and [Ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff). From 0f5e1672f72678209a615035b659b11da0d47e7f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:38:46 -0800 Subject: [PATCH 1040/1136] separate out linked file check (#25573) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-vsix/action.yml | 2 +- .github/workflows/pr-file-check.yml | 14 ------------ .github/workflows/pr-issue-check.yml | 31 +++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/pr-issue-check.yml diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index bfe90fc940e8..6723dcab8fba 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -93,7 +93,7 @@ runs: VSIX_NAME: ${{ inputs.vsix_name }} - name: Upload VSIX - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ inputs.artifact_name }} path: ${{ inputs.vsix_name }} diff --git a/.github/workflows/pr-file-check.yml b/.github/workflows/pr-file-check.yml index da5d6fa8f696..6364e5fa744e 100644 --- a/.github/workflows/pr-file-check.yml +++ b/.github/workflows/pr-file-check.yml @@ -42,17 +42,3 @@ jobs: .github/test_plan.md skip-label: 'skip tests' failure-message: 'TypeScript code was edited without also editing a ${file-pattern} file; see the Testing page in our wiki on testing guidelines (the ${skip-label} label can be used to pass this check)' - - - name: 'Ensure PR has an associated issue' - uses: actions/github-script@v8 - with: - script: | - const labels = context.payload.pull_request.labels.map(label => label.name); - if (!labels.includes('skip-issue-check')) { - const prBody = context.payload.pull_request.body || ''; - const issueLink = prBody.match(/https:\/\/github\.com\/\S+\/issues\/\d+/); - const issueReference = prBody.match(/#\d+/); - if (!issueLink && !issueReference) { - core.setFailed('No associated issue found in the PR description.'); - } - } diff --git a/.github/workflows/pr-issue-check.yml b/.github/workflows/pr-issue-check.yml new file mode 100644 index 000000000000..25ac91bbd279 --- /dev/null +++ b/.github/workflows/pr-issue-check.yml @@ -0,0 +1,31 @@ +name: PR issue check + +on: + pull_request: + types: + - 'opened' + - 'reopened' + - 'synchronize' + - 'labeled' + - 'unlabeled' + +permissions: {} + +jobs: + check-for-attached-issue: + name: 'Check for attached issue' + runs-on: ubuntu-latest + steps: + - name: 'Ensure PR has an associated issue' + uses: actions/github-script@v8 + with: + script: | + const labels = context.payload.pull_request.labels.map(label => label.name); + if (!labels.includes('skip-issue-check')) { + const prBody = context.payload.pull_request.body || ''; + const issueLink = prBody.match(/https:\/\/github\.com\/\S+\/issues\/\d+/); + const issueReference = prBody.match(/#\d+/); + if (!issueLink && !issueReference) { + core.setFailed('No associated issue found in the PR description.'); + } + } From 6edf3145c9cd563be89b61f32b5ce68c8958c70c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:00:01 -0800 Subject: [PATCH 1041/1136] Bump 2025.19 dev (#25575) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d6391e017d88..9ee1d8242e15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.17.0-dev", + "version": "2025.19.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.17.0-dev", + "version": "2025.19.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index f54d359963a4..f7ba93bfe4dd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.17.0-dev", + "version": "2025.19.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 1e1b2c17cc4f0ac6df5aac439adce934e5d6bbd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:14:30 -0800 Subject: [PATCH 1042/1136] Bump actions/upload-artifact from 4 to 5 (#25549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
Release notes

Sourced from actions/upload-artifact's releases.

v5.0.0

What's Changed

BREAKING CHANGE: this update supports Node v24.x. This is not a breaking change per-se but we're treating it as such.

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v5.0.0

v4.6.2

What's Changed

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.6.2

v4.6.1

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.6.1

v4.6.0

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.6.0

v4.5.0

What's Changed

New Contributors

... (truncated)

Commits
  • 330a01c Merge pull request #734 from actions/danwkennedy/prepare-5.0.0
  • 03f2824 Update github.dep.yml
  • 905a1ec Prepare v5.0.0
  • 2d9f9cd Merge pull request #725 from patrikpolyak/patch-1
  • 9687587 Merge branch 'main' into patch-1
  • 2848b2c Merge pull request #727 from danwkennedy/patch-1
  • 9b51177 Spell out the first use of GHES
  • cd231ca Update GHES guidance to include reference to Node 20 version
  • de65e23 Merge pull request #712 from actions/nebuk89-patch-1
  • 8747d8c Update README.md
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 2af03922411c..102258fb2d18 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -680,7 +680,7 @@ jobs: run: npm run test:cover:report - name: Upload HTML report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ runner.os }}-coverage-report-html path: ./coverage From c4e5dfeb754efc68d52b73de493e8c3c2912a2a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:32:37 -0800 Subject: [PATCH 1043/1136] Bump tar-fs from 2.1.3 to 2.1.4 (#25487) Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.3 to 2.1.4.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tar-fs&package-manager=npm_and_yarn&previous-version=2.1.3&new-version=2.1.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- package-lock.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ee1d8242e15..7dfccfecb846 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13137,11 +13137,10 @@ } }, "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "chownr": "^1.1.1", @@ -25048,9 +25047,9 @@ "dev": true }, "tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, "optional": true, "requires": { From 2a41070cc1b8f7d09b6a03eb87e47a34f0d6cfbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:33:08 -0800 Subject: [PATCH 1044/1136] Bump peter-evans/find-comment from 3.1.0 to 4.0.0 (#25499) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [peter-evans/find-comment](https://github.com/peter-evans/find-comment) from 3.1.0 to 4.0.0.
Release notes

Sourced from peter-evans/find-comment's releases.

Find Comment v4.0.0

⚙️ Requires Actions Runner v2.327.1 or later if you are using a self-hosted runner for Node 24 support.

What's Changed

... (truncated)

Commits
  • b30e6a3 feat: v4 (#389)
  • b4929e7 build(deps-dev): bump @​types/node from 18.19.124 to 18.19.127 (#388)
  • 1f47d94 build(deps-dev): bump @​vercel/ncc from 0.38.3 to 0.38.4 (#387)
  • a723a15 build(deps): bump actions/setup-node from 4 to 5 (#386)
  • 8bacb1b build(deps-dev): bump @​types/node from 18.19.123 to 18.19.124 (#385)
  • 048de65 build(deps): bump actions/checkout from 4 to 5 (#384)
  • c02750f build(deps-dev): bump @​types/node from 18.19.122 to 18.19.123 (#383)
  • 092c582 build(deps): bump actions/download-artifact from 4 to 5 (#382)
  • c115bb0 build(deps-dev): bump eslint-plugin-prettier from 5.5.3 to 5.5.4 (#381)
  • 8d3be5d build(deps-dev): bump @​types/node from 18.19.121 to 18.19.122 (#380)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=peter-evans/find-comment&package-manager=github_actions&previous-version=3.1.0&new-version=4.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- .github/workflows/community-feedback-auto-comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/community-feedback-auto-comment.yml b/.github/workflows/community-feedback-auto-comment.yml index f606148f6e86..2274a6951ff6 100644 --- a/.github/workflows/community-feedback-auto-comment.yml +++ b/.github/workflows/community-feedback-auto-comment.yml @@ -12,7 +12,7 @@ jobs: issues: write steps: - name: Check For Existing Comment - uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 id: finder with: issue-number: ${{ github.event.issue.number }} From 4af0af5814ad75a87ef69ab7445844fc8306f6ff Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:17:24 -0800 Subject: [PATCH 1045/1136] Add telemetry for Python Environments extension use on activation (#25578) --- src/client/startupTelemetry.ts | 2 ++ src/client/telemetry/index.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/client/startupTelemetry.ts b/src/client/startupTelemetry.ts index 5a2c12e2dd37..f7a2a6aea517 100644 --- a/src/client/startupTelemetry.ts +++ b/src/client/startupTelemetry.ts @@ -136,6 +136,7 @@ async function getActivationTelemetryProps( const usingGlobalInterpreter = interpreter ? isUsingGlobalInterpreterInWorkspace(interpreter.path, serviceContainer) : false; + const usingEnvironmentsExtension = useEnvExtension(); return { condaVersion, @@ -148,5 +149,6 @@ async function getActivationTelemetryProps( usingGlobalInterpreter, appName, isFirstSession, + usingEnvironmentsExtension, }; } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 581fcfed1f63..738c5f8a2776 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -404,6 +404,10 @@ export interface IEventNamePropertyMapping { * to approximately guess if it's the first session. */ isFirstSession?: boolean; + /** + * If user has enabled the Python Environments extension integration + */ + usingEnvironmentsExtension?: boolean; }; /** * Telemetry event sent when substituting Environment variables to calculate value of variables From 2d28f7102ccbaa9e92b6fe13753324219413be6c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:20:58 -0800 Subject: [PATCH 1046/1136] Bump tomli from 2.2.1 to 2.3.0 (#25515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tomli](https://github.com/hukkin/tomli) from 2.2.1 to 2.3.0.
Changelog

Sourced from tomli's changelog.

2.3.0

  • Added
    • Binary wheels for Python 3.14 (also free-threaded)
  • Performance
    • Reduced import time
Commits
  • 3fccd16 Bump version: 2.2.1 → 2.3.0
  • 6504016 Add 2.3.0 changelog
  • 0bc66fc Remove now off-by-default PyPy from cibuildwheel skip list
  • 0aa242f Update license metadata to appease PEP 639
  • a18221e Bump GitHub CI actions
  • 6fa4d90 [pre-commit.ci] pre-commit autoupdate (#260)
  • b974fa1 [pre-commit.ci] pre-commit autoupdate (#248)
  • f574f36 Update mypy to 1.15 and use --strict mode (#257)
  • 1da01ef Reduce import time by removing typing import (#251)
  • 4188188 Reduce import time by removing string and tomli._types imports
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tomli&package-manager=pip&previous-version=2.2.1&new-version=2.3.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- requirements.txt | 76 +++++++++++-------- .../codeExecution/codeExecutionManager.ts | 10 +-- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/requirements.txt b/requirements.txt index dddc2ee9691c..1e5f673f43db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,39 +12,49 @@ packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f # via -r requirements.in -tomli==2.2.1 \ - --hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \ - --hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \ - --hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \ - --hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \ - --hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \ - --hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \ - --hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \ - --hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \ - --hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \ - --hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \ - --hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \ - --hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \ - --hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \ - --hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \ - --hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \ - --hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \ - --hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \ - --hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \ - --hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \ - --hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \ - --hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \ - --hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \ - --hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \ - --hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \ - --hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \ - --hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \ - --hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \ - --hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \ - --hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \ - --hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \ - --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ - --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 +tomli==2.3.0 \ + --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ + --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ + --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ + --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ + --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ + --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ + --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ + --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ + --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ + --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ + --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ + --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ + --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ + --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ + --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ + --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ + --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ + --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ + --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ + --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ + --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ + --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ + --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ + --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ + --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ + --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ + --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ + --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ + --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ + --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ + --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ + --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ + --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ + --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ + --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ + --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ + --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ + --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ + --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ + --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ + --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ + --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 # via -r requirements.in typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index 30e5b7facd2d..48165adcd169 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -131,6 +131,11 @@ export class CodeExecutionManager implements ICodeExecutionManager { return; } + const fileAfterSave = await codeExecutionHelper.saveFileIfDirty(fileToExecute); + if (fileAfterSave) { + fileToExecute = fileAfterSave; + } + // Check on setting terminal.executeInFileDir const pythonSettings = this.configSettings.getSettings(file); let cwd = pythonSettings.terminal.executeInFileDir ? path.dirname(fileToExecute.fsPath) : undefined; @@ -139,11 +144,6 @@ export class CodeExecutionManager implements ICodeExecutionManager { const launchArgs = pythonSettings.terminal.launchArgs; const totalArgs = [...launchArgs, fileToExecute.fsPath.fileToCommandArgumentForPythonExt()]; - const fileAfterSave = await codeExecutionHelper.saveFileIfDirty(fileToExecute); - if (fileAfterSave) { - fileToExecute = fileAfterSave; - } - const show = this.shouldTerminalFocusOnStart(fileToExecute); let terminal: Terminal | undefined; if (dedicated) { From 040e590334852ddc2183258f0d0c098f9a95f561 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 08:16:09 -0800 Subject: [PATCH 1047/1136] Bump github/codeql-action from 3 to 4 (#25511) --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 84de97c4dc9a..168ef0a05b3d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,4 +67,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 From e19c6bbedb371107888ecfe14489175286e1b038 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 11 Nov 2025 09:19:01 -0800 Subject: [PATCH 1048/1136] Enable proposed API for all launch configurations in tests (#25582) attempted fix for main failing on CI --- src/test/standardTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/standardTest.ts b/src/test/standardTest.ts index 00eb3d7cf8c4..c3a7968c9c7a 100644 --- a/src/test/standardTest.ts +++ b/src/test/standardTest.ts @@ -89,7 +89,7 @@ async function start() { console.log('VS Code executable', vscodeExecutablePath); const launchArgs = baseLaunchArgs .concat([workspacePath]) - .concat(channel === 'insiders' ? ['--enable-proposed-api'] : []) + .concat(['--enable-proposed-api']) .concat(['--timeout', '5000']); console.log(`Starting vscode ${channel} with args ${launchArgs.join(' ')}`); const options: TestOptions = { From 455a1303cfb7fd6b89772ae8887856e2d344622b Mon Sep 17 00:00:00 2001 From: Luciana Abud <45497113+luabud@users.noreply.github.com> Date: Tue, 11 Nov 2025 09:40:53 -0800 Subject: [PATCH 1049/1136] Update setting name (#25464) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a9af73ec708..e9dd52a538cd 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,6 @@ The Microsoft Python Extension for Visual Studio Code collects usage data and sends it to Microsoft to help improve our products and services. Read our [privacy statement](https://privacy.microsoft.com/privacystatement) to -learn more. This extension respects the `telemetry.enableTelemetry` +learn more. This extension respects the `telemetry.telemetryLevel` setting which you can learn more about at https://code.visualstudio.com/docs/supporting/faq#_how-to-disable-telemetry-reporting. From 00e529fe798fc744fdcaf53ddb2e089adb7cadea Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:10:46 -0800 Subject: [PATCH 1050/1136] fix flaky test (#25583) fix flaky smoke test by setting the setting for useEnvExt to false. Fixes test runs like this: https://github.com/microsoft/vscode-python/actions/runs/19273356638/job/55107677667 --- src/test/smoke/runInTerminal.smoke.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/smoke/runInTerminal.smoke.test.ts b/src/test/smoke/runInTerminal.smoke.test.ts index bd4c88e44e80..d5ce409f3ab1 100644 --- a/src/test/smoke/runInTerminal.smoke.test.ts +++ b/src/test/smoke/runInTerminal.smoke.test.ts @@ -17,8 +17,13 @@ suite('Smoke Test: Run Python File In Terminal', () => { return this.skip(); } await initialize(); + // Ensure the environments extension is not used for this test + await vscode.workspace + .getConfiguration('python') + .update('useEnvironmentsExtension', false, vscode.ConfigurationTarget.Global); return undefined; }); + setup(initializeTest); suiteTeardown(closeActiveWindows); teardown(closeActiveWindows); From 9fa6d8d8d0f75680c0f56ad84b05e742962c67fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 00:40:43 +0000 Subject: [PATCH 1051/1136] Bump actions/setup-node from 4 to 6 in /.github/actions/build-vsix (#25580) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
Release notes

Sourced from actions/setup-node's releases.

v6.0.0

What's Changed

Breaking Changes

Dependency Upgrades

Full Changelog: https://github.com/actions/setup-node/compare/v5...v6.0.0

v5.0.0

What's Changed

Breaking Changes

This update, introduces automatic caching when a valid packageManager field is present in your package.json. This aims to improve workflow performance and make dependency management more seamless. To disable this automatic caching, set package-manager-cache: false

steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
  with:
    package-manager-cache: false

Make sure your runner is on version v2.327.1 or later to ensure compatibility with this release. See Release Notes

Dependency Upgrades

New Contributors

Full Changelog: https://github.com/actions/setup-node/compare/v4...v5.0.0

v4.4.0

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=4&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- .github/actions/build-vsix/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index 6723dcab8fba..1b665363b34f 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -22,7 +22,7 @@ runs: using: 'composite' steps: - name: Install Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ inputs.node_version }} cache: 'npm' From cf088e884e2ca327968a8ac7e5917f4d7ced7f08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:49:32 -0800 Subject: [PATCH 1052/1136] Bump peter-evans/create-or-update-comment from 4.0.0 to 5.0.0 (#25503) --- .github/workflows/community-feedback-auto-comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/community-feedback-auto-comment.yml b/.github/workflows/community-feedback-auto-comment.yml index 2274a6951ff6..27f93400a023 100644 --- a/.github/workflows/community-feedback-auto-comment.yml +++ b/.github/workflows/community-feedback-auto-comment.yml @@ -21,7 +21,7 @@ jobs: - name: Add Community Feedback Comment if: steps.finder.outputs.comment-id == '' - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 with: issue-number: ${{ github.event.issue.number }} body: | From a51b29e708c1dce86d14edde6a1702d40ad0402f Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:06:34 -0800 Subject: [PATCH 1053/1136] Refactor test processing and folder construction in vscode_pytest module (#25478) --- .../python-quality-checks.instructions.md | 76 ++++++ .vscode/tasks.json | 11 +- package.json | 3 + python_files/vscode_pytest/__init__.py | 249 ++++++++++++------ 4 files changed, 255 insertions(+), 84 deletions(-) create mode 100644 .github/instructions/python-quality-checks.instructions.md diff --git a/.github/instructions/python-quality-checks.instructions.md b/.github/instructions/python-quality-checks.instructions.md new file mode 100644 index 000000000000..d07699965cc2 --- /dev/null +++ b/.github/instructions/python-quality-checks.instructions.md @@ -0,0 +1,76 @@ +--- +applyTo: 'python_files/**' +description: Guide for running and fixing Python quality checks (Ruff and Pyright) that run in CI +--- + +# Python Quality Checks — Ruff and Pyright + +Run the same Python quality checks that run in CI. All checks target `python_files/` and use config from `python_files/pyproject.toml`. + +## Commands + +```bash +npm run check-python # Run both Ruff and Pyright +npm run check-python:ruff # Linting and formatting only +npm run check-python:pyright # Type checking only +``` + +## Fixing Ruff Errors + +**Auto-fix most issues:** + +```bash +cd python_files +python -m ruff check . --fix +python -m ruff format +npm run check-python:ruff # Verify +``` + +**Manual fixes:** + +- Ruff shows file, line number, rule code (e.g., `F841`), and description +- Open the file, read the error, fix the code +- Common: line length (100 char max), import sorting, unused variables + +## Fixing Pyright Errors + +**Common patterns and fixes:** + +- **Undefined variable/import**: Add the missing import +- **Type mismatch**: Correct the type or add type annotations +- **Missing return type**: Add `-> ReturnType` to function signatures + ```python + def my_function() -> str: # Add return type + return "result" + ``` + +**Verify:** + +```bash +npm run check-python:pyright +``` + +## Configuration + +- **Ruff**: Line length 100, Python 3.9+, 40+ rule families (flake8, isort, pyupgrade, etc.) +- **Pyright**: Version 1.1.308 (or whatever is found in the environment), ignores `lib/` and 15+ legacy files +- Config: `python_files/pyproject.toml` sections `[tool.ruff]` and `[tool.pyright]` + +## Troubleshooting + +**"Module not found" in Pyright**: Install dependencies + +```bash +python -m pip install --upgrade -r build/test-requirements.txt +nox --session install_python_libs +``` + +**Import order errors**: Auto-fix with `ruff check . --fix` + +**Type errors in ignored files**: Legacy files in `pyproject.toml` ignore list—fix if working on them + +## Learnings + +- Always run `npm run check-python` before pushing to catch CI failures early (1) +- Use `ruff check . --fix` to auto-fix most linting issues before manual review (1) +- Pyright version must match CI (1.1.308) to avoid inconsistent results between local and CI runs (1) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e1468bdfc2ad..0e33420c11db 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,9 +12,7 @@ "type": "npm", "script": "compile", "isBackground": true, - "problemMatcher": [ - "$tsc-watch" - ], + "problemMatcher": ["$tsc-watch"], "group": { "kind": "build", "isDefault": true @@ -34,6 +32,13 @@ "script": "preTestJediLSP", "problemMatcher": [], "label": "preTestJediLSP" + }, + { + "type": "npm", + "script": "check-python", + "problemMatcher": ["$python"], + "label": "npm: check-python", + "detail": "npm run check-python:ruff && npm run check-python:pyright" } ] } diff --git a/package.json b/package.json index f7ba93bfe4dd..5d13d01033a8 100644 --- a/package.json +++ b/package.json @@ -1695,6 +1695,9 @@ "lint-fix": "eslint --fix src build pythonExtensionApi gulpfile.js", "format-check": "prettier --check 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", "format-fix": "prettier --write 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", + "check-python": "npm run check-python:ruff && npm run check-python:pyright", + "check-python:ruff": "cd python_files && python -m pip install -U ruff && python -m ruff check . && python -m ruff format --check", + "check-python:pyright": "cd python_files && npx --yes pyright@1.1.308 .", "clean": "gulp clean", "addExtensionPackDependencies": "gulp addExtensionPackDependencies", "updateBuildNumber": "gulp updateBuildNumber", diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 72eaa7a787d5..0eac4a74f4c3 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -10,7 +10,7 @@ import pathlib import sys import traceback -from typing import TYPE_CHECKING, Any, Dict, Generator, Literal, TypedDict +from typing import TYPE_CHECKING, Any, Dict, Generator, Literal, Protocol, TypedDict, cast import pytest @@ -25,6 +25,13 @@ USES_PYTEST_DESCRIBE = True +class HasPathOrFspath(Protocol): + """Protocol defining objects that have either a path or fspath attribute.""" + + path: pathlib.Path | None = None + fspath: Any | None = None + + class TestData(TypedDict): """A general class that all test objects inherit from.""" @@ -522,11 +529,130 @@ def pytest_sessionfinish(session, exitstatus): send_message(payload) +def construct_nested_folders( + file_nodes_dict: dict[str, TestNode], + session_node: TestNode, + session_children_dict: dict[str, TestNode], +) -> dict[str, TestNode]: + """Iterate through all files and construct them into nested folders. + + Keyword arguments: + file_nodes_dict -- Dictionary of all file nodes + session_node -- The session node that will be parent to the folder structure + session_children_dict -- Dictionary of session's children nodes indexed by ID + + Returns: + dict[str, TestNode] -- Updated session_children_dict with folder nodes added + """ + created_files_folders_dict: dict[str, TestNode] = {} + for file_node in file_nodes_dict.values(): + # Iterate through all the files that exist and construct them into nested folders. + root_folder_node: TestNode + try: + root_folder_node: TestNode = build_nested_folders( + file_node, created_files_folders_dict, session_node + ) + except ValueError: + # This exception is raised when the session node is not a parent of the file node. + print( + "[vscode-pytest]: Session path not a parent of test paths, adjusting session node to common parent." + ) + file_path_str: str = str(file_node["path"]) + session_path_str: str = str(session_node["path"]) + common_parent = os.path.commonpath([file_path_str, session_path_str]) + common_parent_path = pathlib.Path(common_parent) + print("[vscode-pytest]: Session node now set to: ", common_parent) + session_node["path"] = common_parent_path # pathlib.Path + session_node["id_"] = common_parent # str + session_node["name"] = common_parent_path.name # str + root_folder_node = build_nested_folders( + file_node, created_files_folders_dict, session_node + ) + # The final folder we get to is the highest folder in the path + # and therefore we add this as a child to the session. + root_id = root_folder_node.get("id_") + if root_id and root_id not in session_children_dict: + session_children_dict[root_id] = root_folder_node + + return session_children_dict + + +def process_parameterized_test( + test_case: pytest.Item, + test_node: TestItem, + function_nodes_dict: dict[str, TestNode], + file_nodes_dict: dict[str, TestNode], +) -> TestNode: + """Process a parameterized test case and create appropriate function nodes. + + Keyword arguments: + test_case -- the parameterized pytest test case; must have callspec attribute + test_node -- the test node created from the test case + function_nodes_dict -- dictionary of function nodes indexed by ID + file_nodes_dict -- dictionary of file nodes indexed by path + + Returns: + TestNode -- the node to use for further processing (function node or original test node) + """ + function_name: str = "" + # parameterized test cases cut the repetitive part of the name off. + parent_part, parameterized_section = test_node["name"].split("[", 1) + test_node["name"] = "[" + parameterized_section + + first_split = test_case.nodeid.rsplit( + "::", 1 + ) # splits the parameterized test name from the rest of the nodeid + second_split = first_split[0].rsplit( + ".py", 1 + ) # splits the file path from the rest of the nodeid + + class_and_method = second_split[1] + "::" # This has "::" separator at both ends + # construct the parent id, so it is absolute path :: any class and method :: parent_part + parent_id = os.fspath(get_node_path(test_case)) + class_and_method + parent_part + + try: + function_name = test_case.originalname # type: ignore + function_test_node = function_nodes_dict[parent_id] + except AttributeError: # actual error has occurred + ERRORS.append( + f"unable to find original name for {test_case.name} with parameterization detected." + ) + raise VSCodePytestError( + "Unable to find original name for parameterized test case" + ) from None + except KeyError: + function_test_node: TestNode = create_parameterized_function_node( + function_name, get_node_path(test_case), parent_id + ) + function_nodes_dict[parent_id] = function_test_node + + if test_node not in function_test_node["children"]: + function_test_node["children"].append(test_node) + + # Check if the parent node of the function is file, if so create/add to this file node. + if isinstance(test_case.parent, pytest.File): + # calculate the parent path of the test case + parent_path = get_node_path(test_case.parent) + try: + parent_test_case = file_nodes_dict[os.fspath(parent_path)] + except KeyError: + parent_test_case = create_file_node(parent_path) + file_nodes_dict[os.fspath(parent_path)] = parent_test_case + if function_test_node not in parent_test_case["children"]: + parent_test_case["children"].append(function_test_node) + + # Return the function node as the test node to handle subsequent nesting + return function_test_node + + def build_test_tree(session: pytest.Session) -> TestNode: """Builds a tree made up of testing nodes from the pytest session. Keyword arguments: - session -- the pytest session object. + session -- the pytest session object that contains test items. + + Returns: + TestNode -- The root node of the constructed test tree. """ session_node = create_session_node(session) session_children_dict: dict[str, TestNode] = {} @@ -542,54 +668,10 @@ def build_test_tree(session: pytest.Session) -> TestNode: for test_case in session.items: test_node = create_test_node(test_case) if hasattr(test_case, "callspec"): # This means it is a parameterized test. - function_name: str = "" - # parameterized test cases cut the repetitive part of the name off. - parent_part, parameterized_section = test_node["name"].split("[", 1) - test_node["name"] = "[" + parameterized_section - - first_split = test_case.nodeid.rsplit( - "::", 1 - ) # splits the parameterized test name from the rest of the nodeid - second_split = first_split[0].rsplit( - ".py", 1 - ) # splits the file path from the rest of the nodeid - - class_and_method = second_split[1] + "::" # This has "::" separator at both ends - # construct the parent id, so it is absolute path :: any class and method :: parent_part - parent_id = os.fspath(get_node_path(test_case)) + class_and_method + parent_part - # file, middle, param = test_case.nodeid.rsplit("::", 2) - # parent_id = test_case.nodeid.rsplit("::", 1)[0] + "::" + parent_part - # parent_path = os.fspath(get_node_path(test_case)) + "::" + parent_part - try: - function_name = test_case.originalname # type: ignore - function_test_node = function_nodes_dict[parent_id] - except AttributeError: # actual error has occurred - ERRORS.append( - f"unable to find original name for {test_case.name} with parameterization detected." - ) - raise VSCodePytestError( - "Unable to find original name for parameterized test case" - ) from None - except KeyError: - function_test_node: TestNode = create_parameterized_function_node( - function_name, get_node_path(test_case), parent_id - ) - function_nodes_dict[parent_id] = function_test_node - if test_node not in function_test_node["children"]: - function_test_node["children"].append(test_node) - # Check if the parent node of the function is file, if so create/add to this file node. - if isinstance(test_case.parent, pytest.File): - # calculate the parent path of the test case - parent_path = get_node_path(test_case.parent) - try: - parent_test_case = file_nodes_dict[os.fspath(parent_path)] - except KeyError: - parent_test_case = create_file_node(parent_path) - file_nodes_dict[os.fspath(parent_path)] = parent_test_case - if function_test_node not in parent_test_case["children"]: - parent_test_case["children"].append(function_test_node) - # If the parent is not a file, it is a class, add the function node as the test node to handle subsequent nesting. - test_node = function_test_node + # Process parameterized test and get the function node to use for further processing + test_node = process_parameterized_test( + test_case, test_node, function_nodes_dict, file_nodes_dict + ) if isinstance(test_case.parent, pytest.Class) or ( USES_PYTEST_DESCRIBE and isinstance(test_case.parent, DescribeBlock) ): @@ -629,40 +711,25 @@ def build_test_tree(session: pytest.Session) -> TestNode: test_file_node["children"].append(test_class_node) elif not hasattr(test_case, "callspec"): # This includes test cases that are pytest functions or a doctests. - parent_path = get_node_path(test_case.parent) + if test_case.parent is None: + ERRORS.append(f"Test case {test_case.name} has no parent") + continue + parent_path = get_node_path( + cast( + "pytest.Session | pytest.Item | pytest.File | pytest.Class | pytest.Module | HasPathOrFspath", + test_case.parent, + ) + ) try: parent_test_case = file_nodes_dict[os.fspath(parent_path)] except KeyError: parent_test_case = create_file_node(parent_path) file_nodes_dict[os.fspath(parent_path)] = parent_test_case parent_test_case["children"].append(test_node) - created_files_folders_dict: dict[str, TestNode] = {} - for file_node in file_nodes_dict.values(): - # Iterate through all the files that exist and construct them into nested folders. - root_folder_node: TestNode - try: - root_folder_node: TestNode = build_nested_folders( - file_node, created_files_folders_dict, session_node - ) - except ValueError: - # This exception is raised when the session node is not a parent of the file node. - print( - "[vscode-pytest]: Session path not a parent of test paths, adjusting session node to common parent." - ) - common_parent = os.path.commonpath([file_node["path"], get_node_path(session)]) - common_parent_path = pathlib.Path(common_parent) - print("[vscode-pytest]: Session node now set to: ", common_parent) - session_node["path"] = common_parent_path # pathlib.Path - session_node["id_"] = common_parent # str - session_node["name"] = common_parent_path.name # str - root_folder_node = build_nested_folders( - file_node, created_files_folders_dict, session_node - ) - # The final folder we get to is the highest folder in the path - # and therefore we add this as a child to the session. - root_id = root_folder_node.get("id_") - if root_id and root_id not in session_children_dict: - session_children_dict[root_id] = root_folder_node + # Process all files and construct them into nested folders + session_children_dict = construct_nested_folders( + file_nodes_dict, session_node, session_children_dict + ) session_node["children"] = list(session_children_dict.values()) return session_node @@ -851,12 +918,29 @@ class CoveragePayloadDict(Dict): error: str | None # Currently unused need to check -def get_node_path(node: Any) -> pathlib.Path: +def get_node_path( + node: pytest.Session + | pytest.Item + | pytest.File + | pytest.Class + | pytest.Module + | HasPathOrFspath, +) -> pathlib.Path: """A function that returns the path of a node given the switch to pathlib.Path. It also evaluates if the node is a symlink and returns the equivalent path. + + Parameters: + node: A pytest object or any object that has a path or fspath attribute. + Do NOT pass a pathlib.Path object directly; use it directly instead. + + Returns: + pathlib.Path: The resolved path for the node. """ - node_path = getattr(node, "path", None) or pathlib.Path(node.fspath) + node_path = getattr(node, "path", None) + if node_path is None: + fspath = getattr(node, "fspath", None) + node_path = pathlib.Path(fspath) if fspath is not None else None if not node_path: raise VSCodePytestError( @@ -868,7 +952,10 @@ def get_node_path(node: Any) -> pathlib.Path: # Get relative between the cwd (resolved path) and the node path. try: # Check to see if the node path contains the symlink root already - common_path = os.path.commonpath([SYMLINK_PATH, node_path]) + # Convert Path objects to strings for os.path.commonpath + symlink_str: str = str(SYMLINK_PATH) + node_path_str: str = str(node_path) + common_path = os.path.commonpath([symlink_str, node_path_str]) if common_path == os.fsdecode(SYMLINK_PATH): # The node path is already relative to the SYMLINK_PATH root therefore return return node_path From cd5ecb9c2fa69e7aeaf4778e87b4b3c32ab61435 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:54:39 -0800 Subject: [PATCH 1054/1136] parsing mixed-value toml arrays (#25585) Fixes: https://github.com/microsoft/vscode-python/issues/25413 --------- Signed-off-by: JP-Ellis Co-authored-by: JP-Ellis --- package-lock.json | 15 ++++++++------- package.json | 2 +- .../creation/pyProjectTomlContext.unit.test.ts | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7dfccfecb846..9a85807e6068 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2025.19.0-dev", "license": "MIT", "dependencies": { - "@iarna/toml": "^2.2.5", + "@iarna/toml": "^3.0.0", "@vscode/extension-telemetry": "^0.8.4", "arch": "^2.1.0", "fs-extra": "^11.2.0", @@ -1086,9 +1086,10 @@ "license": "BSD-3-Clause" }, "node_modules/@iarna/toml": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", - "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", + "license": "ISC" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -15947,9 +15948,9 @@ "dev": true }, "@iarna/toml": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", - "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==" }, "@isaacs/cliui": { "version": "8.0.2", diff --git a/package.json b/package.json index 5d13d01033a8..2d4e67425806 100644 --- a/package.json +++ b/package.json @@ -1705,7 +1705,7 @@ "webpack": "webpack" }, "dependencies": { - "@iarna/toml": "^2.2.5", + "@iarna/toml": "^3.0.0", "@vscode/extension-telemetry": "^0.8.4", "arch": "^2.1.0", "fs-extra": "^11.2.0", diff --git a/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts b/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts index 8363837a4a36..3e787570304a 100644 --- a/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts +++ b/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts @@ -28,7 +28,7 @@ function getInstallableToml(): typemoq.IMock { .setup((p) => p.getText(typemoq.It.isAny())) .returns( () => - '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[dependency-groups]\ndev = ["ruff", { include-group = "test" }]\ntest = ["pytest"]', ); return pyprojectToml; } From d970068f5ed38511dd856d9da511f39be1d6b455 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:42:43 -0800 Subject: [PATCH 1055/1136] support extra patching for doctest (#25591) fixes https://github.com/microsoft/vscode-python/issues/25469 --- .../python-quality-checks.instructions.md | 21 ++++++++ .../.data/doctest_patched_module.py | 17 +++++++ .../unittestadapter/.data/doctest_standard.py | 7 +++ .../.data/test_doctest_patched.py | 50 +++++++++++++++++++ .../.data/test_doctest_standard.py | 16 ++++++ .../tests/unittestadapter/test_utils.py | 46 +++++++++++++++++ python_files/unittestadapter/pvsc_utils.py | 14 +++--- 7 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 python_files/tests/unittestadapter/.data/doctest_patched_module.py create mode 100644 python_files/tests/unittestadapter/.data/doctest_standard.py create mode 100644 python_files/tests/unittestadapter/.data/test_doctest_patched.py create mode 100644 python_files/tests/unittestadapter/.data/test_doctest_standard.py diff --git a/.github/instructions/python-quality-checks.instructions.md b/.github/instructions/python-quality-checks.instructions.md index d07699965cc2..48f37529dfbc 100644 --- a/.github/instructions/python-quality-checks.instructions.md +++ b/.github/instructions/python-quality-checks.instructions.md @@ -69,8 +69,29 @@ nox --session install_python_libs **Type errors in ignored files**: Legacy files in `pyproject.toml` ignore list—fix if working on them +## When Writing Tests + +**Always format your test files before committing:** + +```bash +cd python_files +ruff format tests/ # Format all test files +# or format specific files: +ruff format tests/unittestadapter/test_utils.py +``` + +**Best practice workflow:** + +1. Write your test code +2. Run `ruff format` on the test files +3. Run the tests to verify they pass +4. Run `npm run check-python` to catch any remaining issues + +This ensures your tests pass both functional checks and quality checks in CI. + ## Learnings - Always run `npm run check-python` before pushing to catch CI failures early (1) - Use `ruff check . --fix` to auto-fix most linting issues before manual review (1) - Pyright version must match CI (1.1.308) to avoid inconsistent results between local and CI runs (1) +- Always run `ruff format` on test files after writing them to avoid formatting CI failures (1) diff --git a/python_files/tests/unittestadapter/.data/doctest_patched_module.py b/python_files/tests/unittestadapter/.data/doctest_patched_module.py new file mode 100644 index 000000000000..636c5320b6d6 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/doctest_patched_module.py @@ -0,0 +1,17 @@ +""" +Patched doctest module. +This module's doctests will be patched to have proper IDs. + +>>> 2 + 2 +4 +""" + + +def example_function(): + """ + Example function with doctest. + + >>> example_function() + 'works' + """ + return "works" diff --git a/python_files/tests/unittestadapter/.data/doctest_standard.py b/python_files/tests/unittestadapter/.data/doctest_standard.py new file mode 100644 index 000000000000..52a10aa46a7f --- /dev/null +++ b/python_files/tests/unittestadapter/.data/doctest_standard.py @@ -0,0 +1,7 @@ +""" +Standard doctest module that should be blocked. +This has a simple doctest with short ID. + +>>> 2 + 2 +4 +""" diff --git a/python_files/tests/unittestadapter/.data/test_doctest_patched.py b/python_files/tests/unittestadapter/.data/test_doctest_patched.py new file mode 100644 index 000000000000..3a719c7139ca --- /dev/null +++ b/python_files/tests/unittestadapter/.data/test_doctest_patched.py @@ -0,0 +1,50 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Test file with patched doctest integration that should work.""" + +import unittest +import doctest +import sys +import doctest_patched_module + + +# Patch DocTestCase to modify test IDs to be compatible with the extension +original_init = doctest.DocTestCase.__init__ + + +def patched_init(self, test, optionflags=0, setUp=None, tearDown=None, checker=None): + """Patch to modify doctest names to have proper hierarchy.""" + if hasattr(test, 'name'): + # Get module name + module_hierarchy = test.name.split('.') + module_name = module_hierarchy[0] if module_hierarchy else 'unknown' + + # Reconstruct with proper formatting to have enough components + # Format: module.file.class.function + if test.filename.endswith('.py'): + file_base = test.filename.split('/')[-1].replace('.py', '') + test_name = test.name.split('.')[-1] if '.' in test.name else test.name + # Create a properly formatted ID with enough components + test.name = f"{module_name}.{file_base}._DocTests.{test_name}" + + # Call original init + original_init(self, test, optionflags, setUp, tearDown, checker) + + +# Apply the patch +doctest.DocTestCase.__init__ = patched_init + + +def load_tests(loader, tests, ignore): + """ + Standard hook for unittest to load tests. + This uses patched doctest to create compatible test IDs. + """ + tests.addTests(doctest.DocTestSuite(doctest_patched_module)) + return tests + + +# Clean up the patch after loading +def tearDownModule(): + """Restore original DocTestCase.__init__""" + doctest.DocTestCase.__init__ = original_init diff --git a/python_files/tests/unittestadapter/.data/test_doctest_standard.py b/python_files/tests/unittestadapter/.data/test_doctest_standard.py new file mode 100644 index 000000000000..f5dba1209b98 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/test_doctest_standard.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Test file with standard doctest integration that should be blocked.""" + +import unittest +import doctest +import doctest_standard + + +def load_tests(loader, tests, ignore): + """ + Standard hook for unittest to load tests. + This uses standard doctest without any patching. + """ + tests.addTests(doctest.DocTestSuite(doctest_standard)) + return tests diff --git a/python_files/tests/unittestadapter/test_utils.py b/python_files/tests/unittestadapter/test_utils.py index 390b0779a44d..dc8a81175e70 100644 --- a/python_files/tests/unittestadapter/test_utils.py +++ b/python_files/tests/unittestadapter/test_utils.py @@ -291,3 +291,49 @@ def test_build_empty_tree() -> None: assert tests is not None assert tests.get("children") == [] assert not errors + + +def test_doctest_standard_blocked() -> None: + """Standard doctests with short IDs should be skipped with an error message.""" + start_dir = os.fsdecode(TEST_DATA_PATH) + pattern = "test_doctest_standard*" + + loader = unittest.TestLoader() + suite = loader.discover(start_dir, pattern) + tests, errors = build_test_tree(suite, start_dir) + + # Should return a tree but with no test children (since doctests are skipped) + assert tests is not None + # Check that we got an error about doctests not being supported + assert len(errors) > 0 + assert "Skipping doctest as it is not supported for the extension" in errors[0] + + +def test_doctest_patched_works() -> None: + """Patched doctests with properly formatted IDs should be processed normally.""" + start_dir = os.fsdecode(TEST_DATA_PATH) + pattern = "test_doctest_patched*" + + loader = unittest.TestLoader() + suite = loader.discover(start_dir, pattern) + tests, errors = build_test_tree(suite, start_dir) + + # Should successfully build a tree with the patched doctest + assert tests is not None + + # The patched doctests should have proper IDs and be included + # We should find at least one test child (the doctests that were patched) + def count_tests(node): + """Recursively count test nodes.""" + if node.get("type_") == "test": + return 1 + count = 0 + for child in node.get("children", []): + count += count_tests(child) + return count + + test_count = count_tests(tests) + # We expect at least the module doctest and function doctest + assert test_count > 0, "Patched doctests should be included in the tree" + # Should not have doctest-related errors since they're properly formatted + assert not any("doctest" in str(e).lower() for e in errors) diff --git a/python_files/unittestadapter/pvsc_utils.py b/python_files/unittestadapter/pvsc_utils.py index 09a5ec9f3be5..e9d7bc092992 100644 --- a/python_files/unittestadapter/pvsc_utils.py +++ b/python_files/unittestadapter/pvsc_utils.py @@ -203,12 +203,6 @@ def build_test_tree( root = build_test_node(top_level_directory, directory_path.name, TestNodeTypeEnum.folder) for test_case in get_test_case(suite): - if isinstance(test_case, doctest.DocTestCase): - print( - "Skipping doctest as it is not supported for the extension. Test case: ", test_case - ) - error = ["Skipping doctest as it is not supported for the extension."] - continue test_id = test_case.id() if test_id.startswith("unittest.loader._FailedTest"): error.append(str(test_case._exception)) # type: ignore # noqa: SLF001 @@ -221,6 +215,14 @@ def build_test_tree( else: # Get the static test path components: filename, class name and function name. components = test_id.split(".") + # Check if this is a doctest with insufficient components that would cause unpacking to fail + if len(components) < 3 and isinstance(test_case, doctest.DocTestCase): + print( + "Skipping doctest as it is not supported for the extension. Test case: ", + test_case, + ) + error = ["Skipping doctest as it is not supported for the extension."] + continue *folders, filename, class_name, function_name = components py_filename = f"{filename}.py" From d7af377c7b14958fbef34f1d864215b34cb31fd7 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 23:42:47 +0000 Subject: [PATCH 1056/1136] Fix: Open file browser at workspace root when selecting interpreter path (#25520) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem When users click "Enter interpreter path..." and then select "Find..." to browse for a Python interpreter, the file browser dialog opens at the user's home directory instead of the current workspace root. This creates a frustrating experience when trying to navigate to virtual environments (like `.venv`) or other interpreters located within the project directory. ![File browser opening at wrong location](https://github.com/user-attachments/assets/8cbb89e6-8f1b-4ff2-8f15-d44ef6a7d01f) ## Solution Added the `defaultUri` parameter to the `showOpenDialog` call in the `_enterOrBrowseInterpreterPath` method. This parameter is set to `state.workspace`, which contains the current workspace folder URI when a workspace is open. ```typescript const uris = await this.applicationShell.showOpenDialog({ filters: this.platformService.isWindows ? filtersObject : undefined, openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, canSelectMany: false, title: InterpreterQuickPickList.browsePath.title, defaultUri: state.workspace, // ← Added this line }); ``` ## Impact - **With workspace open:** File browser now opens at the workspace root directory, making it easy to find `.venv` folders and project-specific interpreters - **Without workspace open:** Behavior unchanged - file browser opens at the default location (typically user's home directory) ## Testing - Updated all existing unit tests to include the new `defaultUri` parameter in expected values - Added new test case: "If `Browse...` option is selected with workspace, file browser opens at workspace root" - All 35 tests in the "Set Interpreter Command" suite pass ✅ Fixes issue where users expected the file browser to open in their current project directory when browsing for interpreters, improving the user experience when setting up virtual environments in VS Code. Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- .../commands/setInterpreter.ts | 1 + .../commands/setInterpreter.unit.test.ts | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 54440485da02..a629d1bc793c 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -554,6 +554,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, canSelectMany: false, title: InterpreterQuickPickList.browsePath.title, + defaultUri: state.workspace, }); if (uris && uris.length > 0) { state.path = uris[0].fsPath; diff --git a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts index 0016ca339bfe..7837245ec9d2 100644 --- a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts +++ b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -1052,6 +1052,7 @@ suite('Set Interpreter Command', () => { openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, canSelectMany: false, title: InterpreterQuickPickList.browsePath.title, + defaultUri: undefined, }; const multiStepInput = TypeMoq.Mock.ofType>(); multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); @@ -1073,6 +1074,27 @@ suite('Set Interpreter Command', () => { openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, canSelectMany: false, title: InterpreterQuickPickList.browsePath.title, + defaultUri: undefined, + }; + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); + appShell.setup((a) => a.showOpenDialog(expectedParams)).verifiable(TypeMoq.Times.once()); + platformService.setup((p) => p.isWindows).returns(() => false); + + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state).ignoreErrors(); + + appShell.verifyAll(); + }); + + test('If `Browse...` option is selected with workspace, file browser opens at workspace root', async () => { + const workspaceUri = Uri.parse('file:///workspace/root'); + const state: InterpreterStateArgs = { path: undefined, workspace: workspaceUri }; + const multiStepInput = TypeMoq.Mock.ofType>(); + const expectedParams = { + filters: undefined, + openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, + canSelectMany: false, + title: InterpreterQuickPickList.browsePath.title, + defaultUri: workspaceUri, }; multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); appShell.setup((a) => a.showOpenDialog(expectedParams)).verifiable(TypeMoq.Times.once()); @@ -1126,6 +1148,7 @@ suite('Set Interpreter Command', () => { openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, canSelectMany: false, title: InterpreterQuickPickList.browsePath.title, + defaultUri: undefined, }; multiStepInput .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) From 8989323b1b98e2a9da26e53ad3af99914e61717a Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:46:39 -0800 Subject: [PATCH 1057/1136] Add line number support for class nodes in pytest and unittest (#25593) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Plan: Add Location to TestClass Items for Run Functionality This PR implements the fix for issue #25592 - TestClass items need a location (lineno) to be runnable and show the green arrow in VS Code's Test Explorer. ### Changes Completed: #### Pytest: - [x] Update Python pytest adapter to add `lineno` field to class nodes - [x] Modify `create_class_node()` in `python_files/vscode_pytest/__init__.py` to extract and include line number - [x] Update TypeScript types to allow `lineno` on class nodes - [x] Fix Python type checking errors - [x] Update expected test outputs - [x] Update all expected discovery outputs in `python_files/tests/pytestadapter/expected_discovery_test_output.py` to include `lineno` for class nodes - [x] Add `find_class_line_number()` helper function #### Unittest: - [x] Update Python unittest adapter to add `lineno` field to class nodes - [x] Modify `build_test_tree()` in `python_files/unittestadapter/pvsc_utils.py` to add line numbers - [x] Add `get_class_line()` function to extract class line numbers - [x] Make `lineno` optional field in `TestNode` TypedDict - [x] Return empty string instead of "*" when line cannot be determined - [x] Update expected test outputs - [x] Update expected discovery outputs in `python_files/tests/unittestadapter/expected_discovery_test_output.py` to include `lineno` for class nodes - [x] Add `find_class_line_number()` helper function #### TypeScript: - [x] Update TypeScript result resolver - [x] Modify `populateTestTree()` in `src/client/testing/testController/common/utils.ts` to handle `lineno` for class nodes - [x] Update type definitions to allow optional `lineno` on `DiscoveredTestNode` - [x] Add 'function' to `DiscoveredTestType` enum ### Test Results: **Pytest tests:** ✅ 13/15 passing (2 failures expected - pytest-describe plugin not installed) **Unittest tests:** ✅ Verified class nodes include lineno **Code quality:** - ✅ Ruff formatting and linting passed - ⚠️ Pyright has pre-existing errors (not introduced by this change) ### Technical Implementation: **Pytest:** - Extract line number from pytest.Class objects using Python's `inspect.getsourcelines()` - Add `lineno` as optional field to TestNode TypedDict (using NotRequired) - Return empty string when line number cannot be determined **Unittest:** - Extract line number from test_case.__class__ using `inspect.getsourcelines()` - Add `lineno` field to class nodes during tree building - Use same optional field pattern in TestNode TypedDict - Return empty string when line number cannot be determined (consistent with pytest) **TypeScript:** - Create VS Code ranges for class nodes when `lineno` is present This minimal change ensures TestClass items are treated similarly to test items in terms of having a location, making them runnable in the VS Code UI for both pytest and unittest.
Original prompt fantastic analysis. Please implement this and keep in mind all this context you have; both how this should be done, what should be changed, and how tests are impacted [Chronological Review: The conversation began with the user requesting a discussion about issue #25592 on GitHub, specifically regarding the need for TestClass items to have a location for proper functionality. The user then asked about the risks associated with this change and which tests would need updating. Following a positive response to the analysis, the user requested implementation of the discussed changes while considering the context of how it should be done, what should be changed, and how tests are impacted.] [Intent Mapping: 1. "lets talk about this issue: https://github.com/microsoft/vscode-python/issues/25592 what needs to be done (likely) is TestClass items need a location so it shows as the green arrow and makes it runnable." - User wants to discuss the issue and create a plan for implementation. 2. "what risk does this change have? what tests will need to be updated?" - User seeks to understand the implications of the changes and necessary test updates. 3. "fantastic analysis. Please implement this and keep in mind all this context you have; both how this should be done, what should be changed, and how tests are impacted." - User requests implementation of the analysis with a focus on context and impact.] [Technical Inventory: - Repository: vscode-python - Current Branch: supposed-spoonbill - Default Branch: main - Active Pull Request: support extra patching for doctest (https://github.com/microsoft/vscode-python/pull/25591) - Issue Discussed: #25592 regarding TestClass items needing a location.] [Code Archaeology: No specific files or code changes were discussed in detail, but the focus was on the functionality of TestClass items and their integration into the existing codebase.] [Progress Assessment: - Completed Tasks: Analysis of the issue and identification of necessary changes. - Partially Complete Work: Implementation of the discussed changes is pending. - Validated Outcomes: None yet, as implementation has not been executed.] [Context Validation: All critical information for continuation is captured, including the need for changes to TestClass items and the implications for testing.] [Recent Commands Analysis: - Last Agent Commands: User requested a plan for the issue, an analysis of risks, and implementation of the discussed changes. - Tool Results Summary: No specific tool results were returned as the conversation focused on planning and analysis rather than executing code or commands. - Pre-Summary State: The agent was actively discussing the implementation of changes related to issue #25592 when the token budget was exceeded. - Operation Context: The commands were executed to align with the user's goals of addressing the issue and ensuring proper functionality of TestClass items.] 1. Conversation Overview: - Primary Objectives: The user aimed to discuss issue #25592, focusing on making TestClass items runnable by providing them with a location. The user also sought to understand the risks and necessary test updates related to this change. - Session Context: The conversation flowed from discussing the issue, analyzing risks, and finally requesting implementation of the proposed changes. - User Intent Evolution: The user transitioned from seeking information and analysis to requesting concrete implementation of the discussed changes. 2. Technical Foundation: - Repository: vscode-python - Current Branch: supposed-spoonbill - Default Branch: main - Active Pull Request: support extra patching for doctest (https://github.com/microsoft/vscode-python/pull/25591) 3. Codebase Status: - No specific files were modified or discussed in detail, but the focus was on the functionality of TestClass items. 4. Problem Resolution: - Issues Encountered: The need for TestClass items to have a location for proper functionality. - Solutions Implemented: Analysis of the issue and identification of necessary changes were completed, but implementation is pending. - Debugging Context: No ongoing troubleshooting efforts were mentioned. - Lessons Learned: Understanding the implications of changes on existing functionality and tests is crucial. 5. Progress Tracking: - Completed Tasks: Analysis of the issue and identification of necessary changes. - Partially Complete Work: Implementation of the discussed changes is pending. - Validated Outcomes: None yet, as implementation has not been executed. 6. Active Work State: - Current Focus: The user was focused on implementing changes related to issue #25592. - Recent Context: The last few exchanges involved discussing the issue, analyzing risks, and planning for implementation. - Working Code: No specific code snippets were discussed recently. - Immediate Context: The specific problem being addressed was the need for TestClass items to have a locat...
Created from [VS Code](https://code.visualstudio.com/docs/copilot/copilot-coding-agent). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../expected_discovery_test_output.py | 46 ++++++++++++++++++- python_files/tests/pytestadapter/helpers.py | 22 +++++++++ .../expected_discovery_test_output.py | 33 +++++++++++++ python_files/unittestadapter/pvsc_utils.py | 17 +++++++ python_files/vscode_pytest/__init__.py | 26 ++++++++++- .../testing/testController/common/types.ts | 3 +- .../testing/testController/common/utils.ts | 15 ++++++ 7 files changed, 159 insertions(+), 3 deletions(-) diff --git a/python_files/tests/pytestadapter/expected_discovery_test_output.py b/python_files/tests/pytestadapter/expected_discovery_test_output.py index e00db5d660a3..b6f0779cf982 100644 --- a/python_files/tests/pytestadapter/expected_discovery_test_output.py +++ b/python_files/tests/pytestadapter/expected_discovery_test_output.py @@ -1,6 +1,11 @@ import os -from .helpers import TEST_DATA_PATH, find_test_line_number, get_absolute_test_id +from .helpers import ( + TEST_DATA_PATH, + find_class_line_number, + find_test_line_number, + get_absolute_test_id, +) # This file contains the expected output dictionaries for tests discovery and is used in test_discovery.py. @@ -95,6 +100,7 @@ "unittest_pytest_same_file.py::TestExample", unit_pytest_same_file_path, ), + "lineno": find_class_line_number("TestExample", unit_pytest_same_file_path), }, { "name": "test_true_pytest", @@ -207,6 +213,7 @@ "unittest_folder/test_add.py::TestAddFunction", test_add_path, ), + "lineno": find_class_line_number("TestAddFunction", test_add_path), }, { "name": "TestDuplicateFunction", @@ -235,6 +242,9 @@ "unittest_folder/test_add.py::TestDuplicateFunction", test_add_path, ), + "lineno": find_class_line_number( + "TestDuplicateFunction", test_add_path + ), }, ], }, @@ -288,6 +298,9 @@ "unittest_folder/test_subtract.py::TestSubtractFunction", test_subtract_path, ), + "lineno": find_class_line_number( + "TestSubtractFunction", test_subtract_path + ), }, { "name": "TestDuplicateFunction", @@ -316,6 +329,9 @@ "unittest_folder/test_subtract.py::TestDuplicateFunction", test_subtract_path, ), + "lineno": find_class_line_number( + "TestDuplicateFunction", test_subtract_path + ), }, ], }, @@ -553,6 +569,7 @@ "parametrize_tests.py::TestClass", parameterize_tests_path, ), + "lineno": find_class_line_number("TestClass", parameterize_tests_path), "children": [ { "name": "test_adding", @@ -929,6 +946,7 @@ "test_multi_class_nest.py::TestFirstClass", TEST_MULTI_CLASS_NEST_PATH, ), + "lineno": find_class_line_number("TestFirstClass", TEST_MULTI_CLASS_NEST_PATH), "children": [ { "name": "TestSecondClass", @@ -938,6 +956,9 @@ "test_multi_class_nest.py::TestFirstClass::TestSecondClass", TEST_MULTI_CLASS_NEST_PATH, ), + "lineno": find_class_line_number( + "TestSecondClass", TEST_MULTI_CLASS_NEST_PATH + ), "children": [ { "name": "test_second", @@ -982,6 +1003,9 @@ "test_multi_class_nest.py::TestFirstClass::TestSecondClass2", TEST_MULTI_CLASS_NEST_PATH, ), + "lineno": find_class_line_number( + "TestSecondClass2", TEST_MULTI_CLASS_NEST_PATH + ), "children": [ { "name": "test_second2", @@ -1227,6 +1251,9 @@ "same_function_new_class_param.py::TestNotEmpty", TEST_DATA_PATH / "same_function_new_class_param.py", ), + "lineno": find_class_line_number( + "TestNotEmpty", TEST_DATA_PATH / "same_function_new_class_param.py" + ), }, { "name": "TestEmpty", @@ -1298,6 +1325,9 @@ "same_function_new_class_param.py::TestEmpty", TEST_DATA_PATH / "same_function_new_class_param.py", ), + "lineno": find_class_line_number( + "TestEmpty", TEST_DATA_PATH / "same_function_new_class_param.py" + ), }, ], } @@ -1371,6 +1401,9 @@ "test_param_span_class.py::TestClass1", TEST_DATA_PATH / "test_param_span_class.py", ), + "lineno": find_class_line_number( + "TestClass1", TEST_DATA_PATH / "test_param_span_class.py" + ), }, { "name": "TestClass2", @@ -1427,6 +1460,9 @@ "test_param_span_class.py::TestClass2", TEST_DATA_PATH / "test_param_span_class.py", ), + "lineno": find_class_line_number( + "TestClass2", TEST_DATA_PATH / "test_param_span_class.py" + ), }, ], } @@ -1503,6 +1539,7 @@ "pytest_describe_plugin/describe_only.py::describe_A", describe_only_path, ), + "lineno": find_class_line_number("describe_A", describe_only_path), } ], } @@ -1586,6 +1623,9 @@ "pytest_describe_plugin/nested_describe.py::describe_list::describe_append", nested_describe_path, ), + "lineno": find_class_line_number( + "describe_append", nested_describe_path + ), }, { "name": "describe_remove", @@ -1614,12 +1654,16 @@ "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove", nested_describe_path, ), + "lineno": find_class_line_number( + "describe_remove", nested_describe_path + ), }, ], "id_": get_absolute_test_id( "pytest_describe_plugin/nested_describe.py::describe_list", nested_describe_path, ), + "lineno": find_class_line_number("describe_list", nested_describe_path), } ], } diff --git a/python_files/tests/pytestadapter/helpers.py b/python_files/tests/pytestadapter/helpers.py index 4c337585bece..25e6187e2efa 100644 --- a/python_files/tests/pytestadapter/helpers.py +++ b/python_files/tests/pytestadapter/helpers.py @@ -370,6 +370,28 @@ def find_test_line_number(test_name: str, test_file_path) -> str: raise ValueError(error_str) +def find_class_line_number(class_name: str, test_file_path) -> str: + """Function which finds the correct line number for a class definition. + + Args: + class_name: The name of the class to find the line number for. + test_file_path: The path to the test file where the class is located. + """ + # Look for the class definition line (or function for pytest-describe) + with open(test_file_path) as f: # noqa: PTH123 + for i, line in enumerate(f): + # Match "class ClassName" or "class ClassName(" or "class ClassName:" + # Also match "def ClassName(" for pytest-describe blocks + if ( + line.strip().startswith(f"class {class_name}") + or line.strip().startswith(f"class {class_name}(") + or line.strip().startswith(f"def {class_name}(") + ): + return str(i + 1) + error_str: str = f"Class {class_name!r} not found on any line in {test_file_path}" + raise ValueError(error_str) + + def get_absolute_test_id(test_id: str, test_path: pathlib.Path) -> str: """Get the absolute test id by joining the testPath with the test_id.""" split_id = test_id.split("::")[1:] diff --git a/python_files/tests/unittestadapter/expected_discovery_test_output.py b/python_files/tests/unittestadapter/expected_discovery_test_output.py index 9de0eff8238c..0901f21bfbc2 100644 --- a/python_files/tests/unittestadapter/expected_discovery_test_output.py +++ b/python_files/tests/unittestadapter/expected_discovery_test_output.py @@ -9,6 +9,25 @@ TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" +def find_class_line_number(class_name: str, test_file_path) -> str: + """Function which finds the correct line number for a class definition. + + Args: + class_name: The name of the class to find the line number for. + test_file_path: The path to the test file where the class is located. + """ + # Look for the class definition line + with pathlib.Path(test_file_path).open() as f: + for i, line in enumerate(f): + # Match "class ClassName" or "class ClassName(" or "class ClassName:" + if line.strip().startswith(f"class {class_name}") or line.strip().startswith( + f"class {class_name}(" + ): + return str(i + 1) + error_str: str = f"Class {class_name!r} not found on any line in {test_file_path}" + raise ValueError(error_str) + + skip_unittest_folder_discovery_output = { "path": os.fspath(TEST_DATA_PATH / "unittest_skip"), "name": "unittest_skip", @@ -49,6 +68,10 @@ ], "id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py") + "\\SimpleTest", + "lineno": find_class_line_number( + "SimpleTest", + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py", + ), } ], "id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py"), @@ -114,6 +137,16 @@ }, ], "id_": complex_tree_file_path + "\\" + "TreeOne", + "lineno": find_class_line_number( + "TreeOne", + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + "test_utils_complex_tree.py", + ), + ), } ], "id_": complex_tree_file_path, diff --git a/python_files/unittestadapter/pvsc_utils.py b/python_files/unittestadapter/pvsc_utils.py index e9d7bc092992..d6920592a4d4 100644 --- a/python_files/unittestadapter/pvsc_utils.py +++ b/python_files/unittestadapter/pvsc_utils.py @@ -44,6 +44,7 @@ class TestItem(TestData): class TestNode(TestData): children: "List[TestNode | TestItem]" + lineno: NotRequired[str] # Optional field for class nodes class TestExecutionStatus(str, enum.Enum): @@ -101,6 +102,16 @@ def get_test_case(suite): yield from get_test_case(test) +def get_class_line(test_case: unittest.TestCase) -> Optional[str]: + """Get the line number where a test class is defined.""" + try: + test_class = test_case.__class__ + _sourcelines, lineno = inspect.getsourcelines(test_class) + return str(lineno) + except Exception: + return None + + def get_source_line(obj) -> str: """Get the line number of a test case start line.""" try: @@ -249,6 +260,12 @@ def build_test_tree( class_name, file_path, TestNodeTypeEnum.class_, current_node ) + # Add line number to class node if not already present. + if "lineno" not in current_node: + class_lineno = get_class_line(test_case) + if class_lineno is not None: + current_node["lineno"] = class_lineno + # Get test line number. test_method = getattr(test_case, test_case._testMethodName) # noqa: SLF001 lineno = get_source_line(test_method) diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 0eac4a74f4c3..91a81bff9a36 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -10,9 +10,19 @@ import pathlib import sys import traceback -from typing import TYPE_CHECKING, Any, Dict, Generator, Literal, Protocol, TypedDict, cast +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generator, + Literal, + Protocol, + TypedDict, + cast, +) import pytest +from typing_extensions import NotRequired if TYPE_CHECKING: from pluggy import Result @@ -52,6 +62,7 @@ class TestNode(TestData): """A general class that handles all test data which contains children.""" children: list[TestNode | TestItem | None] + lineno: NotRequired[str] # Optional field for class/function nodes class VSCodePytestError(Exception): @@ -830,12 +841,25 @@ def create_class_node(class_module: pytest.Class | DescribeBlock) -> TestNode: Keyword arguments: class_module -- the pytest object representing a class module. """ + # Get line number for the class definition + class_line = "" + try: + if hasattr(class_module, "obj"): + import inspect + + _, lineno = inspect.getsourcelines(class_module.obj) + class_line = str(lineno) + except (OSError, TypeError): + # If we can't get the source lines, leave lineno empty + pass + return { "name": class_module.name, "path": get_node_path(class_module), "type_": "class", "children": [], "id_": get_absolute_test_id(class_module.nodeid, get_node_path(class_module)), + "lineno": class_line, } diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 5c6796905024..6121b3e24442 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -177,7 +177,7 @@ export interface ITestExecutionAdapter { } // Same types as in python_files/unittestadapter/utils.py -export type DiscoveredTestType = 'folder' | 'file' | 'class' | 'test'; +export type DiscoveredTestType = 'folder' | 'file' | 'class' | 'function' | 'test'; export type DiscoveredTestCommon = { path: string; @@ -194,6 +194,7 @@ export type DiscoveredTestItem = DiscoveredTestCommon & { export type DiscoveredTestNode = DiscoveredTestCommon & { children: (DiscoveredTestNode | DiscoveredTestItem)[]; + lineno?: number | string; }; export type DiscoveredTestPayload = { diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 0bbf0e449dcd..606865e5ad7e 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -257,6 +257,21 @@ export function populateTestTree( node.canResolveChildren = true; node.tags = [RunTestTag, DebugTestTag]; + + // Set range for class nodes (and other nodes) if lineno is available + let range: Range | undefined; + if ('lineno' in child && child.lineno) { + if (Number(child.lineno) === 0) { + range = new Range(new Position(0, 0), new Position(0, 0)); + } else { + range = new Range( + new Position(Number(child.lineno) - 1, 0), + new Position(Number(child.lineno), 0), + ); + } + node.range = range; + } + testRoot!.children.add(node); } populateTestTree(testController, child, node, resultResolver, token); From 782a2f15faf2dd192924d7dde39c68e655e2c739 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 09:05:12 -0800 Subject: [PATCH 1058/1136] Bump js-yaml (#25596) Bumps and [js-yaml](https://github.com/nodeca/js-yaml). These dependencies needed to be updated together. Updates `js-yaml` from 3.13.1 to 3.14.2
Changelog

Sourced from js-yaml's changelog.

[3.14.2] - 2025-11-15

Security

  • Backported v4.1.1 fix to v3

[4.1.1] - 2025-11-12

Security

  • Fix prototype pollution issue in yaml merge (<<) operator.

[4.1.0] - 2021-04-15

Added

  • Types are now exported as yaml.types.XXX.
  • Every type now has options property with original arguments kept as they were (see yaml.types.int.options as an example).

Changed

  • Schema.extend() now keeps old type order in case of conflicts (e.g. Schema.extend([ a, b, c ]).extend([ b, a, d ]) is now ordered as abcd instead of cbad).

[4.0.0] - 2021-01-03

Changed

  • Check migration guide to see details for all breaking changes.
  • Breaking: "unsafe" tags !!js/function, !!js/regexp, !!js/undefined are moved to js-yaml-js-types package.
  • Breaking: removed safe* functions. Use load, loadAll, dump instead which are all now safe by default.
  • yaml.DEFAULT_SAFE_SCHEMA and yaml.DEFAULT_FULL_SCHEMA are removed, use yaml.DEFAULT_SCHEMA instead.
  • yaml.Schema.create(schema, tags) is removed, use schema.extend(tags) instead.
  • !!binary now always mapped to Uint8Array on load.
  • Reduced nesting of /lib folder.
  • Parse numbers according to YAML 1.2 instead of YAML 1.1 (01234 is now decimal, 0o1234 is octal, 1:23 is parsed as string instead of base60).
  • dump() no longer quotes :, [, ], (, ) except when necessary, #470, #557.
  • Line and column in exceptions are now formatted as (X:Y) instead of at line X, column Y (also present in compact format), #332.
  • Code snippet created in exceptions now contains multiple lines with line numbers.
  • dump() now serializes undefined as null in collections and removes keys with undefined in mappings, #571.
  • dump() with skipInvalid=true now serializes invalid items in collections as null.
  • Custom tags starting with ! are now dumped as !tag instead of !<!tag>, #576.
  • Custom tags starting with tag:yaml.org,2002: are now shorthanded using !!, #258.

Added

  • Added .mjs (es modules) support.
  • Added quotingType and forceQuotes options for dumper to configure string literal style, #290, #529.
  • Added styles: { '!!null': 'empty' } option for dumper (serializes { foo: null } as "foo: "), #570.

... (truncated)

Commits

Updates `js-yaml` from 4.1.0 to 4.1.1
Changelog

Sourced from js-yaml's changelog.

[3.14.2] - 2025-11-15

Security

  • Backported v4.1.1 fix to v3

[4.1.1] - 2025-11-12

Security

  • Fix prototype pollution issue in yaml merge (<<) operator.

[4.1.0] - 2021-04-15

Added

  • Types are now exported as yaml.types.XXX.
  • Every type now has options property with original arguments kept as they were (see yaml.types.int.options as an example).

Changed

  • Schema.extend() now keeps old type order in case of conflicts (e.g. Schema.extend([ a, b, c ]).extend([ b, a, d ]) is now ordered as abcd instead of cbad).

[4.0.0] - 2021-01-03

Changed

  • Check migration guide to see details for all breaking changes.
  • Breaking: "unsafe" tags !!js/function, !!js/regexp, !!js/undefined are moved to js-yaml-js-types package.
  • Breaking: removed safe* functions. Use load, loadAll, dump instead which are all now safe by default.
  • yaml.DEFAULT_SAFE_SCHEMA and yaml.DEFAULT_FULL_SCHEMA are removed, use yaml.DEFAULT_SCHEMA instead.
  • yaml.Schema.create(schema, tags) is removed, use schema.extend(tags) instead.
  • !!binary now always mapped to Uint8Array on load.
  • Reduced nesting of /lib folder.
  • Parse numbers according to YAML 1.2 instead of YAML 1.1 (01234 is now decimal, 0o1234 is octal, 1:23 is parsed as string instead of base60).
  • dump() no longer quotes :, [, ], (, ) except when necessary, #470, #557.
  • Line and column in exceptions are now formatted as (X:Y) instead of at line X, column Y (also present in compact format), #332.
  • Code snippet created in exceptions now contains multiple lines with line numbers.
  • dump() now serializes undefined as null in collections and removes keys with undefined in mappings, #571.
  • dump() with skipInvalid=true now serializes invalid items in collections as null.
  • Custom tags starting with ! are now dumped as !tag instead of !<!tag>, #576.
  • Custom tags starting with tag:yaml.org,2002: are now shorthanded using !!, #258.

Added

  • Added .mjs (es modules) support.
  • Added quotingType and forceQuotes options for dumper to configure string literal style, #290, #529.
  • Added styles: { '!!null': 'empty' } option for dumper (serializes { foo: null } as "foo: "), #570.

... (truncated)

Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 50 +++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9a85807e6068..b3dd75602dc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -933,11 +933,10 @@ } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -6197,11 +6196,10 @@ } }, "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -9163,9 +9161,9 @@ "dev": true }, "node_modules/js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "dependencies": { "argparse": "^1.0.7", @@ -10227,9 +10225,9 @@ } }, "node_modules/mocha/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "dependencies": { "argparse": "^2.0.1" @@ -15846,9 +15844,9 @@ } }, "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -19641,9 +19639,9 @@ "dev": true }, "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -22039,9 +22037,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -22840,9 +22838,9 @@ "dev": true }, "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { "argparse": "^2.0.1" From 4b5dc0d44d963fdbed9669131a119dd5b4e058cd Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 21 Nov 2025 15:26:14 -0800 Subject: [PATCH 1059/1136] Skip flaky tests in smoke suite for terminal execution and smart send (#25604) --- src/test/smoke/runInTerminal.smoke.test.ts | 6 +++++- src/test/smoke/smartSend.smoke.test.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/test/smoke/runInTerminal.smoke.test.ts b/src/test/smoke/runInTerminal.smoke.test.ts index d5ce409f3ab1..4bdec0843862 100644 --- a/src/test/smoke/runInTerminal.smoke.test.ts +++ b/src/test/smoke/runInTerminal.smoke.test.ts @@ -28,7 +28,11 @@ suite('Smoke Test: Run Python File In Terminal', () => { suiteTeardown(closeActiveWindows); teardown(closeActiveWindows); - test('Exec', async () => { + // TODO: Re-enable this test once the flakiness on Windows is resolved + test('Exec', async function () { + if (process.platform === 'win32') { + return this.skip(); + } const file = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', diff --git a/src/test/smoke/smartSend.smoke.test.ts b/src/test/smoke/smartSend.smoke.test.ts index dc1f07f047e7..80eabf356330 100644 --- a/src/test/smoke/smartSend.smoke.test.ts +++ b/src/test/smoke/smartSend.smoke.test.ts @@ -19,7 +19,11 @@ suite('Smoke Test: Run Smart Selection and Advance Cursor', async () => { suiteTeardown(closeActiveWindows); teardown(closeActiveWindows); - test('Smart Send', async () => { + // TODO: Re-enable this test once the flakiness on Windows is resolved + test('Smart Send', async function () { + if (process.platform === 'win32') { + return this.skip(); + } const file = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', From f96d91041e1dc5e028a166d59d451d32037ce7f6 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 28 Nov 2025 19:13:55 -0800 Subject: [PATCH 1060/1136] Refactor pytest and unittest test discovery (#25599) cleanup to assist with future changes --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../testController/common/discoveryHelpers.ts | 137 ++++++++ .../pytest/pytestDiscoveryAdapter.ts | 328 ++++++++---------- .../testController/pytest/pytestHelpers.ts | 58 ++++ .../unittest/testDiscoveryAdapter.ts | 309 ++++++++--------- .../unittest/unittestHelpers.ts | 28 ++ 5 files changed, 524 insertions(+), 336 deletions(-) create mode 100644 src/client/testing/testController/common/discoveryHelpers.ts create mode 100644 src/client/testing/testController/pytest/pytestHelpers.ts create mode 100644 src/client/testing/testController/unittest/unittestHelpers.ts diff --git a/src/client/testing/testController/common/discoveryHelpers.ts b/src/client/testing/testController/common/discoveryHelpers.ts new file mode 100644 index 000000000000..e170ad576ae8 --- /dev/null +++ b/src/client/testing/testController/common/discoveryHelpers.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { CancellationToken, CancellationTokenSource, Disposable, Uri } from 'vscode'; +import { Deferred } from '../../../common/utils/async'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { createDiscoveryErrorPayload, fixLogLinesNoTrailing, startDiscoveryNamedPipe } from './utils'; +import { DiscoveredTestPayload, ITestResultResolver } from './types'; + +/** + * Test provider type for logging purposes. + */ +export type TestProvider = 'pytest' | 'unittest'; + +/** + * Sets up the discovery named pipe and wires up cancellation. + * @param resultResolver The resolver to handle discovered test data + * @param token Optional cancellation token from the caller + * @param uri Workspace URI for logging + * @returns Object containing the pipe name, cancellation source, and disposable for the external token handler + */ +export async function setupDiscoveryPipe( + resultResolver: ITestResultResolver | undefined, + token: CancellationToken | undefined, + uri: Uri, +): Promise<{ pipeName: string; cancellation: CancellationTokenSource; tokenDisposable: Disposable | undefined }> { + const discoveryPipeCancellation = new CancellationTokenSource(); + + // Wire up cancellation from external token and store the disposable + const tokenDisposable = token?.onCancellationRequested(() => { + traceInfo(`Test discovery cancelled.`); + discoveryPipeCancellation.cancel(); + }); + + // Start the named pipe with the discovery listener + const discoveryPipeName = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { + if (!token?.isCancellationRequested) { + resultResolver?.resolveDiscovery(data); + } + }, discoveryPipeCancellation.token); + + traceVerbose(`Created discovery pipe: ${discoveryPipeName} for workspace ${uri.fsPath}`); + + return { + pipeName: discoveryPipeName, + cancellation: discoveryPipeCancellation, + tokenDisposable, + }; +} + +/** + * Creates standard process event handlers for test discovery subprocess. + * Handles stdout/stderr logging and error reporting on process exit. + * + * @param testProvider - The test framework being used ('pytest' or 'unittest') + * @param uri - The workspace URI + * @param cwd - The current working directory + * @param resultResolver - Resolver for test discovery results + * @param deferredTillExecClose - Deferred to resolve when process closes + * @param allowedSuccessCodes - Additional exit codes to treat as success (e.g., pytest exit code 5 for no tests found) + */ +export function createProcessHandlers( + testProvider: TestProvider, + uri: Uri, + cwd: string, + resultResolver: ITestResultResolver | undefined, + deferredTillExecClose: Deferred, + allowedSuccessCodes: number[] = [], +): { + onStdout: (data: any) => void; + onStderr: (data: any) => void; + onExit: (code: number | null, signal: NodeJS.Signals | null) => void; + onClose: (code: number | null, signal: NodeJS.Signals | null) => void; +} { + const isSuccessCode = (code: number | null): boolean => { + return code === 0 || (code !== null && allowedSuccessCodes.includes(code)); + }; + + return { + onStdout: (data: any) => { + const out = fixLogLinesNoTrailing(data.toString()); + traceInfo(out); + }, + onStderr: (data: any) => { + const out = fixLogLinesNoTrailing(data.toString()); + traceError(out); + }, + onExit: (code: number | null, _signal: NodeJS.Signals | null) => { + // The 'exit' event fires when the process terminates, but streams may still be open. + // Only log verbose success message here; error handling happens in onClose. + if (isSuccessCode(code)) { + traceVerbose(`${testProvider} discovery subprocess exited successfully for workspace ${uri.fsPath}`); + } + }, + onClose: (code: number | null, signal: NodeJS.Signals | null) => { + // We resolve the deferred here to ensure all output has been captured. + if (!isSuccessCode(code)) { + traceError( + `${testProvider} discovery failed with exit code ${code} and signal ${signal} for workspace ${uri.fsPath}. Creating error payload.`, + ); + resultResolver?.resolveDiscovery(createDiscoveryErrorPayload(code, signal, cwd)); + } else { + traceVerbose(`${testProvider} discovery subprocess streams closed for workspace ${uri.fsPath}`); + } + deferredTillExecClose?.resolve(); + }, + }; +} + +/** + * Handles cleanup when test discovery is cancelled. + * Kills the subprocess (if running), resolves the completion deferred, and cancels the discovery pipe. + * + * @param testProvider - The test framework being used ('pytest' or 'unittest') + * @param proc - The process to kill + * @param processCompletion - Deferred to resolve + * @param pipeCancellation - Cancellation token source to cancel + * @param uri - The workspace URI + */ +export function cleanupOnCancellation( + testProvider: TestProvider, + proc: { kill: () => void } | undefined, + processCompletion: Deferred, + pipeCancellation: CancellationTokenSource, + uri: Uri, +): void { + traceInfo(`Test discovery cancelled, killing ${testProvider} subprocess for workspace ${uri.fsPath}`); + if (proc) { + traceVerbose(`Killing ${testProvider} subprocess for workspace ${uri.fsPath}`); + proc.kill(); + } else { + traceVerbose(`No ${testProvider} subprocess to kill for workspace ${uri.fsPath} (proc is undefined)`); + } + traceVerbose(`Resolving process completion deferred for ${testProvider} discovery in workspace ${uri.fsPath}`); + processCompletion.resolve(); + traceVerbose(`Cancelling discovery pipe for ${testProvider} discovery in workspace ${uri.fsPath}`); + pipeCancellation.cancel(); +} diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 308c9ba1f9bc..7ad69c71fa0e 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as path from 'path'; -import { CancellationToken, CancellationTokenSource, Uri } from 'vscode'; -import * as fs from 'fs'; +import { CancellationToken, Disposable, Uri } from 'vscode'; import { ChildProcess } from 'child_process'; import { ExecutionFactoryCreateWithEnvironmentOptions, @@ -10,24 +9,37 @@ import { SpawnOptions, } from '../../../common/process/types'; import { IConfigurationService } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; +import { Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { traceError, traceInfo, traceVerbose, traceWarn } from '../../../logging'; -import { DiscoveredTestPayload, ITestDiscoveryAdapter, ITestResultResolver } from '../common/types'; -import { - createDiscoveryErrorPayload, - createTestingDeferred, - fixLogLinesNoTrailing, - startDiscoveryNamedPipe, - addValueIfKeyNotExist, - hasSymlinkParent, -} from '../common/utils'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { ITestDiscoveryAdapter, ITestResultResolver } from '../common/types'; +import { createTestingDeferred } from '../common/utils'; import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; import { PythonEnvironment } from '../../../pythonEnvironments/info'; import { useEnvExtension, getEnvironment, runInBackground } from '../../../envExt/api.internal'; +import { buildPytestEnv as configureSubprocessEnv, handleSymlinkAndRootDir } from './pytestHelpers'; +import { cleanupOnCancellation, createProcessHandlers, setupDiscoveryPipe } from '../common/discoveryHelpers'; + +/** + * Configures the subprocess environment for pytest discovery. + * @param envVarsService Service to retrieve environment variables + * @param uri Workspace URI + * @param discoveryPipeName Name of the discovery pipe to pass to the subprocess + * @returns Configured environment variables for the subprocess + */ +async function configureDiscoveryEnv( + envVarsService: IEnvironmentVariablesProvider | undefined, + uri: Uri, + discoveryPipeName: string, +): Promise { + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const envVars = await envVarsService?.getEnvironmentVariables(uri); + const mutableEnv = configureSubprocessEnv(envVars, fullPluginPath, discoveryPipeName); + return mutableEnv; +} /** - * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied + * Wrapper class for pytest test discovery. This is where we call the pytest subprocess. */ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { constructor( @@ -42,185 +54,153 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { token?: CancellationToken, interpreter?: PythonEnvironment, ): Promise { - const cSource = new CancellationTokenSource(); - const deferredReturn = createDeferred(); - - token?.onCancellationRequested(() => { - traceInfo(`Test discovery cancelled.`); - cSource.cancel(); - deferredReturn.resolve(); - }); - - const name = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { - // if the token is cancelled, we don't want process the data - if (!token?.isCancellationRequested) { - this.resultResolver?.resolveDiscovery(data); - } - }, cSource.token); - - this.runPytestDiscovery(uri, name, cSource, executionFactory, interpreter, token).then(() => { - deferredReturn.resolve(); - }); + // Setup discovery pipe and cancellation + const { + pipeName: discoveryPipeName, + cancellation: discoveryPipeCancellation, + tokenDisposable, + } = await setupDiscoveryPipe(this.resultResolver, token, uri); + + // Setup process handlers deferred (used by both execution paths) + const deferredTillExecClose: Deferred = createTestingDeferred(); - return deferredReturn.promise; - } + // Collect all disposables related to discovery to handle cleanup in finally block + const disposables: Disposable[] = []; + if (tokenDisposable) { + disposables.push(tokenDisposable); + } - async runPytestDiscovery( - uri: Uri, - discoveryPipeName: string, - cSource: CancellationTokenSource, - executionFactory: IPythonExecutionFactory, - interpreter?: PythonEnvironment, - token?: CancellationToken, - ): Promise { - const relativePathToPytest = 'python_files'; - const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); - const settings = this.configSettings.getSettings(uri); - let { pytestArgs } = settings.testing; - const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; - - // check for symbolic path - const stats = await fs.promises.lstat(cwd); - const resolvedPath = await fs.promises.realpath(cwd); - let isSymbolicLink = false; - if (stats.isSymbolicLink()) { - isSymbolicLink = true; - traceWarn('The cwd is a symbolic link.'); - } else if (resolvedPath !== cwd) { - traceWarn( - 'The cwd resolves to a different path, checking if it has a symbolic link somewhere in its path.', + try { + // Build pytest command and arguments + const settings = this.configSettings.getSettings(uri); + let { pytestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + pytestArgs = await handleSymlinkAndRootDir(cwd, pytestArgs); + const commandArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + traceVerbose( + `Running pytest discovery with command: ${commandArgs.join(' ')} for workspace ${uri.fsPath}.`, ); - isSymbolicLink = await hasSymlinkParent(cwd); - } - if (isSymbolicLink) { - traceWarn("Symlink found, adding '--rootdir' to pytestArgs only if it doesn't already exist. cwd: ", cwd); - pytestArgs = addValueIfKeyNotExist(pytestArgs, '--rootdir', cwd); - } - // if user has provided `--rootdir` then use that, otherwise add `cwd` - // root dir is required so pytest can find the relative paths and for symlinks - addValueIfKeyNotExist(pytestArgs, '--rootdir', cwd); - - // get and edit env vars - const mutableEnv = { - ...(await this.envVarsService?.getEnvironmentVariables(uri)), - }; - // get python path from mutable env, it contains process.env as well - const pythonPathParts: string[] = mutableEnv.PYTHONPATH?.split(path.delimiter) ?? []; - const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - mutableEnv.PYTHONPATH = pythonPathCommand; - mutableEnv.TEST_RUN_PIPE = discoveryPipeName; - traceInfo( - `Environment variables set for pytest discovery: PYTHONPATH=${mutableEnv.PYTHONPATH}, TEST_RUN_PIPE=${mutableEnv.TEST_RUN_PIPE}`, - ); - - // delete UUID following entire discovery finishing. - const execArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); - traceVerbose(`Running pytest discovery with command: ${execArgs.join(' ')} for workspace ${uri.fsPath}.`); - - if (useEnvExtension()) { - const pythonEnv = await getEnvironment(uri); - if (pythonEnv) { - const deferredTillExecClose: Deferred = createTestingDeferred(); + + // Configure subprocess environment + const mutableEnv = await configureDiscoveryEnv(this.envVarsService, uri, discoveryPipeName); + + // Setup process handlers (shared by both execution paths) + const handlers = createProcessHandlers('pytest', uri, cwd, this.resultResolver, deferredTillExecClose, [5]); + + // Execute using environment extension if available + if (useEnvExtension()) { + traceInfo(`Using environment extension for pytest discovery in workspace ${uri.fsPath}`); + const pythonEnv = await getEnvironment(uri); + if (!pythonEnv) { + traceError( + `Python environment not found for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + traceVerbose(`Using Python environment: ${JSON.stringify(pythonEnv)}`); const proc = await runInBackground(pythonEnv, { cwd, - args: execArgs, + args: commandArgs, env: (mutableEnv as unknown) as { [key: string]: string }, }); - token?.onCancellationRequested(() => { - traceInfo(`Test discovery cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); - proc.kill(); - deferredTillExecClose.resolve(); - cSource.cancel(); - }); - proc.stdout.on('data', (data) => { - const out = fixLogLinesNoTrailing(data.toString()); - traceInfo(out); - }); - proc.stderr.on('data', (data) => { - const out = fixLogLinesNoTrailing(data.toString()); - traceError(out); + traceInfo(`Started pytest discovery subprocess (environment extension) for workspace ${uri.fsPath}`); + + // Wire up cancellation and process events + const envExtCancellationHandler = token?.onCancellationRequested(() => { + cleanupOnCancellation('pytest', proc, deferredTillExecClose, discoveryPipeCancellation, uri); }); + if (envExtCancellationHandler) { + disposables.push(envExtCancellationHandler); + } + proc.stdout.on('data', handlers.onStdout); + proc.stderr.on('data', handlers.onStderr); proc.onExit((code, signal) => { - if (code !== 0) { - traceError( - `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, - ); - this.resultResolver?.resolveDiscovery(createDiscoveryErrorPayload(code, signal, cwd)); - } - deferredTillExecClose.resolve(); + handlers.onExit(code, signal); + handlers.onClose(code, signal); }); + await deferredTillExecClose.promise; - } else { - traceError(`Python Environment not found for: ${uri.fsPath}`); + traceInfo(`Pytest discovery completed for workspace ${uri.fsPath}`); + return; } - return; - } - - const spawnOptions: SpawnOptions = { - cwd, - throwOnStdErr: true, - env: mutableEnv, - token, - }; - // Create the Python environment in which to execute the command. - const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { - allowEnvironmentFetchExceptions: false, - resource: uri, - interpreter, - }; - const execService = await executionFactory.createActivatedEnvironment(creationOptions); + // Execute using execution factory (fallback path) + traceInfo(`Using execution factory for pytest discovery in workspace ${uri.fsPath}`); + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + interpreter, + }; + const execService = await executionFactory.createActivatedEnvironment(creationOptions); + if (!execService) { + traceError( + `Failed to create execution service for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + const execInfo = await execService.getExecutablePath(); + traceVerbose(`Using Python executable: ${execInfo} for workspace ${uri.fsPath}`); - const execInfo = await execService?.getExecutablePath(); - traceVerbose(`Executable path for pytest discovery: ${execInfo}.`); + // Check for cancellation before spawning process + if (token?.isCancellationRequested) { + traceInfo(`Pytest discovery cancelled before spawning process for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } - const deferredTillExecClose: Deferred = createTestingDeferred(); + const spawnOptions: SpawnOptions = { + cwd, + throwOnStdErr: true, + env: mutableEnv, + token, + }; + + let resultProc: ChildProcess | undefined; + + // Set up cancellation handler after all early return checks + const cancellationHandler = token?.onCancellationRequested(() => { + traceInfo(`Cancellation requested during pytest discovery for workspace ${uri.fsPath}`); + cleanupOnCancellation('pytest', resultProc, deferredTillExecClose, discoveryPipeCancellation, uri); + }); + if (cancellationHandler) { + disposables.push(cancellationHandler); + } - let resultProc: ChildProcess | undefined; + try { + const result = execService.execObservable(commandArgs, spawnOptions); + resultProc = result?.proc; - token?.onCancellationRequested(() => { - traceInfo(`Test discovery cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); - // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. - if (resultProc) { - resultProc?.kill(); - } else { + if (!resultProc) { + traceError(`Failed to spawn pytest discovery subprocess for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } + traceInfo(`Started pytest discovery subprocess (execution factory) for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error spawning pytest discovery subprocess for workspace ${uri.fsPath}: ${error}`); deferredTillExecClose.resolve(); - cSource.cancel(); + throw error; } - }); - const result = execService?.execObservable(execArgs, spawnOptions); - resultProc = result?.proc; - - // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. - // Displays output to user and ensure the subprocess doesn't run into buffer overflow. - - result?.proc?.stdout?.on('data', (data) => { - const out = fixLogLinesNoTrailing(data.toString()); - traceInfo(out); - }); - result?.proc?.stderr?.on('data', (data) => { - const out = fixLogLinesNoTrailing(data.toString()); - traceError(out); - }); - result?.proc?.on('exit', (code, signal) => { - if (code !== 0) { - traceError( - `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}.`, - ); - } - }); - result?.proc?.on('close', (code, signal) => { - // pytest exits with code of 5 when 0 tests are found- this is not a failure for discovery. - if (code !== 0 && code !== 5) { - traceError( - `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}. Creating and sending error discovery payload`, - ); - this.resultResolver?.resolveDiscovery(createDiscoveryErrorPayload(code, signal, cwd)); - } - // due to the sync reading of the output. - deferredTillExecClose?.resolve(); - }); - await deferredTillExecClose.promise; + resultProc.stdout?.on('data', handlers.onStdout); + resultProc.stderr?.on('data', handlers.onStderr); + resultProc.on('exit', handlers.onExit); + resultProc.on('close', handlers.onClose); + + traceVerbose(`Waiting for pytest discovery subprocess to complete for workspace ${uri.fsPath}`); + await deferredTillExecClose.promise; + traceInfo(`Pytest discovery completed for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error during pytest discovery for workspace ${uri.fsPath}: ${error}`); + deferredTillExecClose.resolve(); + throw error; + } finally { + // Dispose all cancellation handlers and event subscriptions + disposables.forEach((d) => d.dispose()); + // Dispose the discovery pipe cancellation token + discoveryPipeCancellation.dispose(); + } } } diff --git a/src/client/testing/testController/pytest/pytestHelpers.ts b/src/client/testing/testController/pytest/pytestHelpers.ts new file mode 100644 index 000000000000..c6e748fb85a7 --- /dev/null +++ b/src/client/testing/testController/pytest/pytestHelpers.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import * as fs from 'fs'; +import { traceInfo, traceWarn } from '../../../logging'; +import { addValueIfKeyNotExist, hasSymlinkParent } from '../common/utils'; + +/** + * Checks if the current working directory contains a symlink and ensures --rootdir is set in pytest args. + * This is required for pytest to correctly resolve relative paths in symlinked directories. + */ +export async function handleSymlinkAndRootDir(cwd: string, pytestArgs: string[]): Promise { + const stats = await fs.promises.lstat(cwd); + const resolvedPath = await fs.promises.realpath(cwd); + let isSymbolicLink = false; + if (stats.isSymbolicLink()) { + isSymbolicLink = true; + traceWarn(`Working directory is a symbolic link: ${cwd} -> ${resolvedPath}`); + } else if (resolvedPath !== cwd) { + traceWarn( + `Working directory resolves to different path: ${cwd} -> ${resolvedPath}. Checking for symlinks in parent directories.`, + ); + isSymbolicLink = await hasSymlinkParent(cwd); + } + if (isSymbolicLink) { + traceWarn( + `Symlink detected in path. Adding '--rootdir=${cwd}' to pytest args to ensure correct path resolution.`, + ); + pytestArgs = addValueIfKeyNotExist(pytestArgs, '--rootdir', cwd); + } + // if user has provided `--rootdir` then use that, otherwise add `cwd` + // root dir is required so pytest can find the relative paths and for symlinks + pytestArgs = addValueIfKeyNotExist(pytestArgs, '--rootdir', cwd); + return pytestArgs; +} + +/** + * Builds the environment variables required for pytest discovery. + * Sets PYTHONPATH to include the plugin path and TEST_RUN_PIPE for communication. + */ +export function buildPytestEnv( + envVars: { [key: string]: string | undefined } | undefined, + fullPluginPath: string, + discoveryPipeName: string, +): { [key: string]: string | undefined } { + const mutableEnv = { + ...envVars, + }; + // get python path from mutable env, it contains process.env as well + const pythonPathParts: string[] = mutableEnv.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_RUN_PIPE = discoveryPipeName; + traceInfo( + `Environment variables set for pytest discovery: PYTHONPATH=${mutableEnv.PYTHONPATH}, TEST_RUN_PIPE=${mutableEnv.TEST_RUN_PIPE}`, + ); + return mutableEnv; +} diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index a40e25153fbc..7c986e95a449 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -1,33 +1,43 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as path from 'path'; -import { CancellationTokenSource, Uri } from 'vscode'; -import { CancellationToken } from 'vscode-jsonrpc'; +import { CancellationToken, Disposable, Uri } from 'vscode'; import { ChildProcess } from 'child_process'; import { IConfigurationService } from '../../../common/types'; import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { - DiscoveredTestPayload, - ITestDiscoveryAdapter, - ITestResultResolver, - TestCommandOptions, - TestDiscoveryCommand, -} from '../common/types'; -import { createDeferred } from '../../../common/utils/async'; -import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { ITestDiscoveryAdapter, ITestResultResolver } from '../common/types'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; import { ExecutionFactoryCreateWithEnvironmentOptions, - ExecutionResult, IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { createDiscoveryErrorPayload, fixLogLinesNoTrailing, startDiscoveryNamedPipe } from '../common/utils'; -import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; import { getEnvironment, runInBackground, useEnvExtension } from '../../../envExt/api.internal'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { createTestingDeferred } from '../common/utils'; +import { buildDiscoveryCommand, buildUnittestEnv as configureSubprocessEnv } from './unittestHelpers'; +import { cleanupOnCancellation, createProcessHandlers, setupDiscoveryPipe } from '../common/discoveryHelpers'; /** - * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. + * Configures the subprocess environment for unittest discovery. + * @param envVarsService Service to retrieve environment variables + * @param uri Workspace URI + * @param discoveryPipeName Name of the discovery pipe to pass to the subprocess + * @returns Configured environment variables for the subprocess + */ +async function configureDiscoveryEnv( + envVarsService: IEnvironmentVariablesProvider | undefined, + uri: Uri, + discoveryPipeName: string, +): Promise { + const envVars = await envVarsService?.getEnvironmentVariables(uri); + const mutableEnv = configureSubprocessEnv(envVars, discoveryPipeName); + return mutableEnv; +} + +/** + * Wrapper class for unittest test discovery. */ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { constructor( @@ -36,181 +46,156 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { private readonly envVarsService?: IEnvironmentVariablesProvider, ) {} - public async discoverTests( + async discoverTests( uri: Uri, executionFactory: IPythonExecutionFactory, token?: CancellationToken, + interpreter?: PythonEnvironment, ): Promise { - const settings = this.configSettings.getSettings(uri); - const { unittestArgs } = settings.testing; - const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; - - const cSource = new CancellationTokenSource(); - // Create a deferred to return to the caller - const deferredReturn = createDeferred(); - - token?.onCancellationRequested(() => { - traceInfo(`Test discovery cancelled.`); - cSource.cancel(); - deferredReturn.resolve(); - }); - - const name = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { - if (!token?.isCancellationRequested) { - this.resultResolver?.resolveDiscovery(data); - } - }, cSource.token); - - // set up env with the pipe name - let env: EnvironmentVariables | undefined = await this.envVarsService?.getEnvironmentVariables(uri); - if (env === undefined) { - env = {} as EnvironmentVariables; - } - env.TEST_RUN_PIPE = name; - - const command = buildDiscoveryCommand(unittestArgs); - const options: TestCommandOptions = { - workspaceFolder: uri, - command, - cwd, - token, - }; - - this.runDiscovery(uri, options, name, cwd, cSource, executionFactory).then(() => { - deferredReturn.resolve(); - }); - - return deferredReturn.promise; - } - - async runDiscovery( - uri: Uri, - options: TestCommandOptions, - testRunPipeName: string, - cwd: string, - cSource: CancellationTokenSource, - executionFactory: IPythonExecutionFactory, - ): Promise { - // get and edit env vars - const mutableEnv = { - ...(await this.envVarsService?.getEnvironmentVariables(uri)), - }; - mutableEnv.TEST_RUN_PIPE = testRunPipeName; - const args = [options.command.script].concat(options.command.args); - - if (options.outChannel) { - options.outChannel.appendLine(`python ${args.join(' ')}`); + // Setup discovery pipe and cancellation + const { + pipeName: discoveryPipeName, + cancellation: discoveryPipeCancellation, + tokenDisposable, + } = await setupDiscoveryPipe(this.resultResolver, token, uri); + + // Setup process handlers deferred (used by both execution paths) + const deferredTillExecClose = createTestingDeferred(); + + // Collect all disposables for cleanup in finally block + const disposables: Disposable[] = []; + if (tokenDisposable) { + disposables.push(tokenDisposable); } - - if (useEnvExtension()) { - const pythonEnv = await getEnvironment(uri); - if (pythonEnv) { - const deferredTillExecClose = createDeferred(); + try { + // Build unittest command and arguments + const settings = this.configSettings.getSettings(uri); + const { unittestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + const execArgs = buildDiscoveryCommand(unittestArgs, EXTENSION_ROOT_DIR); + traceVerbose(`Running unittest discovery with command: ${execArgs.join(' ')} for workspace ${uri.fsPath}.`); + + // Configure subprocess environment + const mutableEnv = await configureDiscoveryEnv(this.envVarsService, uri, discoveryPipeName); + + // Setup process handlers (shared by both execution paths) + const handlers = createProcessHandlers('unittest', uri, cwd, this.resultResolver, deferredTillExecClose); + + // Execute using environment extension if available + if (useEnvExtension()) { + traceInfo(`Using environment extension for unittest discovery in workspace ${uri.fsPath}`); + const pythonEnv = await getEnvironment(uri); + if (!pythonEnv) { + traceError( + `Python environment not found for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + traceVerbose(`Using Python environment: ${JSON.stringify(pythonEnv)}`); const proc = await runInBackground(pythonEnv, { cwd, - args, + args: execArgs, env: (mutableEnv as unknown) as { [key: string]: string }, }); - options.token?.onCancellationRequested(() => { - traceInfo(`Test discovery cancelled, killing unittest subprocess for workspace ${uri.fsPath}`); - proc.kill(); - deferredTillExecClose.resolve(); - cSource.cancel(); - }); - proc.stdout.on('data', (data) => { - const out = fixLogLinesNoTrailing(data.toString()); - traceInfo(out); - }); - proc.stderr.on('data', (data) => { - const out = fixLogLinesNoTrailing(data.toString()); - traceError(out); + traceInfo(`Started unittest discovery subprocess (environment extension) for workspace ${uri.fsPath}`); + + // Wire up cancellation and process events + const envExtCancellationHandler = token?.onCancellationRequested(() => { + cleanupOnCancellation('unittest', proc, deferredTillExecClose, discoveryPipeCancellation, uri); }); + if (envExtCancellationHandler) { + disposables.push(envExtCancellationHandler); + } + proc.stdout.on('data', handlers.onStdout); + proc.stderr.on('data', handlers.onStderr); proc.onExit((code, signal) => { - if (code !== 0) { - traceError( - `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, - ); - } - deferredTillExecClose.resolve(); + handlers.onExit(code, signal); + handlers.onClose(code, signal); }); + await deferredTillExecClose.promise; - } else { - traceError(`Python Environment not found for: ${uri.fsPath}`); + traceInfo(`Unittest discovery completed for workspace ${uri.fsPath}`); + return; } - return; - } - const spawnOptions: SpawnOptions = { - token: options.token, - cwd: options.cwd, - throwOnStdErr: true, - env: mutableEnv, - }; - - try { - traceLog(`Discovering unittest tests for workspace ${options.cwd} with arguments: ${args}\r\n`); - const deferredTillExecClose = createDeferred>(); - - // Create the Python environment in which to execute the command. + // Execute using execution factory (fallback path) + traceInfo(`Using execution factory for unittest discovery in workspace ${uri.fsPath}`); const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, - resource: options.workspaceFolder, + resource: uri, + interpreter, }; const execService = await executionFactory.createActivatedEnvironment(creationOptions); - const execInfo = await execService?.getExecutablePath(); - traceVerbose(`Executable path for unittest discovery: ${execInfo}.`); + if (!execService) { + traceError( + `Failed to create execution service for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + const execInfo = await execService.getExecutablePath(); + traceVerbose(`Using Python executable: ${execInfo} for workspace ${uri.fsPath}`); + + // Check for cancellation before spawning process + if (token?.isCancellationRequested) { + traceInfo(`Unittest discovery cancelled before spawning process for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } + + const spawnOptions: SpawnOptions = { + cwd, + throwOnStdErr: true, + env: mutableEnv, + token, + }; let resultProc: ChildProcess | undefined; - options.token?.onCancellationRequested(() => { - traceInfo(`Test discovery cancelled, killing unittest subprocess for workspace ${uri.fsPath}`); - // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. - if (resultProc) { - resultProc?.kill(); - } else { - deferredTillExecClose.resolve(); - cSource.cancel(); - } - }); - const result = execService?.execObservable(args, spawnOptions); - resultProc = result?.proc; - // Displays output to user and ensure the subprocess doesn't run into buffer overflow. - result?.proc?.stdout?.on('data', (data) => { - const out = fixLogLinesNoTrailing(data.toString()); - traceInfo(out); - }); - result?.proc?.stderr?.on('data', (data) => { - const out = fixLogLinesNoTrailing(data.toString()); - traceError(out); + // Set up cancellation handler after all early return checks + const cancellationHandler = token?.onCancellationRequested(() => { + traceInfo(`Cancellation requested during unittest discovery for workspace ${uri.fsPath}`); + cleanupOnCancellation('unittest', resultProc, deferredTillExecClose, discoveryPipeCancellation, uri); }); + if (cancellationHandler) { + disposables.push(cancellationHandler); + } - result?.proc?.on('exit', (code, signal) => { - // if the child has testIds then this is a run request + try { + const result = execService.execObservable(execArgs, spawnOptions); + resultProc = result?.proc; - if (code !== 0) { - // This occurs when we are running discovery - traceError( - `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${options.cwd}. Creating and sending error discovery payload \n`, - ); - traceError( - `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}. Creating and sending error discovery payload`, - ); - this.resultResolver?.resolveDiscovery(createDiscoveryErrorPayload(code, signal, cwd)); + if (!resultProc) { + traceError(`Failed to spawn unittest discovery subprocess for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; } + traceInfo(`Started unittest discovery subprocess (execution factory) for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error spawning unittest discovery subprocess for workspace ${uri.fsPath}: ${error}`); deferredTillExecClose.resolve(); - }); + throw error; + } + resultProc.stdout?.on('data', handlers.onStdout); + resultProc.stderr?.on('data', handlers.onStderr); + resultProc.on('exit', handlers.onExit); + resultProc.on('close', handlers.onClose); + + traceVerbose(`Waiting for unittest discovery subprocess to complete for workspace ${uri.fsPath}`); await deferredTillExecClose.promise; - } catch (ex) { - traceError(`Error while server attempting to run unittest command for workspace ${uri.fsPath}: ${ex}`); + traceInfo(`Unittest discovery completed for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error during unittest discovery for workspace ${uri.fsPath}: ${error}`); + deferredTillExecClose.resolve(); + throw error; + } finally { + traceVerbose(`Cleaning up unittest discovery resources for workspace ${uri.fsPath}`); + // Dispose all cancellation handlers and event subscriptions + disposables.forEach((d) => d.dispose()); + // Dispose the discovery pipe cancellation token + discoveryPipeCancellation.dispose(); } } } -function buildDiscoveryCommand(args: string[]): TestDiscoveryCommand { - const discoveryScript = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); - - return { - script: discoveryScript, - args: ['--udiscovery', ...args], - }; -} diff --git a/src/client/testing/testController/unittest/unittestHelpers.ts b/src/client/testing/testController/unittest/unittestHelpers.ts new file mode 100644 index 000000000000..249a78dda7b7 --- /dev/null +++ b/src/client/testing/testController/unittest/unittestHelpers.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import { traceInfo } from '../../../logging'; + +/** + * Builds the environment variables required for unittest discovery. + * Sets TEST_RUN_PIPE for communication. + */ +export function buildUnittestEnv( + envVars: { [key: string]: string | undefined } | undefined, + discoveryPipeName: string, +): { [key: string]: string | undefined } { + const mutableEnv = { + ...envVars, + }; + mutableEnv.TEST_RUN_PIPE = discoveryPipeName; + traceInfo(`Environment variables set for unittest discovery: TEST_RUN_PIPE=${mutableEnv.TEST_RUN_PIPE}`); + return mutableEnv; +} + +/** + * Builds the unittest discovery command. + */ +export function buildDiscoveryCommand(args: string[], extensionRootDir: string): string[] { + const discoveryScript = path.join(extensionRootDir, 'python_files', 'unittestadapter', 'discovery.py'); + return [discoveryScript, '--udiscovery', ...args]; +} From a33f1507b00496e631b42b04489e2d2709df40ff Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 28 Nov 2025 21:31:00 -0800 Subject: [PATCH 1061/1136] Add NotRequired type from typing_extensions for type hinting (#25613) fixes https://github.com/microsoft/vscode-python/issues/25600 --- python_files/vscode_pytest/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 91a81bff9a36..ba8b270403ac 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -22,10 +22,10 @@ ) import pytest -from typing_extensions import NotRequired if TYPE_CHECKING: from pluggy import Result + from typing_extensions import NotRequired USES_PYTEST_DESCRIBE = False From fb0bd77a6a9f4b7833546550315c6bf67f4030f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 30 Nov 2025 18:20:56 -0800 Subject: [PATCH 1062/1136] Bump actions/checkout from 5 to 6 (#25601) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
Release notes

Sourced from actions/checkout's releases.

v6.0.0

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v5.0.0...v6.0.0

v6-beta

What's Changed

Updated persist-credentials to store the credentials under $RUNNER_TEMP instead of directly in the local git config.

This requires a minimum Actions Runner version of v2.329.0 to access the persisted credentials for Docker container action scenarios.

v5.0.1

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v5...v5.0.1

Changelog

Sourced from actions/checkout's changelog.

Changelog

V6.0.0

V5.0.1

V5.0.0

V4.3.1

V4.3.0

v4.2.2

v4.2.1

v4.2.0

v4.1.7

v4.1.6

v4.1.5

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- .github/workflows/build.yml | 18 ++++++------ .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/gen-issue-velocity.yml | 2 +- .github/workflows/info-needed-closer.yml | 2 +- .github/workflows/issue-labels.yml | 2 +- .github/workflows/pr-check.yml | 28 +++++++++---------- .../workflows/test-plan-item-validator.yml | 2 +- .github/workflows/triage-info-needed.yml | 4 +-- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b4ac46558b63..45bd02d29733 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -84,12 +84,12 @@ jobs: # vsix-target: alpine-arm64 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'microsoft/python-environment-tools' path: 'python-env-tools' @@ -115,7 +115,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false @@ -135,7 +135,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false @@ -178,7 +178,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: ${{ env.special-working-directory-relative }} persist-credentials: false @@ -218,13 +218,13 @@ jobs: test-suite: [ts-unit, venv, single-workspace, multi-workspace, debugger, functional] steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: ${{ env.special-working-directory-relative }} persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'microsoft/python-environment-tools' path: ${{ env.special-working-directory-relative }}/python-env-tools @@ -426,12 +426,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'microsoft/python-environment-tools' path: ${{ env.special-working-directory-relative }}/python-env-tools diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 168ef0a05b3d..5528fbbe9c0a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/gen-issue-velocity.yml b/.github/workflows/gen-issue-velocity.yml index fdcb41cdaba9..41d79e4074d0 100644 --- a/.github/workflows/gen-issue-velocity.yml +++ b/.github/workflows/gen-issue-velocity.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/info-needed-closer.yml b/.github/workflows/info-needed-closer.yml index 33f53fc20dca..46892a58e800 100644 --- a/.github/workflows/info-needed-closer.yml +++ b/.github/workflows/info-needed-closer.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml index a78ca03d5ee9..dcbd114086e2 100644 --- a/.github/workflows/issue-labels.yml +++ b/.github/workflows/issue-labels.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 102258fb2d18..892d5d56f4fc 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -57,12 +57,12 @@ jobs: # vsix-target: alpine-arm64 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'microsoft/python-environment-tools' path: 'python-env-tools' @@ -87,7 +87,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false @@ -106,12 +106,12 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'microsoft/python-environment-tools' path: 'python-env-tools' @@ -162,7 +162,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: ${{ env.special-working-directory-relative }} persist-credentials: false @@ -215,13 +215,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: ${{ env.special-working-directory-relative }} persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'microsoft/python-environment-tools' path: ${{ env.special-working-directory-relative }}/python-env-tools @@ -412,13 +412,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: ${{ env.special-working-directory-relative }} persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'microsoft/python-environment-tools' path: ${{ env.special-working-directory-relative }}/python-env-tools @@ -452,12 +452,12 @@ jobs: steps: # Need the source to have the tests available. - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'microsoft/python-environment-tools' path: python-env-tools @@ -488,12 +488,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false - name: Checkout Python Environment Tools - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'microsoft/python-environment-tools' path: python-env-tools diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml index 6e62058b04c6..57db4a3e18a7 100644 --- a/.github/workflows/test-plan-item-validator.yml +++ b/.github/workflows/test-plan-item-validator.yml @@ -12,7 +12,7 @@ jobs: if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') steps: - name: Checkout Actions - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml index f61d17c033d7..c7a37ba0c78d 100644 --- a/.github/workflows/triage-info-needed.yml +++ b/.github/workflows/triage-info-needed.yml @@ -15,7 +15,7 @@ jobs: issues: write steps: - name: Checkout Actions - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable @@ -39,7 +39,7 @@ jobs: issues: write steps: - name: Checkout Actions - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'microsoft/vscode-github-triage-actions' ref: stable From d14ccc2cb8fe8bf7f01f29fd7610b77e208a9172 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Sun, 30 Nov 2025 18:22:46 -0800 Subject: [PATCH 1063/1136] Refactor test controller logic to improve clarity and maintainability (#25615) --- .../testing/testController/controller.ts | 438 ++++++++++-------- 1 file changed, 256 insertions(+), 182 deletions(-) diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index b38c9b0bcee1..8c8ce422e3c1 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -29,7 +29,7 @@ import { IConfigurationService, IDisposableRegistry, Resource } from '../../comm import { DelayedTrigger, IDelayedTrigger } from '../../common/utils/delayTrigger'; import { noop } from '../../common/utils/misc'; import { IInterpreterService } from '../../interpreter/contracts'; -import { traceError, traceInfo, traceVerbose } from '../../logging'; +import { traceError, traceVerbose } from '../../logging'; import { IEventNamePropertyMapping, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../common/constants'; @@ -253,101 +253,121 @@ export class PythonTestController implements ITestController, IExtensionSingleAc private async refreshTestDataInternal(uri?: Resource): Promise { this.refreshingStartedEvent.fire(); - if (uri) { - const settings = this.configSettings.getSettings(uri); - const workspace = this.workspaceService.getWorkspaceFolder(uri); - traceInfo(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); - // Ensure we send test telemetry if it gets disabled again - this.sendTestDisabledTelemetry = true; - // ** experiment to roll out NEW test discovery mechanism - if (settings.testing.pytestEnabled) { - if (workspace && workspace.uri) { - const testAdapter = this.testAdapters.get(workspace.uri); - if (testAdapter) { - const testProviderInAdapter = testAdapter.getTestProvider(); - if (testProviderInAdapter !== 'pytest') { - traceError('Test provider in adapter is not pytest. Please reload window.'); - this.surfaceErrorNode( - workspace.uri, - 'Test provider types are not aligned, please reload your VS Code window.', - 'pytest', - ); - return Promise.resolve(); - } - await testAdapter.discoverTests( - this.testController, - this.pythonExecFactory, - this.refreshCancellation.token, - await this.interpreterService.getActiveInterpreter(workspace.uri), - ); - } else { - traceError('Unable to find test adapter for workspace.'); - } - } else { - traceError('Unable to find workspace for given file'); - } - } else if (settings.testing.unittestEnabled) { - if (workspace && workspace.uri) { - const testAdapter = this.testAdapters.get(workspace.uri); - if (testAdapter) { - const testProviderInAdapter = testAdapter.getTestProvider(); - if (testProviderInAdapter !== 'unittest') { - traceError('Test provider in adapter is not unittest. Please reload window.'); - this.surfaceErrorNode( - workspace.uri, - 'Test provider types are not aligned, please reload your VS Code window.', - 'unittest', - ); - return Promise.resolve(); - } - await testAdapter.discoverTests( - this.testController, - this.pythonExecFactory, - this.refreshCancellation.token, - await this.interpreterService.getActiveInterpreter(workspace.uri), - ); - } else { - traceError('Unable to find test adapter for workspace.'); - } - } else { - traceError('Unable to find workspace for given file'); - } + try { + if (uri) { + await this.refreshSingleWorkspace(uri); } else { - if (this.sendTestDisabledTelemetry) { - this.sendTestDisabledTelemetry = false; - sendTelemetryEvent(EventName.UNITTEST_DISABLED); - } - // If we are here we may have to remove an existing node from the tree - // This handles the case where user removes test settings. Which should remove the - // tests for that particular case from the tree view - if (workspace) { - const toDelete: string[] = []; - this.testController.items.forEach((i: TestItem) => { - const w = this.workspaceService.getWorkspaceFolder(i.uri); - if (w?.uri.fsPath === workspace.uri.fsPath) { - toDelete.push(i.id); - } - }); - toDelete.forEach((i) => this.testController.items.delete(i)); - } + await this.refreshAllWorkspaces(); } + } finally { + this.refreshingCompletedEvent.fire(); + } + } + + /** + * Discovers tests for a single workspace. + */ + private async refreshSingleWorkspace(uri: Uri): Promise { + const workspace = this.workspaceService.getWorkspaceFolder(uri); + if (!workspace?.uri) { + traceError('Unable to find workspace for given file'); + return; + } + + const settings = this.configSettings.getSettings(uri); + traceVerbose(`Discover tests for workspace name: ${workspace.name} - uri: ${uri.fsPath}`); + + // Ensure we send test telemetry if it gets disabled again + this.sendTestDisabledTelemetry = true; + + if (settings.testing.pytestEnabled) { + await this.discoverTestsForProvider(workspace.uri, 'pytest'); + } else if (settings.testing.unittestEnabled) { + await this.discoverTestsForProvider(workspace.uri, 'unittest'); } else { - traceVerbose('Testing: Refreshing all test data'); - const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; - await Promise.all( - workspaces.map(async (workspace) => { - if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { - this.commandManager - .executeCommand(constants.Commands.TriggerEnvironmentSelection, workspace.uri) - .then(noop, noop); - return; - } - await this.refreshTestDataInternal(workspace.uri); - }), + await this.handleNoTestProviderEnabled(workspace); + } + } + + /** + * Discovers tests for all workspaces in the workspace folders. + */ + private async refreshAllWorkspaces(): Promise { + traceVerbose('Testing: Refreshing all test data'); + const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; + + await Promise.all( + workspaces.map(async (workspace) => { + if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { + this.commandManager + .executeCommand(constants.Commands.TriggerEnvironmentSelection, workspace.uri) + .then(noop, noop); + return; + } + await this.refreshSingleWorkspace(workspace.uri); + }), + ); + } + + /** + * Discovers tests for a specific test provider (pytest or unittest). + * Validates that the adapter's provider matches the expected provider. + */ + private async discoverTestsForProvider(workspaceUri: Uri, expectedProvider: TestProvider): Promise { + const testAdapter = this.testAdapters.get(workspaceUri); + + if (!testAdapter) { + traceError('Unable to find test adapter for workspace.'); + return; + } + + const actualProvider = testAdapter.getTestProvider(); + if (actualProvider !== expectedProvider) { + traceError(`Test provider in adapter is not ${expectedProvider}. Please reload window.`); + this.surfaceErrorNode( + workspaceUri, + 'Test provider types are not aligned, please reload your VS Code window.', + expectedProvider, ); + return; } - this.refreshingCompletedEvent.fire(); - return Promise.resolve(); + + await testAdapter.discoverTests( + this.testController, + this.pythonExecFactory, + this.refreshCancellation.token, + await this.interpreterService.getActiveInterpreter(workspaceUri), + ); + } + + /** + * Handles the case when no test provider is enabled. + * Sends telemetry and removes test items for the workspace from the tree. + */ + private async handleNoTestProviderEnabled(workspace: WorkspaceFolder): Promise { + if (this.sendTestDisabledTelemetry) { + this.sendTestDisabledTelemetry = false; + sendTelemetryEvent(EventName.UNITTEST_DISABLED); + } + + this.removeTestItemsForWorkspace(workspace); + } + + /** + * Removes all test items belonging to a specific workspace from the test controller. + * This is used when test discovery is disabled for a workspace. + */ + private removeTestItemsForWorkspace(workspace: WorkspaceFolder): void { + const itemsToDelete: string[] = []; + + this.testController.items.forEach((testItem: TestItem) => { + const itemWorkspace = this.workspaceService.getWorkspaceFolder(testItem.uri); + if (itemWorkspace?.uri.fsPath === workspace.uri.fsPath) { + itemsToDelete.push(testItem.id); + } + }); + + itemsToDelete.forEach((id) => this.testController.items.delete(id)); } private async resolveChildren(item: TestItem | undefined): Promise { @@ -378,21 +398,13 @@ export class PythonTestController implements ITestController, IExtensionSingleAc } private async runTests(request: TestRunRequest, token: CancellationToken): Promise { - const workspaces: WorkspaceFolder[] = []; - if (request.include) { - uniq(request.include.map((r) => this.workspaceService.getWorkspaceFolder(r.uri))).forEach((w) => { - if (w) { - workspaces.push(w); - } - }); - } else { - (this.workspaceService.workspaceFolders || []).forEach((w) => workspaces.push(w)); - } + const workspaces = this.getWorkspacesForTestRun(request); const runInstance = this.testController.createTestRun( request, `Running Tests for Workspace(s): ${workspaces.map((w) => w.uri.fsPath).join(';')}`, true, ); + const dispose = token.onCancellationRequested(() => { runInstance.appendOutput(`\nRun instance cancelled.\r\n`); runInstance.end(); @@ -402,87 +414,9 @@ export class PythonTestController implements ITestController, IExtensionSingleAc try { await Promise.all( - workspaces.map(async (workspace) => { - if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { - this.commandManager - .executeCommand(constants.Commands.TriggerEnvironmentSelection, workspace.uri) - .then(noop, noop); - return undefined; - } - const testItems: TestItem[] = []; - // If the run request includes test items then collect only items that belong to - // `workspace`. If there are no items in the run request then just run the `workspace` - // root test node. Include will be `undefined` in the "run all" scenario. - (request.include ?? this.testController.items).forEach((i: TestItem) => { - const w = this.workspaceService.getWorkspaceFolder(i.uri); - if (w?.uri.fsPath === workspace.uri.fsPath) { - testItems.push(i); - } - }); - - const settings = this.configSettings.getSettings(workspace.uri); - if (testItems.length > 0) { - const testAdapter = - this.testAdapters.get(workspace.uri) || - (this.testAdapters.values().next().value as WorkspaceTestAdapter); - - // no profile will have TestRunProfileKind.Coverage if rewrite isn't enabled - if (request.profile?.kind && request.profile?.kind === TestRunProfileKind.Coverage) { - request.profile.loadDetailedCoverage = ( - _testRun: TestRun, - fileCoverage, - _token, - ): Thenable => { - const details = testAdapter.resultResolver.detailedCoverageMap.get( - fileCoverage.uri.fsPath, - ); - if (details === undefined) { - // given file has no detailed coverage data - return Promise.resolve([]); - } - return Promise.resolve(details); - }; - } - - if (settings.testing.pytestEnabled) { - sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { - tool: 'pytest', - debugging: request.profile?.kind === TestRunProfileKind.Debug, - }); - return testAdapter.executeTests( - this.testController, - runInstance, - testItems, - this.pythonExecFactory, - token, - request.profile?.kind, - this.debugLauncher, - await this.interpreterService.getActiveInterpreter(workspace.uri), - ); - } - if (settings.testing.unittestEnabled) { - sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { - tool: 'unittest', - debugging: request.profile?.kind === TestRunProfileKind.Debug, - }); - // ** experiment to roll out NEW test discovery mechanism - return testAdapter.executeTests( - this.testController, - runInstance, - testItems, - this.pythonExecFactory, - token, - request.profile?.kind, - this.debugLauncher, - await this.interpreterService.getActiveInterpreter(workspace.uri), - ); - } - } - if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { - unconfiguredWorkspaces.push(workspace); - } - return Promise.resolve(); - }), + workspaces.map((workspace) => + this.runTestsForWorkspace(workspace, request, runInstance, token, unconfiguredWorkspaces), + ), ); } finally { traceVerbose('Finished running tests, ending runInstance.'); @@ -495,6 +429,146 @@ export class PythonTestController implements ITestController, IExtensionSingleAc } } + /** + * Gets the list of workspaces to run tests for based on the test run request. + */ + private getWorkspacesForTestRun(request: TestRunRequest): WorkspaceFolder[] { + if (request.include) { + const workspaces: WorkspaceFolder[] = []; + uniq(request.include.map((r) => this.workspaceService.getWorkspaceFolder(r.uri))).forEach((w) => { + if (w) { + workspaces.push(w); + } + }); + return workspaces; + } + return Array.from(this.workspaceService.workspaceFolders || []); + } + + /** + * Runs tests for a single workspace. + */ + private async runTestsForWorkspace( + workspace: WorkspaceFolder, + request: TestRunRequest, + runInstance: TestRun, + token: CancellationToken, + unconfiguredWorkspaces: WorkspaceFolder[], + ): Promise { + if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { + this.commandManager + .executeCommand(constants.Commands.TriggerEnvironmentSelection, workspace.uri) + .then(noop, noop); + return; + } + + const testItems = this.getTestItemsForWorkspace(workspace, request); + const settings = this.configSettings.getSettings(workspace.uri); + + if (testItems.length === 0) { + if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { + unconfiguredWorkspaces.push(workspace); + } + return; + } + + const testAdapter = + this.testAdapters.get(workspace.uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); + + this.setupCoverageIfNeeded(request, testAdapter); + + if (settings.testing.pytestEnabled) { + await this.executeTestsForProvider( + workspace, + testAdapter, + testItems, + runInstance, + request, + token, + 'pytest', + ); + } else if (settings.testing.unittestEnabled) { + await this.executeTestsForProvider( + workspace, + testAdapter, + testItems, + runInstance, + request, + token, + 'unittest', + ); + } else { + unconfiguredWorkspaces.push(workspace); + } + } + + /** + * Gets test items that belong to a specific workspace from the run request. + */ + private getTestItemsForWorkspace(workspace: WorkspaceFolder, request: TestRunRequest): TestItem[] { + const testItems: TestItem[] = []; + // If the run request includes test items then collect only items that belong to + // `workspace`. If there are no items in the run request then just run the `workspace` + // root test node. Include will be `undefined` in the "run all" scenario. + (request.include ?? this.testController.items).forEach((i: TestItem) => { + const w = this.workspaceService.getWorkspaceFolder(i.uri); + if (w?.uri.fsPath === workspace.uri.fsPath) { + testItems.push(i); + } + }); + return testItems; + } + + /** + * Sets up detailed coverage loading if the run profile is for coverage. + */ + private setupCoverageIfNeeded(request: TestRunRequest, testAdapter: WorkspaceTestAdapter): void { + // no profile will have TestRunProfileKind.Coverage if rewrite isn't enabled + if (request.profile?.kind && request.profile?.kind === TestRunProfileKind.Coverage) { + request.profile.loadDetailedCoverage = ( + _testRun: TestRun, + fileCoverage, + _token, + ): Thenable => { + const details = testAdapter.resultResolver.detailedCoverageMap.get(fileCoverage.uri.fsPath); + if (details === undefined) { + // given file has no detailed coverage data + return Promise.resolve([]); + } + return Promise.resolve(details); + }; + } + } + + /** + * Executes tests using the test adapter for a specific test provider. + */ + private async executeTestsForProvider( + workspace: WorkspaceFolder, + testAdapter: WorkspaceTestAdapter, + testItems: TestItem[], + runInstance: TestRun, + request: TestRunRequest, + token: CancellationToken, + provider: TestProvider, + ): Promise { + sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { + tool: provider, + debugging: request.profile?.kind === TestRunProfileKind.Debug, + }); + + await testAdapter.executeTests( + this.testController, + runInstance, + testItems, + this.pythonExecFactory, + token, + request.profile?.kind, + this.debugLauncher, + await this.interpreterService.getActiveInterpreter(workspace.uri), + ); + } + private invalidateTests(uri: Uri) { this.testController.items.forEach((root) => { const item = getNodeByUri(root, uri); From 2cb58d515fb9f9c27f7b928f04f5bf0021c19351 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:45:30 -0800 Subject: [PATCH 1064/1136] Fix KeyboardInterrupt in REPL (#25625) Resolves: https://github.com/microsoft/vscode-python/issues/25468 Not consistent repro. I was able to repro on one mac, not the other. We can just use sendText in REPL, users will still be able to access shell integration still. Extension doesnt use exitCode or other feature from executeCommand atm. --- src/client/common/terminal/service.ts | 19 +++---------------- .../common/terminals/service.unit.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index e92fbd3d494f..54c1fd1f795e 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -20,12 +20,9 @@ import { TerminalShellType, } from './types'; import { traceVerbose } from '../../logging'; -import { getConfiguration } from '../vscodeApis/workspaceApis'; import { useEnvExtension } from '../../envExt/api.internal'; import { ensureTerminalLegacy } from '../../envExt/api.legacy'; import { sleep } from '../utils/async'; -import { isWindows } from '../utils/platform'; -import { getPythonMinorVersion } from '../../repl/replUtils'; @injectable() export class TerminalService implements ITerminalService, Disposable { @@ -108,20 +105,10 @@ export class TerminalService implements ITerminalService, Disposable { await promise; } - const config = getConfiguration('python'); - const pythonrcSetting = config.get('terminal.shellIntegration.enabled'); - - const minorVersion = this.options?.resource - ? await getPythonMinorVersion( - this.options.resource, - this.serviceContainer.get(IInterpreterService), - ) - : undefined; - - if ((isPythonShell && !pythonrcSetting) || (isPythonShell && isWindows()) || (minorVersion ?? 0) >= 13) { - // If user has explicitly disabled SI for Python, use sendText for inside Terminal REPL. + if (isPythonShell) { + // Prevent KeyboardInterrupt in Python REPL: https://github.com/microsoft/vscode-python/issues/25468 terminal.sendText(commandLine); - return undefined; + traceVerbose(`Python REPL detected, sendText: ${commandLine}`); } else if (terminal.shellIntegration) { const execution = terminal.shellIntegration.executeCommand(commandLine); traceVerbose(`Shell Integration is enabled, executeCommand: ${commandLine}`); diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts index 63a1cd544940..246a599f17d6 100644 --- a/src/test/common/terminals/service.unit.test.ts +++ b/src/test/common/terminals/service.unit.test.ts @@ -258,7 +258,7 @@ suite('Terminal Service', () => { terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); - test('Ensure sendText is NOT called when Python shell integration and terminal shell integration are both enabled - Mac, Linux && Python < 3.13', async () => { + test('Ensure sendText is called when Python shell integration and terminal shell integration are both enabled - Mac, Linux && Python < 3.13', async () => { isWindowsStub.returns(false); pythonConfig .setup((p) => p.get('terminal.shellIntegration.enabled')) @@ -277,7 +277,7 @@ suite('Terminal Service', () => { service.executeCommand(textToSend, true); terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); - terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.never()); + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); test('Ensure sendText is called when Python shell integration and terminal shell integration are both enabled - Mac, Linux && Python >= 3.13', async () => { From 75323b38757c653579736e27fee0820990edf094 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Fri, 5 Dec 2025 21:38:02 +0530 Subject: [PATCH 1065/1136] Improve resolveFilePath function to correctly handle URIs and file paths (#25632) Fixes https://github.com/Microsoft/vscode-python/issues/25382 --- src/client/chat/utils.ts | 20 ++- src/test/chat/utils.unit.test.ts | 248 +++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+), 6 deletions(-) create mode 100644 src/test/chat/utils.unit.test.ts diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts index bddd26049668..84df2901341b 100644 --- a/src/client/chat/utils.ts +++ b/src/client/chat/utils.ts @@ -19,6 +19,7 @@ import { JUPYTER_EXTENSION_ID, NotebookCellScheme } from '../common/constants'; import { dirname, join } from 'path'; import { resolveEnvironment, useEnvExtension } from '../envExt/api.internal'; import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils'; +import { getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; export interface IResourceReference { resourcePath?: string; @@ -26,14 +27,21 @@ export interface IResourceReference { export function resolveFilePath(filepath?: string): Uri | undefined { if (!filepath) { - return workspace.workspaceFolders ? workspace.workspaceFolders[0].uri : undefined; + const folders = getWorkspaceFolders() ?? []; + return folders.length > 0 ? folders[0].uri : undefined; } - // starts with a scheme - try { - return Uri.parse(filepath); - } catch (e) { - return Uri.file(filepath); + // Check if it's a URI with a scheme (contains "://") + // This handles schemes like "file://", "vscode-notebook://", etc. + // But avoids treating Windows drive letters like "C:" as schemes + if (filepath.includes('://')) { + try { + return Uri.parse(filepath); + } catch { + return Uri.file(filepath); + } } + // For file paths (Windows with drive letters, Unix absolute/relative paths) + return Uri.file(filepath); } /** diff --git a/src/test/chat/utils.unit.test.ts b/src/test/chat/utils.unit.test.ts new file mode 100644 index 000000000000..8d45c1ac118f --- /dev/null +++ b/src/test/chat/utils.unit.test.ts @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { resolveFilePath } from '../../client/chat/utils'; +import * as workspaceApis from '../../client/common/vscodeApis/workspaceApis'; + +suite('Chat Utils - resolveFilePath()', () => { + let getWorkspaceFoldersStub: sinon.SinonStub; + + setup(() => { + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getWorkspaceFoldersStub.returns([]); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('When filepath is undefined or empty', () => { + test('Should return first workspace folder URI when workspace folders exist', () => { + const expectedUri = Uri.file('/test/workspace'); + const mockFolder: WorkspaceFolder = { + uri: expectedUri, + name: 'test', + index: 0, + }; + getWorkspaceFoldersStub.returns([mockFolder]); + + const result = resolveFilePath(undefined); + + expect(result?.toString()).to.equal(expectedUri.toString()); + }); + + test('Should return first folder when multiple workspace folders exist', () => { + const firstUri = Uri.file('/first/workspace'); + const secondUri = Uri.file('/second/workspace'); + const mockFolders: WorkspaceFolder[] = [ + { uri: firstUri, name: 'first', index: 0 }, + { uri: secondUri, name: 'second', index: 1 }, + ]; + getWorkspaceFoldersStub.returns(mockFolders); + + const result = resolveFilePath(undefined); + + expect(result?.toString()).to.equal(firstUri.toString()); + }); + + test('Should return undefined when no workspace folders exist', () => { + getWorkspaceFoldersStub.returns(undefined); + + const result = resolveFilePath(undefined); + + expect(result).to.be.undefined; + }); + + test('Should return undefined when workspace folders is empty array', () => { + getWorkspaceFoldersStub.returns([]); + + const result = resolveFilePath(undefined); + + expect(result).to.be.undefined; + }); + + test('Should return undefined for empty string when no workspace folders', () => { + getWorkspaceFoldersStub.returns(undefined); + + const result = resolveFilePath(''); + + expect(result).to.be.undefined; + }); + }); + + suite('Windows file paths', () => { + test('Should handle Windows path with lowercase drive letter', () => { + const filepath = 'c:\\GIT\\tests\\simple-python-app'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + // Uri.file normalizes drive letters to lowercase + expect(result?.fsPath.toLowerCase()).to.include('git'); + }); + + test('Should handle Windows path with uppercase drive letter', () => { + const filepath = 'C:\\Users\\test\\project'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + expect(result?.fsPath.toLowerCase()).to.include('users'); + }); + + test('Should handle Windows path with forward slashes', () => { + const filepath = 'C:/Users/test/project'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + }); + + suite('Unix file paths', () => { + test('Should handle Unix absolute path', () => { + const filepath = '/home/user/projects/myapp'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + expect(result?.path).to.include('/home/user/projects/myapp'); + }); + + test('Should handle Unix root path', () => { + const filepath = '/'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + }); + + suite('Relative paths', () => { + test('Should handle relative path with dot prefix', () => { + const filepath = './src/main.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should handle relative path without prefix', () => { + const filepath = 'src/main.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should handle parent directory reference', () => { + const filepath = '../other-project/file.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + }); + + suite('URI schemes', () => { + test('Should handle file:// URI scheme', () => { + const filepath = 'file:///home/user/test.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + expect(result?.path).to.include('/home/user/test.py'); + }); + + test('Should handle vscode-notebook:// URI scheme', () => { + const filepath = 'vscode-notebook://jupyter/notebook.ipynb'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('vscode-notebook'); + }); + + test('Should handle untitled: URI scheme without double slash as file path', () => { + const filepath = 'untitled:Untitled-1'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + // untitled: doesn't have ://, so it will be treated as a file path + expect(result?.scheme).to.equal('file'); + }); + + test('Should handle https:// URI scheme', () => { + const filepath = 'https://example.com/path'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('https'); + }); + + test('Should handle vscode-vfs:// URI scheme', () => { + const filepath = 'vscode-vfs://github/microsoft/vscode/file.ts'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('vscode-vfs'); + }); + }); + + suite('Edge cases', () => { + test('Should handle path with spaces', () => { + const filepath = '/home/user/my project/file.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should handle path with special characters', () => { + const filepath = '/home/user/project-name_v2/file.py'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should not treat Windows drive letter colon as URI scheme', () => { + // Windows path should not be confused with a URI scheme + const filepath = 'd:\\projects\\test'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + + test('Should not treat single colon as URI scheme', () => { + // A path with a colon but not :// should be treated as a file + const filepath = 'c:somepath'; + + const result = resolveFilePath(filepath); + + expect(result).to.not.be.undefined; + expect(result?.scheme).to.equal('file'); + }); + }); +}); From 4f98cf281b0d682f907ab22d60995797bd449cd9 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 5 Dec 2025 09:43:19 -0800 Subject: [PATCH 1066/1136] bump to use PET 2025.16 (#25633) --- build/azure-pipeline.stable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 71399281efd9..1815605b278d 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -128,7 +128,7 @@ extends: project: 'Monaco' definition: 593 buildVersionToDownload: 'latestFromBranch' - branchName: 'refs/heads/release/2025.14' + branchName: 'refs/heads/release/2025.16' targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' artifactName: 'bin-$(buildTarget)' itemPattern: | From 7466d105c2016e2fa5fd978d218762b9d9cf4221 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 5 Dec 2025 10:45:32 -0800 Subject: [PATCH 1067/1136] bump version to 2025.20.0 (#25634) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3dd75602dc8..d124b8fc2353 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.19.0-dev", + "version": "2025.20.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.19.0-dev", + "version": "2025.20.0", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/package.json b/package.json index 2d4e67425806..f0da2a5bfab3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.19.0-dev", + "version": "2025.20.0", "featureFlags": { "usingNewInterpreterStorage": true }, From 762180c240be60d92545590f40e836ec94f5425d Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 5 Dec 2025 10:56:55 -0800 Subject: [PATCH 1068/1136] bump version to 2025.21.0-dev (#25635) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d124b8fc2353..402ccef65b8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.20.0", + "version": "2025.21.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.20.0", + "version": "2025.21.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/package.json b/package.json index f0da2a5bfab3..286e80baa2df 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.20.0", + "version": "2025.21.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 6ec13c7c55c00e4f1180b4595358b82b6f28fa55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:21:00 -0800 Subject: [PATCH 1069/1136] Bump jws (#25627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps and [jws](https://github.com/brianloveswords/node-jws). These dependencies needed to be updated together. Updates `jws` from 3.2.2 to 3.2.3
Release notes

Sourced from jws's releases.

v3.2.3

Changed

  • Fix advisory GHSA-869p-cjfg-cm3x: createSign and createVerify now require that a non empty secret is provided (via opts.secret, opts.privateKey or opts.key) when using HMAC algorithms.
  • Upgrading JWA version to 1.4.2, addressing a compatibility issue for Node >= 25.
Changelog

Sourced from jws's changelog.

[3.2.3]

Changed

  • Fix advisory GHSA-869p-cjfg-cm3x: createSign and createVerify now require that a non empty secret is provided (via opts.secret, opts.privateKey or opts.key) when using HMAC algorithms.
  • Upgrading JWA version to 1.4.2, adressing a compatibility issue for Node >= 25.

[3.0.0]

Changed

2.0.0 - 2015-01-30

Changed

  • BREAKING: Default payload encoding changed from binary to utf8. utf8 is a is a more sensible default than binary because many payloads, as far as I can tell, will contain user-facing strings that could be in any language. (6b6de48)

  • Code reorganization, thanks @​fearphage! (7880050)

Added

  • Option in all relevant methods for encoding. For those few users that might be depending on a binary encoding of the messages, this is for them. (6b6de48)
Commits
  • 4f6e73f Merge commit from fork
  • bd0fea5 version 3.2.3
  • 7c3b4b4 Enhance tests for HMAC streaming sign and verify
  • a9b8ed9 Improve secretOrKey initialization in VerifyStream
  • 6707fde Improve secret handling in SignStream
  • See full diff in compare view
Maintainer changes

This version was pushed to npm by julien.wollscheid, a new releaser for jws since your current version.


Updates `jws` from 4.0.0 to 4.0.1
Release notes

Sourced from jws's releases.

v3.2.3

Changed

  • Fix advisory GHSA-869p-cjfg-cm3x: createSign and createVerify now require that a non empty secret is provided (via opts.secret, opts.privateKey or opts.key) when using HMAC algorithms.
  • Upgrading JWA version to 1.4.2, addressing a compatibility issue for Node >= 25.
Changelog

Sourced from jws's changelog.

[3.2.3]

Changed

  • Fix advisory GHSA-869p-cjfg-cm3x: createSign and createVerify now require that a non empty secret is provided (via opts.secret, opts.privateKey or opts.key) when using HMAC algorithms.
  • Upgrading JWA version to 1.4.2, adressing a compatibility issue for Node >= 25.

[3.0.0]

Changed

2.0.0 - 2015-01-30

Changed

  • BREAKING: Default payload encoding changed from binary to utf8. utf8 is a is a more sensible default than binary because many payloads, as far as I can tell, will contain user-facing strings that could be in any language. (6b6de48)

  • Code reorganization, thanks @​fearphage! (7880050)

Added

  • Option in all relevant methods for encoding. For those few users that might be depending on a binary encoding of the messages, this is for them. (6b6de48)
Commits
  • 4f6e73f Merge commit from fork
  • bd0fea5 version 3.2.3
  • 7c3b4b4 Enhance tests for HMAC streaming sign and verify
  • a9b8ed9 Improve secretOrKey initialization in VerifyStream
  • 6707fde Improve secret handling in SignStream
  • See full diff in compare view
Maintainer changes

This version was pushed to npm by julien.wollscheid, a new releaser for jws since your current version.


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 64 +++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 402ccef65b8d..9d0c761b8e5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9263,23 +9263,23 @@ } }, "node_modules/jsonwebtoken/node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "dev": true, "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jsonwebtoken/node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "dev": true, "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -9315,23 +9315,23 @@ "dev": true }, "node_modules/jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "dev": true, "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "dev": true, "dependencies": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -22114,23 +22114,23 @@ }, "dependencies": { "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "dev": true, "requires": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "dev": true, "requires": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } } @@ -22165,23 +22165,23 @@ "dev": true }, "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "dev": true, "requires": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "dev": true, "requires": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, From 7e11d0691bfa74e1bf08c22ac6eacd27424dc2fe Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 8 Dec 2025 07:57:01 -0800 Subject: [PATCH 1070/1136] refactor test resultResolver (#25619) --- .../testing-workflow.instructions.md | 580 +++++++++++ .../testController/common/resultResolver.ts | 476 ++------- .../common/testCoverageHandler.ts | 93 ++ .../common/testDiscoveryHandler.ts | 104 ++ .../common/testExecutionHandler.ts | 231 +++++ .../testController/common/testItemIndex.ts | 225 +++++ .../testing/common/testingAdapter.test.ts | 152 +-- .../common/testCoverageHandler.unit.test.ts | 502 ++++++++++ .../common/testDiscoveryHandler.unit.test.ts | 517 ++++++++++ .../common/testExecutionHandler.unit.test.ts | 922 ++++++++++++++++++ .../common/testItemIndex.unit.test.ts | 359 +++++++ .../resultResolver.unit.test.ts | 49 +- src/test/vscode-mock.ts | 14 + 13 files changed, 3720 insertions(+), 504 deletions(-) create mode 100644 .github/instructions/testing-workflow.instructions.md create mode 100644 src/client/testing/testController/common/testCoverageHandler.ts create mode 100644 src/client/testing/testController/common/testDiscoveryHandler.ts create mode 100644 src/client/testing/testController/common/testExecutionHandler.ts create mode 100644 src/client/testing/testController/common/testItemIndex.ts create mode 100644 src/test/testing/testController/common/testCoverageHandler.unit.test.ts create mode 100644 src/test/testing/testController/common/testDiscoveryHandler.unit.test.ts create mode 100644 src/test/testing/testController/common/testExecutionHandler.unit.test.ts create mode 100644 src/test/testing/testController/common/testItemIndex.unit.test.ts diff --git a/.github/instructions/testing-workflow.instructions.md b/.github/instructions/testing-workflow.instructions.md new file mode 100644 index 000000000000..948886a59635 --- /dev/null +++ b/.github/instructions/testing-workflow.instructions.md @@ -0,0 +1,580 @@ +--- +applyTo: '**/test/**' +--- + +# AI Testing Workflow Guide: Write, Run, and Fix Tests + +This guide provides comprehensive instructions for AI agents on the complete testing workflow: writing tests, running them, diagnosing failures, and fixing issues. Use this guide whenever working with test files or when users request testing tasks. + +## Complete Testing Workflow + +This guide covers the full testing lifecycle: + +1. **📝 Writing Tests** - Create comprehensive test suites +2. **▶️ Running Tests** - Execute tests using VS Code tools +3. **🔍 Diagnosing Issues** - Analyze failures and errors +4. **🛠️ Fixing Problems** - Resolve compilation and runtime issues +5. **✅ Validation** - Ensure coverage and resilience + +### When to Use This Guide + +**User Requests Testing:** + +- "Write tests for this function" +- "Run the tests" +- "Fix the failing tests" +- "Test this code" +- "Add test coverage" + +**File Context Triggers:** + +- Working in `**/test/**` directories +- Files ending in `.test.ts` or `.unit.test.ts` +- Test failures or compilation errors +- Coverage reports or test output analysis + +## Test Types + +When implementing tests as an AI agent, choose between two main types: + +### Unit Tests (`*.unit.test.ts`) + +- **Fast isolated testing** - Mock all external dependencies +- **Use for**: Pure functions, business logic, data transformations +- **Execute with**: `runTests` tool with specific file patterns +- **Mock everything** - VS Code APIs automatically mocked via `/src/test/unittests.ts` + +### Extension Tests (`*.test.ts`) + +- **Full VS Code integration** - Real environment with actual APIs +- **Use for**: Command registration, UI interactions, extension lifecycle +- **Execute with**: VS Code launch configurations or `runTests` tool +- **Slower but comprehensive** - Tests complete user workflows + +## 🤖 Agent Tool Usage for Test Execution + +### Primary Tool: `runTests` + +Use the `runTests` tool to execute tests programmatically rather than terminal commands for better integration and result parsing: + +```typescript +// Run specific test files +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + mode: 'run', +}); + +// Run tests with coverage +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + mode: 'coverage', + coverageFiles: ['/absolute/path/to/source.ts'], +}); + +// Run specific test names +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + testNames: ['should handle edge case', 'should validate input'], +}); +``` + +### Compilation Requirements + +Before running tests, ensure compilation. Always start compilation with `npm run watch-tests` before test execution to ensure TypeScript files are built. Recompile after making import/export changes before running tests, as stubs won't work if they're applied to old compiled JavaScript that doesn't have the updated imports: + +```typescript +// Start watch mode for auto-compilation +await run_in_terminal({ + command: 'npm run watch-tests', + isBackground: true, + explanation: 'Start test compilation in watch mode', +}); + +// Or compile manually +await run_in_terminal({ + command: 'npm run compile-tests', + isBackground: false, + explanation: 'Compile TypeScript test files', +}); +``` + +### Alternative: Terminal Execution + +For targeted test runs when `runTests` tool is unavailable. Note: When a targeted test run yields 0 tests, first verify the compiled JS exists under `out/test` (rootDir is `src`); absence almost always means the test file sits outside `src` or compilation hasn't run yet: + +```typescript +// Run specific test suite +await run_in_terminal({ + command: 'npm run unittest -- --grep "Suite Name"', + isBackground: false, + explanation: 'Run targeted unit tests', +}); +``` + +## 🔍 Diagnosing Test Failures + +### Common Failure Patterns + +**Compilation Errors:** + +```typescript +// Missing imports +if (error.includes('Cannot find module')) { + await addMissingImports(testFile); +} + +// Type mismatches +if (error.includes("Type '" && error.includes("' is not assignable"))) { + await fixTypeIssues(testFile); +} +``` + +**Runtime Errors:** + +```typescript +// Mock setup issues +if (error.includes('stub') || error.includes('mock')) { + await fixMockConfiguration(testFile); +} + +// Assertion failures +if (error.includes('AssertionError')) { + await analyzeAssertionFailure(error); +} +``` + +### Systematic Failure Analysis + +Fix test issues iteratively - run tests, analyze failures, apply fixes, repeat until passing. When unit tests fail with VS Code API errors like `TypeError: X is not a constructor` or `Cannot read properties of undefined (reading 'Y')`, check if VS Code APIs are properly mocked in `/src/test/unittests.ts` - add missing APIs following the existing pattern. + +```typescript +interface TestFailureAnalysis { + type: 'compilation' | 'runtime' | 'assertion' | 'timeout'; + message: string; + location: { file: string; line: number; col: number }; + suggestedFix: string; +} + +function analyzeFailure(failure: TestFailure): TestFailureAnalysis { + if (failure.message.includes('Cannot find module')) { + return { + type: 'compilation', + message: failure.message, + location: failure.location, + suggestedFix: 'Add missing import statement', + }; + } + // ... other failure patterns +} +``` + +### Agent Decision Logic for Test Type Selection + +**Choose Unit Tests (`*.unit.test.ts`) when analyzing:** + +- Functions with clear inputs/outputs and no VS Code API dependencies +- Data transformation, parsing, or utility functions +- Business logic that can be isolated with mocks +- Error handling scenarios with predictable inputs + +**Choose Extension Tests (`*.test.ts`) when analyzing:** + +- Functions that register VS Code commands or use `vscode.*` APIs +- UI components, tree views, or command palette interactions +- File system operations requiring workspace context +- Extension lifecycle events (activation, deactivation) + +**Agent Implementation Pattern:** + +```typescript +function determineTestType(functionCode: string): 'unit' | 'extension' { + if ( + functionCode.includes('vscode.') || + functionCode.includes('commands.register') || + functionCode.includes('window.') || + functionCode.includes('workspace.') + ) { + return 'extension'; + } + return 'unit'; +} +``` + +## 🎯 Step 1: Automated Function Analysis + +As an AI agent, analyze the target function systematically: + +### Code Analysis Checklist + +```typescript +interface FunctionAnalysis { + name: string; + inputs: string[]; // Parameter types and names + outputs: string; // Return type + dependencies: string[]; // External modules/APIs used + sideEffects: string[]; // Logging, file system, network calls + errorPaths: string[]; // Exception scenarios + testType: 'unit' | 'extension'; +} +``` + +### Analysis Implementation + +1. **Read function source** using `read_file` tool +2. **Identify imports** - look for `vscode.*`, `child_process`, `fs`, etc. +3. **Map data flow** - trace inputs through transformations to outputs +4. **Catalog dependencies** - external calls that need mocking +5. **Document side effects** - logging, file operations, state changes + +### Test Setup Differences + +#### Unit Test Setup (\*.unit.test.ts) + +```typescript +// Mock VS Code APIs - handled automatically by unittests.ts +import * as sinon from 'sinon'; +import * as workspaceApis from '../../common/workspace.apis'; // Wrapper functions + +// Stub wrapper functions, not VS Code APIs directly +// Always mock wrapper functions (e.g., workspaceApis.getConfiguration()) instead of +// VS Code APIs directly to avoid stubbing issues +const mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); +``` + +#### Extension Test Setup (\*.test.ts) + +```typescript +// Use real VS Code APIs +import * as vscode from 'vscode'; + +// Real VS Code APIs available - no mocking needed +const config = vscode.workspace.getConfiguration('python'); +``` + +## 🎯 Step 2: Generate Test Coverage Matrix + +Based on function analysis, automatically generate comprehensive test scenarios: + +### Coverage Matrix Generation + +```typescript +interface TestScenario { + category: 'happy-path' | 'edge-case' | 'error-handling' | 'side-effects'; + description: string; + inputs: Record; + expectedOutput?: any; + expectedSideEffects?: string[]; + shouldThrow?: boolean; +} +``` + +### Automated Scenario Creation + +1. **Happy Path**: Normal execution with typical inputs +2. **Edge Cases**: Boundary conditions, empty/null inputs, unusual but valid data +3. **Error Scenarios**: Invalid inputs, dependency failures, exception paths +4. **Side Effects**: Verify logging calls, file operations, state changes + +### Agent Pattern for Scenario Generation + +```typescript +function generateTestScenarios(analysis: FunctionAnalysis): TestScenario[] { + const scenarios: TestScenario[] = []; + + // Generate happy path for each input combination + scenarios.push(...generateHappyPathScenarios(analysis)); + + // Generate edge cases for boundary conditions + scenarios.push(...generateEdgeCaseScenarios(analysis)); + + // Generate error scenarios for each dependency + scenarios.push(...generateErrorScenarios(analysis)); + + return scenarios; +} +``` + +## 🗺️ Step 3: Plan Your Test Coverage + +### Create a Test Coverage Matrix + +#### Main Flows + +- ✅ **Happy path scenarios** - normal expected usage +- ✅ **Alternative paths** - different configuration combinations +- ✅ **Integration scenarios** - multiple features working together + +#### Edge Cases + +- 🔸 **Boundary conditions** - empty inputs, missing data +- 🔸 **Error scenarios** - network failures, permission errors +- 🔸 **Data validation** - invalid inputs, type mismatches + +#### Real-World Scenarios + +- ✅ **Fresh install** - clean slate +- ✅ **Existing user** - migration scenarios +- ✅ **Power user** - complex configurations +- 🔸 **Error recovery** - graceful degradation + +### Example Test Plan Structure + +```markdown +## Test Categories + +### 1. Configuration Migration Tests + +- No legacy settings exist +- Legacy settings already migrated +- Fresh migration needed +- Partial migration required +- Migration failures + +### 2. Configuration Source Tests + +- Global search paths +- Workspace search paths +- Settings precedence +- Configuration errors + +### 3. Path Resolution Tests + +- Absolute vs relative paths +- Workspace folder resolution +- Path validation and filtering + +### 4. Integration Scenarios + +- Combined configurations +- Deduplication logic +- Error handling flows +``` + +## 🔧 Step 4: Set Up Your Test Infrastructure + +### Test File Structure + +```typescript +// 1. Imports - group logically +import assert from 'node:assert'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as logging from '../../../common/logging'; +import * as pathUtils from '../../../common/utils/pathUtils'; +import * as workspaceApis from '../../../common/workspace.apis'; + +// 2. Function under test +import { getAllExtraSearchPaths } from '../../../managers/common/nativePythonFinder'; + +// 3. Mock interfaces +interface MockWorkspaceConfig { + get: sinon.SinonStub; + inspect: sinon.SinonStub; + update: sinon.SinonStub; +} +``` + +### Mock Setup Strategy + +Create minimal mock objects with only required methods and use TypeScript type assertions (e.g., `mockApi as PythonEnvironmentApi`) to satisfy interface requirements instead of implementing all interface methods when only specific methods are needed for the test. Simplify mock setup by only mocking methods actually used in tests and use `as unknown as Type` for TypeScript compatibility. + +```typescript +suite('Function Integration Tests', () => { + // 1. Declare all mocks + let mockGetConfiguration: sinon.SinonStub; + let mockGetWorkspaceFolders: sinon.SinonStub; + let mockTraceLog: sinon.SinonStub; + let mockTraceError: sinon.SinonStub; + let mockTraceWarn: sinon.SinonStub; + + // 2. Mock complex objects + let pythonConfig: MockWorkspaceConfig; + let envConfig: MockWorkspaceConfig; + + setup(() => { + // 3. Initialize all mocks + mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); + mockGetWorkspaceFolders = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + mockTraceLog = sinon.stub(logging, 'traceLog'); + mockTraceError = sinon.stub(logging, 'traceError'); + mockTraceWarn = sinon.stub(logging, 'traceWarn'); + + // 4. Set up default behaviors + mockGetWorkspaceFolders.returns(undefined); + + // 5. Create mock configuration objects + // When fixing mock environment creation, use null to truly omit + // properties rather than undefined + pythonConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + + envConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + }); + + teardown(() => { + sinon.restore(); // Always clean up! + }); +}); +``` + +## Step 4: Write Tests Using Mock → Run → Assert Pattern + +### The Three-Phase Pattern + +#### Phase 1: Mock (Set up the scenario) + +```typescript +test('Description of what this tests', async () => { + // Mock → Clear description of the scenario + pythonConfig.inspect.withArgs('venvPath').returns({ globalValue: '/path' }); + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); + mockGetWorkspaceFolders.returns([{ uri: Uri.file('/workspace') }]); +``` + +#### Phase 2: Run (Execute the function) + +```typescript +// Run +const result = await getAllExtraSearchPaths(); +``` + +#### Phase 3: Assert (Verify the behavior) + +```typescript + // Assert - Use set-based comparison for order-agnostic testing + const expected = new Set(['/expected', '/paths']); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + + // Verify side effects + // Use sinon.match() patterns for resilient assertions that don't break on minor output changes + assert(mockTraceLog.calledWith(sinon.match(/completion/i)), 'Should log completion'); +}); +``` + +## Step 6: Make Tests Resilient + +### Use Order-Agnostic Comparisons + +```typescript +// ❌ Brittle - depends on order +assert.deepStrictEqual(result, ['/path1', '/path2', '/path3']); + +// ✅ Resilient - order doesn't matter +const expected = new Set(['/path1', '/path2', '/path3']); +const actual = new Set(result); +assert.strictEqual(actual.size, expected.size, 'Should have correct number of paths'); +assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); +``` + +### Use Flexible Error Message Testing + +```typescript +// ❌ Brittle - exact text matching +assert(mockTraceError.calledWith('Error during legacy python settings migration:')); + +// ✅ Resilient - pattern matching +assert(mockTraceError.calledWith(sinon.match.string, sinon.match.instanceOf(Error)), 'Should log migration error'); + +// ✅ Resilient - key terms with regex +assert(mockTraceError.calledWith(sinon.match(/migration.*error/i)), 'Should log migration error'); +``` + +### Handle Complex Mock Scenarios + +```typescript +// For functions that call the same mock multiple times +envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); +envConfig.inspect + .withArgs('globalSearchPaths') + .onSecondCall() + .returns({ + globalValue: ['/migrated/paths'], + }); + +// Testing async functions with child processes: +// Call the function first to get a promise, then use setTimeout to emit mock events, +// then await the promise - this ensures proper timing of mock setup versus function execution + +// Cannot stub internal function calls within the same module after import - stub external +// dependencies instead (e.g., stub childProcessApis.spawnProcess rather than trying to stub +// helpers.isUvInstalled when testing helpers.shouldUseUv) because intra-module calls use +// direct references, not module exports +``` + +## 🧪 Step 7: Test Categories and Patterns + +### Configuration Tests + +- Test different setting combinations +- Test setting precedence (workspace > user > default) +- Test configuration errors and recovery +- Always use dynamic path construction with Node.js `path` module when testing functions that resolve paths against workspace folders to ensure cross-platform compatibility + +### Data Flow Tests + +- Test how data moves through the system +- Test transformations (path resolution, filtering) +- Test state changes (migrations, updates) + +### Error Handling Tests + +- Test graceful degradation +- Test error logging +- Test fallback behaviors + +### Integration Tests + +- Test multiple features together +- Test real-world scenarios +- Test edge case combinations + +## 📊 Step 8: Review and Refine + +### Test Quality Checklist + +- [ ] **Clear naming** - test names describe the scenario and expected outcome +- [ ] **Good coverage** - main flows, edge cases, error scenarios +- [ ] **Resilient assertions** - won't break due to minor changes +- [ ] **Readable structure** - follows Mock → Run → Assert pattern +- [ ] **Isolated tests** - each test is independent +- [ ] **Fast execution** - tests run quickly with proper mocking + +### Common Anti-Patterns to Avoid + +- ❌ Testing implementation details instead of behavior +- ❌ Brittle assertions that break on cosmetic changes +- ❌ Order-dependent tests that fail due to processing changes +- ❌ Tests that don't clean up mocks properly +- ❌ Overly complex test setup that's hard to understand + +## 🔄 Reviewing and Improving Existing Tests + +### Quick Review Process + +1. **Read test files** - Check structure and mock setup +2. **Run tests** - Establish baseline functionality +3. **Apply improvements** - Use patterns below. When reviewing existing tests, focus on behavior rather than implementation details in test names and assertions +4. **Verify** - Ensure tests still pass + +### Common Fixes + +- Over-complex mocks → Minimal mocks with only needed methods +- Brittle assertions → Behavior-focused with error messages +- Vague test names → Clear scenario descriptions (transform "should return X when Y" into "should [expected behavior] when [scenario context]") +- Missing structure → Mock → Run → Assert pattern +- Untestable Node.js APIs → Create proxy abstraction functions (use function overloads to preserve intelligent typing while making functions mockable) + +## 🧠 Agent Learnings + +- When mocking `testController.createTestItem()` in unit tests, use `typemoq.It.isAny()` for parameters when testing handler behavior (not ID/label generation logic), but consider using specific matchers (e.g., `It.is((id: string) => id.startsWith('_error_'))`) when the actual values being passed are important for correctness - this balances test precision with maintainability (2) +- Remove unused variables from test code immediately - leftover tracking variables like `validationCallCount` that aren't referenced indicate dead code that should be simplified (1) +- Use `Uri.file(path).fsPath` for both sides of path comparisons in tests to ensure cross-platform compatibility - Windows converts forward slashes to backslashes automatically (1) diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index b92e7a870f20..959d08fee1a9 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -1,470 +1,106 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { - CancellationToken, - TestController, - TestItem, - Uri, - TestMessage, - Location, - TestRun, - MarkdownString, - TestCoverageCount, - FileCoverage, - FileCoverageDetail, - StatementCoverage, - Range, -} from 'vscode'; -import * as util from 'util'; -import { - CoveragePayload, - DiscoveredTestPayload, - ExecutionTestPayload, - FileCoverageMetrics, - ITestResultResolver, -} from './types'; +import { CancellationToken, TestController, TestItem, Uri, TestRun, FileCoverageDetail } from 'vscode'; +import { CoveragePayload, DiscoveredTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; import { TestProvider } from '../../types'; -import { traceError, traceVerbose } from '../../../logging'; -import { Testing } from '../../../common/utils/localize'; -import { clearAllChildren, createErrorTestItem, getTestCaseNodes } from './testItemUtilities'; +import { traceInfo } from '../../../logging'; import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; -import { splitLines } from '../../../common/stringUtils'; -import { buildErrorNodeOptions, populateTestTree, splitTestNameWithRegex } from './utils'; +import { TestItemIndex } from './testItemIndex'; +import { TestDiscoveryHandler } from './testDiscoveryHandler'; +import { TestExecutionHandler } from './testExecutionHandler'; +import { TestCoverageHandler } from './testCoverageHandler'; export class PythonResultResolver implements ITestResultResolver { testController: TestController; testProvider: TestProvider; - public runIdToTestItem: Map; + private testItemIndex: TestItemIndex; - public runIdToVSid: Map; - - public vsIdToRunId: Map; - - public subTestStats: Map = new Map(); + // Shared singleton handlers + private static discoveryHandler: TestDiscoveryHandler = new TestDiscoveryHandler(); + private static executionHandler: TestExecutionHandler = new TestExecutionHandler(); + private static coverageHandler: TestCoverageHandler = new TestCoverageHandler(); public detailedCoverageMap = new Map(); constructor(testController: TestController, testProvider: TestProvider, private workspaceUri: Uri) { this.testController = testController; this.testProvider = testProvider; - - this.runIdToTestItem = new Map(); - this.runIdToVSid = new Map(); - this.vsIdToRunId = new Map(); + // Initialize a new TestItemIndex which will be used to track test items in this workspace + this.testItemIndex = new TestItemIndex(); } - public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void { - if (!payload) { - // No test data is available - } else { - this._resolveDiscovery(payload as DiscoveredTestPayload, token); - } + // Expose for backward compatibility (WorkspaceTestAdapter accesses these) + public get runIdToTestItem(): Map { + return this.testItemIndex.runIdToTestItemMap; } - public _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void { - const workspacePath = this.workspaceUri.fsPath; - const rawTestData = payload as DiscoveredTestPayload; - // Check if there were any errors in the discovery process. - if (rawTestData.status === 'error') { - const testingErrorConst = - this.testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; - const { error } = rawTestData; - traceError(testingErrorConst, 'for workspace: ', workspacePath, '\r\n', error?.join('\r\n\r\n') ?? ''); - - let errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`); - const message = util.format( - `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, - error?.join('\r\n\r\n') ?? '', - ); - - if (errorNode === undefined) { - const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider); - errorNode = createErrorTestItem(this.testController, options); - this.testController.items.add(errorNode); - } - const errorNodeLabel: MarkdownString = new MarkdownString( - `[Show output](command:python.viewOutput) to view error logs`, - ); - errorNodeLabel.isTrusted = true; - errorNode.error = errorNodeLabel; - } else { - // remove error node only if no errors exist. - this.testController.items.delete(`DiscoveryError:${workspacePath}`); - } - if (rawTestData.tests || rawTestData.tests === null) { - // if any tests exist, they should be populated in the test tree, regardless of whether there were errors or not. - // parse and insert test data. - - // Clear existing mappings before rebuilding test tree - this.runIdToTestItem.clear(); - this.runIdToVSid.clear(); - this.vsIdToRunId.clear(); + public get runIdToVSid(): Map { + return this.testItemIndex.runIdToVSidMap; + } - // If the test root for this folder exists: Workspace refresh, update its children. - // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. - populateTestTree(this.testController, rawTestData.tests, undefined, this, token); - } + public get vsIdToRunId(): Map { + return this.testItemIndex.vsIdToRunIdMap; + } + public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void { + PythonResultResolver.discoveryHandler.processDiscovery( + payload, + this.testController, + this.testItemIndex, + this.workspaceUri, + this.testProvider, + token, + ); sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: false, }); } + public _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void { + // Delegate to the public method for backward compatibility + this.resolveDiscovery(payload, token); + } + public resolveExecution(payload: ExecutionTestPayload | CoveragePayload, runInstance: TestRun): void { if ('coverage' in payload) { // coverage data is sent once per connection - traceVerbose('Coverage data received.'); - this._resolveCoverage(payload as CoveragePayload, runInstance); + traceInfo('Coverage data received, processing...'); + this.detailedCoverageMap = PythonResultResolver.coverageHandler.processCoverage( + payload as CoveragePayload, + runInstance, + ); + traceInfo('Coverage data processing complete.'); } else { - this._resolveExecution(payload as ExecutionTestPayload, runInstance); - } - } - - public _resolveCoverage(payload: CoveragePayload, runInstance: TestRun): void { - if (payload.result === undefined) { - return; - } - for (const [key, value] of Object.entries(payload.result)) { - const fileNameStr = key; - const fileCoverageMetrics: FileCoverageMetrics = value; - const linesCovered = fileCoverageMetrics.lines_covered ? fileCoverageMetrics.lines_covered : []; // undefined if no lines covered - const linesMissed = fileCoverageMetrics.lines_missed ? fileCoverageMetrics.lines_missed : []; // undefined if no lines missed - const executedBranches = fileCoverageMetrics.executed_branches; - const totalBranches = fileCoverageMetrics.total_branches; - - const lineCoverageCount = new TestCoverageCount( - linesCovered.length, - linesCovered.length + linesMissed.length, + PythonResultResolver.executionHandler.processExecution( + payload as ExecutionTestPayload, + runInstance, + this.testItemIndex, + this.testController, ); - let fileCoverage: FileCoverage; - const uri = Uri.file(fileNameStr); - if (totalBranches === -1) { - // branch coverage was not enabled and should not be displayed - fileCoverage = new FileCoverage(uri, lineCoverageCount); - } else { - const branchCoverageCount = new TestCoverageCount(executedBranches, totalBranches); - fileCoverage = new FileCoverage(uri, lineCoverageCount, branchCoverageCount); - } - runInstance.addCoverage(fileCoverage); - - // create detailed coverage array for each file (only line coverage on detailed, not branch) - const detailedCoverageArray: FileCoverageDetail[] = []; - // go through all covered lines, create new StatementCoverage, and add to detailedCoverageArray - for (const line of linesCovered) { - // line is 1-indexed, so we need to subtract 1 to get the 0-indexed line number - // true value means line is covered - const statementCoverage = new StatementCoverage( - true, - new Range(line - 1, 0, line - 1, Number.MAX_SAFE_INTEGER), - ); - detailedCoverageArray.push(statementCoverage); - } - for (const line of linesMissed) { - // line is 1-indexed, so we need to subtract 1 to get the 0-indexed line number - // false value means line is NOT covered - const statementCoverage = new StatementCoverage( - false, - new Range(line - 1, 0, line - 1, Number.MAX_SAFE_INTEGER), - ); - detailedCoverageArray.push(statementCoverage); - } - - this.detailedCoverageMap.set(uri.fsPath, detailedCoverageArray); } } - /** - * Collect all test case items from the test controller tree. - * Note: This performs full tree traversal - use cached lookups when possible. - */ - private collectAllTestCases(): TestItem[] { - const testCases: TestItem[] = []; - - this.testController.items.forEach((i) => { - const tempArr: TestItem[] = getTestCaseNodes(i); - testCases.push(...tempArr); - }); - - return testCases; - } - - /** - * Find a test item efficiently using cached maps with fallback strategies. - * Uses a three-tier approach: direct lookup, ID mapping, then tree search. - */ - private findTestItemByIdEfficient(keyTemp: string): TestItem | undefined { - // Try direct O(1) lookup first - const directItem = this.runIdToTestItem.get(keyTemp); - if (directItem) { - // Validate the item is still in the test tree - if (this.isTestItemValid(directItem)) { - return directItem; - } else { - // Clean up stale reference - this.runIdToTestItem.delete(keyTemp); - } - } - - // Try vsId mapping as fallback - const vsId = this.runIdToVSid.get(keyTemp); - if (vsId) { - // Search by VS Code ID in the controller - let foundItem: TestItem | undefined; - this.testController.items.forEach((item) => { - if (item.id === vsId) { - foundItem = item; - return; - } - if (!foundItem) { - item.children.forEach((child) => { - if (child.id === vsId) { - foundItem = child; - } - }); - } - }); - - if (foundItem) { - // Cache for future lookups - this.runIdToTestItem.set(keyTemp, foundItem); - return foundItem; - } else { - // Clean up stale mapping - this.runIdToVSid.delete(keyTemp); - this.vsIdToRunId.delete(vsId); - } - } - - // Last resort: full tree search - traceError(`Falling back to tree search for test: ${keyTemp}`); - const testCases = this.collectAllTestCases(); - return testCases.find((item) => item.id === vsId); + public _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): void { + // Delegate to the public method for backward compatibility + this.resolveExecution(payload, runInstance); } - /** - * Check if a TestItem is still valid (exists in the TestController tree) - * - * Time Complexity: O(depth) where depth is the maximum nesting level of the test tree. - * In most cases this is O(1) to O(3) since test trees are typically shallow. - */ - private isTestItemValid(testItem: TestItem): boolean { - // Simple validation: check if the item's parent chain leads back to the controller - let current: TestItem | undefined = testItem; - while (current?.parent) { - current = current.parent; - } - - // If we reached a root item, check if it's in the controller - if (current) { - return this.testController.items.get(current.id) === current; - } - - // If no parent chain, check if it's directly in the controller - return this.testController.items.get(testItem.id) === testItem; + public _resolveCoverage(payload: CoveragePayload, runInstance: TestRun): void { + // Delegate to the public method for backward compatibility + this.resolveExecution(payload, runInstance); } /** * Clean up stale test item references from the cache maps. * Validates cached items and removes any that are no longer in the test tree. + * Delegates to TestItemIndex. */ public cleanupStaleReferences(): void { - const staleRunIds: string[] = []; - - // Check all runId->TestItem mappings - this.runIdToTestItem.forEach((testItem, runId) => { - if (!this.isTestItemValid(testItem)) { - staleRunIds.push(runId); - } - }); - - // Remove stale entries - staleRunIds.forEach((runId) => { - const vsId = this.runIdToVSid.get(runId); - this.runIdToTestItem.delete(runId); - this.runIdToVSid.delete(runId); - if (vsId) { - this.vsIdToRunId.delete(vsId); - } - }); - - if (staleRunIds.length > 0) { - traceVerbose(`Cleaned up ${staleRunIds.length} stale test item references`); - } - } - - /** - * Handle test items that errored during execution. - * Extracts error details, finds the corresponding TestItem, and reports the error to VS Code's Test Explorer. - */ - private handleTestError(keyTemp: string, testItem: any, runInstance: TestRun): void { - const rawTraceback = testItem.traceback ?? ''; - const traceback = splitLines(rawTraceback, { - trim: false, - removeEmptyEntries: true, - }).join('\r\n'); - const text = `${testItem.test} failed with error: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; - const message = new TestMessage(text); - - const foundItem = this.findTestItemByIdEfficient(keyTemp); - - if (foundItem?.uri) { - if (foundItem.range) { - message.location = new Location(foundItem.uri, foundItem.range); - } - runInstance.errored(foundItem, message); - } - } - - /** - * Handle test items that failed during execution - */ - private handleTestFailure(keyTemp: string, testItem: any, runInstance: TestRun): void { - const rawTraceback = testItem.traceback ?? ''; - const traceback = splitLines(rawTraceback, { - trim: false, - removeEmptyEntries: true, - }).join('\r\n'); - - const text = `${testItem.test} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; - const message = new TestMessage(text); - - const foundItem = this.findTestItemByIdEfficient(keyTemp); - - if (foundItem?.uri) { - if (foundItem.range) { - message.location = new Location(foundItem.uri, foundItem.range); - } - runInstance.failed(foundItem, message); - } - } - - /** - * Handle test items that passed during execution - */ - private handleTestSuccess(keyTemp: string, runInstance: TestRun): void { - const grabTestItem = this.runIdToTestItem.get(keyTemp); - - if (grabTestItem !== undefined) { - const foundItem = this.findTestItemByIdEfficient(keyTemp); - if (foundItem?.uri) { - runInstance.passed(grabTestItem); - } - } - } - - /** - * Handle test items that were skipped during execution - */ - private handleTestSkipped(keyTemp: string, runInstance: TestRun): void { - const grabTestItem = this.runIdToTestItem.get(keyTemp); - - if (grabTestItem !== undefined) { - const foundItem = this.findTestItemByIdEfficient(keyTemp); - if (foundItem?.uri) { - runInstance.skipped(grabTestItem); - } - } - } - - /** - * Handle subtest failures - */ - private handleSubtestFailure(keyTemp: string, testItem: any, runInstance: TestRun): void { - const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); - const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); - - if (parentTestItem) { - const subtestStats = this.subTestStats.get(parentTestCaseId); - if (subtestStats) { - subtestStats.failed += 1; - } else { - this.subTestStats.set(parentTestCaseId, { - failed: 1, - passed: 0, - }); - clearAllChildren(parentTestItem); - } - - const subTestItem = this.testController?.createTestItem(subtestId, subtestId, parentTestItem.uri); - - if (subTestItem) { - const traceback = testItem.traceback ?? ''; - const text = `${testItem.subtest} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; - parentTestItem.children.add(subTestItem); - runInstance.started(subTestItem); - const message = new TestMessage(text); - if (parentTestItem.uri && parentTestItem.range) { - message.location = new Location(parentTestItem.uri, parentTestItem.range); - } - runInstance.failed(subTestItem, message); - } else { - throw new Error('Unable to create new child node for subtest'); - } - } else { - throw new Error('Parent test item not found'); - } - } - - /** - * Handle subtest successes - */ - private handleSubtestSuccess(keyTemp: string, runInstance: TestRun): void { - const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp); - const parentTestItem = this.runIdToTestItem.get(parentTestCaseId); - - if (parentTestItem) { - const subtestStats = this.subTestStats.get(parentTestCaseId); - if (subtestStats) { - subtestStats.passed += 1; - } else { - this.subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 }); - clearAllChildren(parentTestItem); - } - - const subTestItem = this.testController?.createTestItem(subtestId, subtestId, parentTestItem.uri); - - if (subTestItem) { - parentTestItem.children.add(subTestItem); - runInstance.started(subTestItem); - runInstance.passed(subTestItem); - } else { - throw new Error('Unable to create new child node for subtest'); - } - } else { - throw new Error('Parent test item not found'); - } - } - - /** - * Process test execution results and update VS Code's Test Explorer with outcomes. - * Uses efficient lookup methods to handle large numbers of test results. - */ - public _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): void { - const rawTestExecData = payload as ExecutionTestPayload; - if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { - for (const keyTemp of Object.keys(rawTestExecData.result)) { - const testItem = rawTestExecData.result[keyTemp]; - - // Delegate to specific outcome handlers using efficient lookups - if (testItem.outcome === 'error') { - this.handleTestError(keyTemp, testItem, runInstance); - } else if (testItem.outcome === 'failure' || testItem.outcome === 'passed-unexpected') { - this.handleTestFailure(keyTemp, testItem, runInstance); - } else if (testItem.outcome === 'success' || testItem.outcome === 'expected-failure') { - this.handleTestSuccess(keyTemp, runInstance); - } else if (testItem.outcome === 'skipped') { - this.handleTestSkipped(keyTemp, runInstance); - } else if (testItem.outcome === 'subtest-failure') { - this.handleSubtestFailure(keyTemp, testItem, runInstance); - } else if (testItem.outcome === 'subtest-success') { - this.handleSubtestSuccess(keyTemp, runInstance); - } - } - } + this.testItemIndex.cleanupStaleReferences(this.testController); } } diff --git a/src/client/testing/testController/common/testCoverageHandler.ts b/src/client/testing/testController/common/testCoverageHandler.ts new file mode 100644 index 000000000000..81ec80579730 --- /dev/null +++ b/src/client/testing/testController/common/testCoverageHandler.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestRun, Uri, TestCoverageCount, FileCoverage, FileCoverageDetail, StatementCoverage, Range } from 'vscode'; +import { CoveragePayload, FileCoverageMetrics } from './types'; + +/** + * Stateless handler for processing coverage payloads and creating coverage objects. + * This handler is shared across all workspaces and contains no instance state. + */ +export class TestCoverageHandler { + /** + * Process coverage payload + * Pure function - returns coverage data without storing it + */ + public processCoverage(payload: CoveragePayload, runInstance: TestRun): Map { + const detailedCoverageMap = new Map(); + + if (payload.result === undefined) { + return detailedCoverageMap; + } + + for (const [key, value] of Object.entries(payload.result)) { + const fileNameStr = key; + const fileCoverageMetrics: FileCoverageMetrics = value; + + // Create FileCoverage object and add to run instance + const fileCoverage = this.createFileCoverage(Uri.file(fileNameStr), fileCoverageMetrics); + runInstance.addCoverage(fileCoverage); + + // Create detailed coverage array for this file + const detailedCoverage = this.createDetailedCoverage( + fileCoverageMetrics.lines_covered ?? [], + fileCoverageMetrics.lines_missed ?? [], + ); + detailedCoverageMap.set(Uri.file(fileNameStr).fsPath, detailedCoverage); + } + + return detailedCoverageMap; + } + + /** + * Create FileCoverage object from metrics + */ + private createFileCoverage(uri: Uri, metrics: FileCoverageMetrics): FileCoverage { + const linesCovered = metrics.lines_covered ?? []; + const linesMissed = metrics.lines_missed ?? []; + const executedBranches = metrics.executed_branches; + const totalBranches = metrics.total_branches; + + const lineCoverageCount = new TestCoverageCount(linesCovered.length, linesCovered.length + linesMissed.length); + + if (totalBranches === -1) { + // branch coverage was not enabled and should not be displayed + return new FileCoverage(uri, lineCoverageCount); + } else { + const branchCoverageCount = new TestCoverageCount(executedBranches, totalBranches); + return new FileCoverage(uri, lineCoverageCount, branchCoverageCount); + } + } + + /** + * Create detailed coverage array for a file + * Only line coverage on detailed, not branch coverage + */ + private createDetailedCoverage(linesCovered: number[], linesMissed: number[]): FileCoverageDetail[] { + const detailedCoverageArray: FileCoverageDetail[] = []; + + // Add covered lines + for (const line of linesCovered) { + // line is 1-indexed, so we need to subtract 1 to get the 0-indexed line number + // true value means line is covered + const statementCoverage = new StatementCoverage( + true, + new Range(line - 1, 0, line - 1, Number.MAX_SAFE_INTEGER), + ); + detailedCoverageArray.push(statementCoverage); + } + + // Add missed lines + for (const line of linesMissed) { + // line is 1-indexed, so we need to subtract 1 to get the 0-indexed line number + // false value means line is NOT covered + const statementCoverage = new StatementCoverage( + false, + new Range(line - 1, 0, line - 1, Number.MAX_SAFE_INTEGER), + ); + detailedCoverageArray.push(statementCoverage); + } + + return detailedCoverageArray; + } +} diff --git a/src/client/testing/testController/common/testDiscoveryHandler.ts b/src/client/testing/testController/common/testDiscoveryHandler.ts new file mode 100644 index 000000000000..50f4fa71406a --- /dev/null +++ b/src/client/testing/testController/common/testDiscoveryHandler.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, TestController, Uri, MarkdownString } from 'vscode'; +import * as util from 'util'; +import { DiscoveredTestPayload } from './types'; +import { TestProvider } from '../../types'; +import { traceError } from '../../../logging'; +import { Testing } from '../../../common/utils/localize'; +import { createErrorTestItem } from './testItemUtilities'; +import { buildErrorNodeOptions, populateTestTree } from './utils'; +import { TestItemIndex } from './testItemIndex'; + +/** + * Stateless handler for processing discovery payloads and building/updating the TestItem tree. + * This handler is shared across all workspaces and contains no instance state. + */ +export class TestDiscoveryHandler { + /** + * Process discovery payload and update test tree + * Pure function - no instance state used + */ + public processDiscovery( + payload: DiscoveredTestPayload, + testController: TestController, + testItemIndex: TestItemIndex, + workspaceUri: Uri, + testProvider: TestProvider, + token?: CancellationToken, + ): void { + if (!payload) { + // No test data is available + return; + } + + const workspacePath = workspaceUri.fsPath; + const rawTestData = payload as DiscoveredTestPayload; + + // Check if there were any errors in the discovery process. + if (rawTestData.status === 'error') { + this.createErrorNode(testController, workspaceUri, rawTestData.error, testProvider); + } else { + // remove error node only if no errors exist. + testController.items.delete(`DiscoveryError:${workspacePath}`); + } + + if (rawTestData.tests || rawTestData.tests === null) { + // if any tests exist, they should be populated in the test tree, regardless of whether there were errors or not. + // parse and insert test data. + + // Clear existing mappings before rebuilding test tree + testItemIndex.clear(); + + // If the test root for this folder exists: Workspace refresh, update its children. + // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. + // Note: populateTestTree will call testItemIndex.registerTestItem() for each discovered test + populateTestTree( + testController, + rawTestData.tests, + undefined, + { + runIdToTestItem: testItemIndex.runIdToTestItemMap, + runIdToVSid: testItemIndex.runIdToVSidMap, + vsIdToRunId: testItemIndex.vsIdToRunIdMap, + } as any, + token, + ); + } + } + + /** + * Create an error node for discovery failures + */ + public createErrorNode( + testController: TestController, + workspaceUri: Uri, + error: string[] | undefined, + testProvider: TestProvider, + ): void { + const workspacePath = workspaceUri.fsPath; + const testingErrorConst = + testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; + + traceError(testingErrorConst, 'for workspace: ', workspacePath, '\r\n', error?.join('\r\n\r\n') ?? ''); + + let errorNode = testController.items.get(`DiscoveryError:${workspacePath}`); + const message = util.format( + `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, + error?.join('\r\n\r\n') ?? '', + ); + + if (errorNode === undefined) { + const options = buildErrorNodeOptions(workspaceUri, message, testProvider); + errorNode = createErrorTestItem(testController, options); + testController.items.add(errorNode); + } + + const errorNodeLabel: MarkdownString = new MarkdownString( + `[Show output](command:python.viewOutput) to view error logs`, + ); + errorNodeLabel.isTrusted = true; + errorNode.error = errorNodeLabel; + } +} diff --git a/src/client/testing/testController/common/testExecutionHandler.ts b/src/client/testing/testController/common/testExecutionHandler.ts new file mode 100644 index 000000000000..127e6980ae46 --- /dev/null +++ b/src/client/testing/testController/common/testExecutionHandler.ts @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestRun, TestMessage, Location } from 'vscode'; +import { ExecutionTestPayload } from './types'; +import { TestItemIndex } from './testItemIndex'; +import { splitLines } from '../../../common/stringUtils'; +import { splitTestNameWithRegex } from './utils'; +import { clearAllChildren } from './testItemUtilities'; + +/** + * Stateless handler for processing execution payloads and updating TestRun instances. + * This handler is shared across all workspaces and contains no instance state. + */ +export class TestExecutionHandler { + /** + * Process execution payload and update test run + * Pure function - no instance state used + */ + public processExecution( + payload: ExecutionTestPayload, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const rawTestExecData = payload as ExecutionTestPayload; + + if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { + for (const keyTemp of Object.keys(rawTestExecData.result)) { + const testItem = rawTestExecData.result[keyTemp]; + + // Delegate to specific outcome handlers + this.handleTestOutcome(keyTemp, testItem, runInstance, testItemIndex, testController); + } + } + } + + /** + * Handle a single test result based on outcome + */ + private handleTestOutcome( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + if (testItem.outcome === 'error') { + this.handleTestError(runId, testItem, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'failure' || testItem.outcome === 'passed-unexpected') { + this.handleTestFailure(runId, testItem, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'success' || testItem.outcome === 'expected-failure') { + this.handleTestSuccess(runId, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'skipped') { + this.handleTestSkipped(runId, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'subtest-failure') { + this.handleSubtestFailure(runId, testItem, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'subtest-success') { + this.handleSubtestSuccess(runId, runInstance, testItemIndex, testController); + } + } + + /** + * Handle test items that errored during execution + */ + private handleTestError( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const rawTraceback = testItem.traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + const text = `${testItem.test} failed with error: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + const message = new TestMessage(text); + + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem?.uri) { + if (foundItem.range) { + message.location = new Location(foundItem.uri, foundItem.range); + } + runInstance.errored(foundItem, message); + } + } + + /** + * Handle test items that failed during execution + */ + private handleTestFailure( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const rawTraceback = testItem.traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + + const text = `${testItem.test} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + const message = new TestMessage(text); + + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem?.uri) { + if (foundItem.range) { + message.location = new Location(foundItem.uri, foundItem.range); + } + runInstance.failed(foundItem, message); + } + } + + /** + * Handle test items that passed during execution + */ + private handleTestSuccess( + runId: string, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem !== undefined && foundItem.uri) { + runInstance.passed(foundItem); + } + } + + /** + * Handle test items that were skipped during execution + */ + private handleTestSkipped( + runId: string, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem !== undefined && foundItem.uri) { + runInstance.skipped(foundItem); + } + } + + /** + * Handle subtest failures + */ + private handleSubtestFailure( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(runId); + const parentTestItem = testItemIndex.getTestItem(parentTestCaseId, testController); + + if (parentTestItem) { + const stats = testItemIndex.getSubtestStats(parentTestCaseId); + if (stats) { + stats.failed += 1; + } else { + testItemIndex.setSubtestStats(parentTestCaseId, { + failed: 1, + passed: 0, + }); + clearAllChildren(parentTestItem); + } + + const subTestItem = testController?.createTestItem(subtestId, subtestId, parentTestItem.uri); + + if (subTestItem) { + const traceback = testItem.traceback ?? ''; + const text = `${testItem.subtest} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + const message = new TestMessage(text); + if (parentTestItem.uri && parentTestItem.range) { + message.location = new Location(parentTestItem.uri, parentTestItem.range); + } + runInstance.failed(subTestItem, message); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } + + /** + * Handle subtest successes + */ + private handleSubtestSuccess( + runId: string, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(runId); + const parentTestItem = testItemIndex.getTestItem(parentTestCaseId, testController); + + if (parentTestItem) { + const stats = testItemIndex.getSubtestStats(parentTestCaseId); + if (stats) { + stats.passed += 1; + } else { + testItemIndex.setSubtestStats(parentTestCaseId, { failed: 0, passed: 1 }); + clearAllChildren(parentTestItem); + } + + const subTestItem = testController?.createTestItem(subtestId, subtestId, parentTestItem.uri); + + if (subTestItem) { + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + runInstance.passed(subTestItem); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } +} diff --git a/src/client/testing/testController/common/testItemIndex.ts b/src/client/testing/testController/common/testItemIndex.ts new file mode 100644 index 000000000000..448903eae7d5 --- /dev/null +++ b/src/client/testing/testController/common/testItemIndex.ts @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem } from 'vscode'; +import { traceError, traceVerbose } from '../../../logging'; +import { getTestCaseNodes } from './testItemUtilities'; + +export interface SubtestStats { + passed: number; + failed: number; +} + +/** + * Maintains persistent ID mappings between Python test IDs and VS Code TestItems. + * This is a stateful component that bridges discovery and execution phases. + * + * Lifecycle: + * - Created: When PythonResultResolver is instantiated (during workspace activation) + * - Populated: During discovery - each discovered test registers its mappings + * - Queried: During execution - to look up TestItems by Python run ID + * - Cleared: When discovery runs again (fresh start) or workspace is disposed + * - Cleaned: Periodically to remove stale references to deleted tests + */ +export class TestItemIndex { + // THE STATE - these maps persist across discovery and execution + private runIdToTestItem: Map; + private runIdToVSid: Map; + private vsIdToRunId: Map; + private subtestStatsMap: Map; + + constructor() { + this.runIdToTestItem = new Map(); + this.runIdToVSid = new Map(); + this.vsIdToRunId = new Map(); + this.subtestStatsMap = new Map(); + } + + /** + * Register a test item with its Python run ID and VS Code ID + * Called during DISCOVERY to populate the index + */ + public registerTestItem(runId: string, vsId: string, testItem: TestItem): void { + this.runIdToTestItem.set(runId, testItem); + this.runIdToVSid.set(runId, vsId); + this.vsIdToRunId.set(vsId, runId); + } + + /** + * Get TestItem by Python run ID (with validation and fallback strategies) + * Called during EXECUTION to look up tests + * + * Uses a three-tier approach: + * 1. Direct O(1) lookup in runIdToTestItem map + * 2. If stale, try vsId mapping and search by VS Code ID + * 3. Last resort: full tree search + */ + public getTestItem(runId: string, testController: TestController): TestItem | undefined { + // Try direct O(1) lookup first + const directItem = this.runIdToTestItem.get(runId); + if (directItem) { + // Validate the item is still in the test tree + if (this.isTestItemValid(directItem, testController)) { + return directItem; + } else { + // Clean up stale reference + this.runIdToTestItem.delete(runId); + } + } + + // Try vsId mapping as fallback + const vsId = this.runIdToVSid.get(runId); + if (vsId) { + // Search by VS Code ID in the controller + let foundItem: TestItem | undefined; + testController.items.forEach((item) => { + if (item.id === vsId) { + foundItem = item; + return; + } + if (!foundItem) { + item.children.forEach((child) => { + if (child.id === vsId) { + foundItem = child; + } + }); + } + }); + + if (foundItem) { + // Cache for future lookups + this.runIdToTestItem.set(runId, foundItem); + return foundItem; + } else { + // Clean up stale mapping + this.runIdToVSid.delete(runId); + this.vsIdToRunId.delete(vsId); + } + } + + // Last resort: full tree search + traceError(`Falling back to tree search for test: ${runId}`); + const testCases = this.collectAllTestCases(testController); + return testCases.find((item) => item.id === vsId); + } + + /** + * Get Python run ID from VS Code ID + * Called by WorkspaceTestAdapter.executeTests() to convert selected tests to Python IDs + */ + public getRunId(vsId: string): string | undefined { + return this.vsIdToRunId.get(vsId); + } + + /** + * Get VS Code ID from Python run ID + */ + public getVSId(runId: string): string | undefined { + return this.runIdToVSid.get(runId); + } + + /** + * Check if a TestItem reference is still valid in the tree + * + * Time Complexity: O(depth) where depth is the maximum nesting level of the test tree. + * In most cases this is O(1) to O(3) since test trees are typically shallow. + */ + public isTestItemValid(testItem: TestItem, testController: TestController): boolean { + // Simple validation: check if the item's parent chain leads back to the controller + let current: TestItem | undefined = testItem; + while (current?.parent) { + current = current.parent; + } + + // If we reached a root item, check if it's in the controller + if (current) { + return testController.items.get(current.id) === current; + } + + // If no parent chain, check if it's directly in the controller + return testController.items.get(testItem.id) === testItem; + } + + /** + * Get subtest statistics for a parent test case + * Returns undefined if no stats exist yet for this parent + */ + public getSubtestStats(parentId: string): SubtestStats | undefined { + return this.subtestStatsMap.get(parentId); + } + + /** + * Set subtest statistics for a parent test case + */ + public setSubtestStats(parentId: string, stats: SubtestStats): void { + this.subtestStatsMap.set(parentId, stats); + } + + /** + * Remove all mappings + * Called at the start of discovery to ensure clean state + */ + public clear(): void { + this.runIdToTestItem.clear(); + this.runIdToVSid.clear(); + this.vsIdToRunId.clear(); + this.subtestStatsMap.clear(); + } + + /** + * Clean up stale references that no longer exist in the test tree + * Called after test tree modifications + */ + public cleanupStaleReferences(testController: TestController): void { + const staleRunIds: string[] = []; + + // Check all runId->TestItem mappings + this.runIdToTestItem.forEach((testItem, runId) => { + if (!this.isTestItemValid(testItem, testController)) { + staleRunIds.push(runId); + } + }); + + // Remove stale entries + staleRunIds.forEach((runId) => { + const vsId = this.runIdToVSid.get(runId); + this.runIdToTestItem.delete(runId); + this.runIdToVSid.delete(runId); + if (vsId) { + this.vsIdToRunId.delete(vsId); + } + }); + + if (staleRunIds.length > 0) { + traceVerbose(`Cleaned up ${staleRunIds.length} stale test item references`); + } + } + + /** + * Collect all test case items from the test controller tree. + * Note: This performs full tree traversal - use cached lookups when possible. + */ + private collectAllTestCases(testController: TestController): TestItem[] { + const testCases: TestItem[] = []; + + testController.items.forEach((i) => { + const tempArr: TestItem[] = getTestCaseNodes(i); + testCases.push(...tempArr); + }); + + return testCases; + } + + // Expose maps for backward compatibility (read-only access) + public get runIdToTestItemMap(): Map { + return this.runIdToTestItem; + } + + public get runIdToVSidMap(): Map { + return this.runIdToVSid; + } + + public get vsIdToRunIdMap(): Map { + return this.vsIdToRunId; + } +} diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts index 97c04d5dfdf1..478e9dd85744 100644 --- a/src/test/testing/common/testingAdapter.test.ts +++ b/src/test/testing/common/testingAdapter.test.ts @@ -161,11 +161,10 @@ suite('End to End Tests: test adapters', () => { resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); let callCount = 0; // const deferredTillEOT = createTestingDeferred(); - resultResolver._resolveDiscovery = async (payload, _token?) => { + resultResolver.resolveDiscovery = (payload, _token?) => { traceLog(`resolveDiscovery ${payload}`); callCount = callCount + 1; actualData = payload; - return Promise.resolve(); }; // set workspace to test workspace folder and set up settings @@ -202,11 +201,10 @@ suite('End to End Tests: test adapters', () => { }; resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); let callCount = 0; - resultResolver._resolveDiscovery = async (payload, _token?) => { + resultResolver.resolveDiscovery = (payload, _token?) => { traceLog(`resolveDiscovery ${payload}`); callCount = callCount + 1; actualData = payload; - return Promise.resolve(); }; // set settings to work for the given workspace @@ -242,10 +240,9 @@ suite('End to End Tests: test adapters', () => { workspaceUri = Uri.parse(rootPathSmallWorkspace); resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); let callCount = 0; - resultResolver._resolveDiscovery = async (payload, _token?) => { + resultResolver.resolveDiscovery = (payload, _token?) => { callCount = callCount + 1; actualData = payload; - return Promise.resolve(); }; // run pytest discovery const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); @@ -291,11 +288,10 @@ suite('End to End Tests: test adapters', () => { resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); let callCount = 0; - resultResolver._resolveDiscovery = async (payload, _token?) => { + resultResolver.resolveDiscovery = (payload, _token?) => { traceLog(`resolveDiscovery ${payload}`); callCount = callCount + 1; actualData = payload; - return Promise.resolve(); }; // run pytest discovery const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); @@ -375,11 +371,10 @@ suite('End to End Tests: test adapters', () => { resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); let callCount = 0; - resultResolver._resolveDiscovery = async (payload, _token?) => { + resultResolver.resolveDiscovery = (payload, _token?) => { traceLog(`resolveDiscovery ${payload}`); callCount = callCount + 1; actualData = payload; - return Promise.resolve(); }; // run pytest discovery const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); @@ -446,11 +441,10 @@ suite('End to End Tests: test adapters', () => { }; resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); let callCount = 0; - resultResolver._resolveDiscovery = async (payload, _token?) => { + resultResolver.resolveDiscovery = (payload, _token?) => { traceLog(`resolveDiscovery ${payload}`); callCount = callCount + 1; actualData = payload; - return Promise.resolve(); }; // run pytest discovery const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); @@ -480,22 +474,23 @@ suite('End to End Tests: test adapters', () => { let callCount = 0; let failureOccurred = false; let failureMsg = ''; - resultResolver._resolveExecution = async (payload, _token?) => { + resultResolver.resolveExecution = (payload, _token?) => { traceLog(`resolveDiscovery ${payload}`); callCount = callCount + 1; // the payloads that get to the _resolveExecution are all data and should be successful. try { - assert.strictEqual( - payload.status, - 'success', - `Expected status to be 'success', instead status is ${payload.status}`, - ); - assert.ok(payload.result, 'Expected results to be present'); + if ('status' in payload) { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } } catch (err) { failureMsg = err ? (err as Error).toString() : ''; failureOccurred = true; } - return Promise.resolve(); }; // set workspace to test workspace folder @@ -554,22 +549,25 @@ suite('End to End Tests: test adapters', () => { let callCount = 0; let failureOccurred = false; let failureMsg = ''; - resultResolver._resolveExecution = async (payload, _token?) => { + resultResolver.resolveExecution = (payload, _token?) => { traceLog(`resolveDiscovery ${payload}`); callCount = callCount + 1; // the payloads that get to the _resolveExecution are all data and should be successful. try { - const validStatuses = ['subtest-success', 'subtest-failure']; - assert.ok( - validStatuses.includes(payload.status), - `Expected status to be one of ${validStatuses.join(', ')}, but instead status is ${payload.status}`, - ); - assert.ok(payload.result, 'Expected results to be present'); + if ('status' in payload) { + const validStatuses = ['subtest-success', 'subtest-failure']; + assert.ok( + validStatuses.includes(payload.status), + `Expected status to be one of ${validStatuses.join(', ')}, but instead status is ${ + payload.status + }`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } } catch (err) { failureMsg = err ? (err as Error).toString() : ''; failureOccurred = true; } - return Promise.resolve(); }; // set workspace to test workspace folder @@ -625,22 +623,23 @@ suite('End to End Tests: test adapters', () => { let callCount = 0; let failureOccurred = false; let failureMsg = ''; - resultResolver._resolveExecution = async (payload, _token?) => { + resultResolver.resolveExecution = (payload, _token?) => { traceLog(`resolveDiscovery ${payload}`); callCount = callCount + 1; // the payloads that get to the _resolveExecution are all data and should be successful. try { - assert.strictEqual( - payload.status, - 'success', - `Expected status to be 'success', instead status is ${payload.status}`, - ); - assert.ok(payload.result, 'Expected results to be present'); + if ('status' in payload) { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } } catch (err) { failureMsg = err ? (err as Error).toString() : ''; failureOccurred = true; } - return Promise.resolve(); }; // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathSmallWorkspace); @@ -707,7 +706,7 @@ suite('End to End Tests: test adapters', () => { test('Unittest execution with coverage, small workspace', async () => { // result resolver and saved data for assertions resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); - resultResolver._resolveCoverage = async (payload, _token?) => { + resultResolver._resolveCoverage = (payload, _token?) => { assert.strictEqual(payload.cwd, rootPathCoverageWorkspace, 'Expected cwd to be the workspace folder'); assert.ok(payload.result, 'Expected results to be present'); const simpleFileCov = payload.result[`${rootPathCoverageWorkspace}/even.py`]; @@ -717,7 +716,6 @@ suite('End to End Tests: test adapters', () => { assert.strictEqual(simpleFileCov.lines_missed.length, 1, 'Expected 3 lines to be missed in even.py'); assert.strictEqual(simpleFileCov.executed_branches, 1, 'Expected 1 branch to be executed in even.py'); assert.strictEqual(simpleFileCov.total_branches, 2, 'Expected 2 branches in even.py'); - return Promise.resolve(); }; // set workspace to test workspace folder @@ -757,7 +755,7 @@ suite('End to End Tests: test adapters', () => { test('pytest coverage execution, small workspace', async () => { // result resolver and saved data for assertions resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); - resultResolver._resolveCoverage = async (payload, _runInstance?) => { + resultResolver._resolveCoverage = (payload, _runInstance?) => { assert.strictEqual(payload.cwd, rootPathCoverageWorkspace, 'Expected cwd to be the workspace folder'); assert.ok(payload.result, 'Expected results to be present'); const simpleFileCov = payload.result[`${rootPathCoverageWorkspace}/even.py`]; @@ -767,8 +765,6 @@ suite('End to End Tests: test adapters', () => { assert.strictEqual(simpleFileCov.lines_missed.length, 1, 'Expected 3 lines to be missed in even.py'); assert.strictEqual(simpleFileCov.executed_branches, 1, 'Expected 1 branch to be executed in even.py'); assert.strictEqual(simpleFileCov.total_branches, 2, 'Expected 2 branches in even.py'); - - return Promise.resolve(); }; // set workspace to test workspace folder workspaceUri = Uri.parse(rootPathCoverageWorkspace); @@ -811,22 +807,23 @@ suite('End to End Tests: test adapters', () => { let callCount = 0; let failureOccurred = false; let failureMsg = ''; - resultResolver._resolveExecution = async (payload, _token?) => { + resultResolver.resolveExecution = (payload, _token?) => { traceLog(`resolveDiscovery ${payload}`); callCount = callCount + 1; // the payloads that get to the _resolveExecution are all data and should be successful. try { - assert.strictEqual( - payload.status, - 'success', - `Expected status to be 'success', instead status is ${payload.status}`, - ); - assert.ok(payload.result, 'Expected results to be present'); + if ('status' in payload) { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } } catch (err) { failureMsg = err ? (err as Error).toString() : ''; failureOccurred = true; } - return Promise.resolve(); }; // set workspace to test workspace folder @@ -878,7 +875,7 @@ suite('End to End Tests: test adapters', () => { let callCount = 0; let failureOccurred = false; let failureMsg = ''; - resultResolver._resolveDiscovery = async (data, _token?) => { + resultResolver.resolveDiscovery = (data, _token?) => { // do the following asserts for each time resolveExecution is called, should be called once per test. callCount = callCount + 1; traceLog(`unittest discovery adapter seg fault error handling \n ${JSON.stringify(data)}`); @@ -903,7 +900,6 @@ suite('End to End Tests: test adapters', () => { failureMsg = err ? (err as Error).toString() : ''; failureOccurred = true; } - return Promise.resolve(); }; // set workspace to test workspace folder @@ -931,7 +927,7 @@ suite('End to End Tests: test adapters', () => { let callCount = 0; let failureOccurred = false; let failureMsg = ''; - resultResolver._resolveDiscovery = async (data, _token?) => { + resultResolver.resolveDiscovery = (data, _token?) => { // do the following asserts for each time resolveExecution is called, should be called once per test. callCount = callCount + 1; traceLog(`add one to call count, is now ${callCount}`); @@ -961,7 +957,6 @@ suite('End to End Tests: test adapters', () => { failureMsg = err ? (err as Error).toString() : ''; failureOccurred = true; } - return Promise.resolve(); }; // run pytest discovery const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); @@ -984,22 +979,24 @@ suite('End to End Tests: test adapters', () => { let callCount = 0; let failureOccurred = false; let failureMsg = ''; - resultResolver._resolveExecution = async (data, _token?) => { + resultResolver.resolveExecution = (data, _token?) => { // do the following asserts for each time resolveExecution is called, should be called once per test. console.log(`pytest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); callCount = callCount + 1; try { - if (data.status === 'error') { - assert.ok(data.error, "Expected errors in 'error' field"); - } else { - const indexOfTest = JSON.stringify(data.result).search('error'); - assert.notDeepEqual( - indexOfTest, - -1, - 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', - ); + if ('status' in data) { + if (data.status === 'error') { + assert.ok(data.error, "Expected errors in 'error' field"); + } else { + const indexOfTest = JSON.stringify(data.result).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + assert.ok(data.result, 'Expected results to be present'); } - assert.ok(data.result, 'Expected results to be present'); // make sure the testID is found in the results const indexOfTest = JSON.stringify(data).search( 'test_seg_fault.py::TestSegmentationFault::test_segfault', @@ -1009,7 +1006,6 @@ suite('End to End Tests: test adapters', () => { failureMsg = err ? (err as Error).toString() : ''; failureOccurred = true; } - return Promise.resolve(); }; const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; @@ -1038,8 +1034,8 @@ suite('End to End Tests: test adapters', () => { }); }); - test('_resolveExecution performance test: validates efficient test result processing', async () => { - // This test validates that _resolveExecution processes test results efficiently + test('resolveExecution performance test: validates efficient test result processing', async () => { + // This test validates that resolveExecution processes test results efficiently // without expensive tree rebuilding or linear searching operations. // // The test ensures that processing many test results (like parameterized tests) @@ -1085,21 +1081,23 @@ suite('End to End Tests: test adapters', () => { const testItemUtilities = require('../../../client/testing/testController/common/testItemUtilities'); testItemUtilities.getTestCaseNodes = getTestCaseNodesSpy; + // Stub isTestItemValid to always return true for performance test + // This prevents expensive tree searches during validation + const testItemIndexStub = sinon.stub((resultResolver as any).testItemIndex, 'isTestItemValid').returns(true); + // Wrap the _resolveExecution function to measure performance - const original_resolveExecution = resultResolver._resolveExecution.bind(resultResolver); - resultResolver._resolveExecution = async (payload, runInstance) => { + const original_resolveExecution = resultResolver.resolveExecution.bind(resultResolver); + resultResolver.resolveExecution = (payload, runInstance) => { const startTime = performance.now(); callCount++; // Call the actual implementation - await original_resolveExecution(payload, runInstance); + original_resolveExecution(payload, runInstance); const endTime = performance.now(); const callTime = endTime - startTime; callTimes.push(callTime); totalCallTime += callTime; - - return Promise.resolve(); }; // ================================================================ @@ -1160,7 +1158,8 @@ suite('End to End Tests: test adapters', () => { } // Create payload with multiple test results (simulates real test execution) const testResults: Record = {}; for (let i = 0; i < numParameterizedResults; i++) { - testResults[`test_0_${i % 20}`] = { + // Use test IDs that actually exist in our mock setup (test_0_0 through test_0_9) + testResults[`test_0_${i % testFunctionsPerFile}`] = { test: `test_method[${i}]`, outcome: 'success', message: null, @@ -1189,8 +1188,8 @@ suite('End to End Tests: test adapters', () => { const overallStartTime = performance.now(); - // Run the _resolveExecution function with test data - await resultResolver._resolveExecution(payload, mockRunInstance as any); + // Run the resolveExecution function with test data + await resultResolver.resolveExecution(payload, mockRunInstance as any); const overallEndTime = performance.now(); const totalTime = overallEndTime - overallStartTime; @@ -1199,6 +1198,7 @@ suite('End to End Tests: test adapters', () => { // CLEANUP: Restore original functions // ================================================================ testItemUtilities.getTestCaseNodes = originalGetTestCaseNodes; + testItemIndexStub.restore(); // ================================================================ // ASSERT: Verify efficient performance characteristics @@ -1214,7 +1214,7 @@ suite('End to End Tests: test adapters', () => { console.log(`Results processed: ${numParameterizedResults}`); // Basic function call verification - assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(callCount, 1, 'Expected resolveExecution to be called once'); // EFFICIENCY VERIFICATION: Ensure minimal expensive operations assert.strictEqual( diff --git a/src/test/testing/testController/common/testCoverageHandler.unit.test.ts b/src/test/testing/testController/common/testCoverageHandler.unit.test.ts new file mode 100644 index 000000000000..a81aed591128 --- /dev/null +++ b/src/test/testing/testController/common/testCoverageHandler.unit.test.ts @@ -0,0 +1,502 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestRun, Uri, FileCoverage } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import { TestCoverageHandler } from '../../../../client/testing/testController/common/testCoverageHandler'; +import { CoveragePayload } from '../../../../client/testing/testController/common/types'; + +suite('TestCoverageHandler', () => { + let coverageHandler: TestCoverageHandler; + let runInstanceMock: typemoq.IMock; + + setup(() => { + coverageHandler = new TestCoverageHandler(); + runInstanceMock = typemoq.Mock.ofType(); + }); + + suite('processCoverage', () => { + test('should return empty map for undefined result', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: undefined, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.strictEqual(result.size, 0); + runInstanceMock.verify((r) => r.addCoverage(typemoq.It.isAny()), typemoq.Times.never()); + }); + + test('should create FileCoverage for each file', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file1.py': { + lines_covered: [1, 2, 3], + lines_missed: [4, 5], + executed_branches: 5, + total_branches: 10, + }, + '/path/to/file2.py': { + lines_covered: [1, 2], + lines_missed: [3], + executed_branches: 2, + total_branches: 4, + }, + }, + error: '', + }; + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + runInstanceMock.verify((r) => r.addCoverage(typemoq.It.isAny()), typemoq.Times.exactly(2)); + }); + + test('should call runInstance.addCoverage with correct FileCoverage', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [4, 5], + executed_branches: 5, + total_branches: 10, + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + assert.strictEqual(capturedCoverage!.uri.fsPath, Uri.file('/path/to/file.py').fsPath); + }); + + test('should return detailed coverage map with correct keys', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file1.py': { + lines_covered: [1, 2], + lines_missed: [3], + executed_branches: 2, + total_branches: 4, + }, + '/path/to/file2.py': { + lines_covered: [5, 6, 7], + lines_missed: [], + executed_branches: 3, + total_branches: 3, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.strictEqual(result.size, 2); + assert.ok(result.has(Uri.file('/path/to/file1.py').fsPath)); + assert.ok(result.has(Uri.file('/path/to/file2.py').fsPath)); + }); + + test('should handle empty coverage data', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: {}, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.strictEqual(result.size, 0); + }); + + test('should handle file with no covered lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [], + lines_missed: [1, 2, 3], + executed_branches: 0, + total_branches: 5, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); // Only missed lines + }); + + test('should handle file with no missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [], + executed_branches: 5, + total_branches: 5, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); // Only covered lines + }); + + test('should handle undefined lines_covered', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: undefined as any, + lines_missed: [1, 2], + executed_branches: 0, + total_branches: 2, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 2); // Only missed lines + }); + + test('should handle undefined lines_missed', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2], + lines_missed: undefined as any, + executed_branches: 2, + total_branches: 2, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 2); // Only covered lines + }); + }); + + suite('createFileCoverage', () => { + test('should handle line coverage only when totalBranches is -1', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [4, 5], + executed_branches: 0, + total_branches: -1, // Branch coverage disabled + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + // Branch coverage should not be included + assert.strictEqual((capturedCoverage as any).branchCoverage, undefined); + }); + + test('should include branch coverage when available', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [4], + executed_branches: 7, + total_branches: 10, + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + // Should have branch coverage + assert.ok((capturedCoverage as any).branchCoverage); + }); + + test('should calculate line coverage counts correctly', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3, 4, 5], + lines_missed: [6, 7], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + // 5 covered out of 7 total (5 covered + 2 missed) + assert.strictEqual((capturedCoverage as any).statementCoverage.covered, 5); + assert.strictEqual((capturedCoverage as any).statementCoverage.total, 7); + }); + }); + + suite('createDetailedCoverage', () => { + test('should create StatementCoverage for covered lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); + + // All should be covered (true) + detailedCoverage!.forEach((coverage) => { + assert.strictEqual((coverage as any).executed, true); + }); + }); + + test('should create StatementCoverage for missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [], + lines_missed: [1, 2, 3], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); + + // All should be NOT covered (false) + detailedCoverage!.forEach((coverage) => { + assert.strictEqual((coverage as any).executed, false); + }); + }); + + test('should convert 1-indexed to 0-indexed line numbers for covered lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 5, 10], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + + // Line 1 should map to range starting at line 0 + assert.strictEqual((detailedCoverage![0] as any).location.start.line, 0); + // Line 5 should map to range starting at line 4 + assert.strictEqual((detailedCoverage![1] as any).location.start.line, 4); + // Line 10 should map to range starting at line 9 + assert.strictEqual((detailedCoverage![2] as any).location.start.line, 9); + }); + + test('should convert 1-indexed to 0-indexed line numbers for missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [], + lines_missed: [3, 7, 12], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + + // Line 3 should map to range starting at line 2 + assert.strictEqual((detailedCoverage![0] as any).location.start.line, 2); + // Line 7 should map to range starting at line 6 + assert.strictEqual((detailedCoverage![1] as any).location.start.line, 6); + // Line 12 should map to range starting at line 11 + assert.strictEqual((detailedCoverage![2] as any).location.start.line, 11); + }); + + test('should handle large line numbers', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1000, 5000, 10000], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); + + // Verify conversion is correct for large numbers + assert.strictEqual((detailedCoverage![0] as any).location.start.line, 999); + assert.strictEqual((detailedCoverage![1] as any).location.start.line, 4999); + assert.strictEqual((detailedCoverage![2] as any).location.start.line, 9999); + }); + + test('should create detailed coverage with both covered and missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 3, 5], + lines_missed: [2, 4, 6], + executed_branches: 3, + total_branches: 6, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 6); // 3 covered + 3 missed + + // Count covered vs not covered + const covered = detailedCoverage!.filter((c) => (c as any).executed === true); + const notCovered = detailedCoverage!.filter((c) => (c as any).executed === false); + + assert.strictEqual(covered.length, 3); + assert.strictEqual(notCovered.length, 3); + }); + + test('should set range to cover entire line', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + + const coverage = detailedCoverage![0] as any; + // Start at column 0 + assert.strictEqual(coverage.location.start.character, 0); + // End at max safe integer (entire line) + assert.strictEqual(coverage.location.end.character, Number.MAX_SAFE_INTEGER); + }); + }); +}); diff --git a/src/test/testing/testController/common/testDiscoveryHandler.unit.test.ts b/src/test/testing/testController/common/testDiscoveryHandler.unit.test.ts new file mode 100644 index 000000000000..458e3d984405 --- /dev/null +++ b/src/test/testing/testController/common/testDiscoveryHandler.unit.test.ts @@ -0,0 +1,517 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem, Uri, CancellationToken, TestItemCollection } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { TestDiscoveryHandler } from '../../../../client/testing/testController/common/testDiscoveryHandler'; +import { TestItemIndex } from '../../../../client/testing/testController/common/testItemIndex'; +import { DiscoveredTestPayload, DiscoveredTestNode } from '../../../../client/testing/testController/common/types'; +import { TestProvider } from '../../../../client/testing/types'; +import * as utils from '../../../../client/testing/testController/common/utils'; +import * as testItemUtilities from '../../../../client/testing/testController/common/testItemUtilities'; + +suite('TestDiscoveryHandler', () => { + let discoveryHandler: TestDiscoveryHandler; + let testControllerMock: typemoq.IMock; + let testItemIndexMock: typemoq.IMock; + let testItemCollectionMock: typemoq.IMock; + let workspaceUri: Uri; + let testProvider: TestProvider; + let cancelationToken: CancellationToken; + + setup(() => { + discoveryHandler = new TestDiscoveryHandler(); + testControllerMock = typemoq.Mock.ofType(); + testItemIndexMock = typemoq.Mock.ofType(); + testItemCollectionMock = typemoq.Mock.ofType(); + + // Setup default test controller items mock + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + testItemCollectionMock.setup((x) => x.delete(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + + workspaceUri = Uri.file('/foo/bar'); + testProvider = 'pytest'; + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + }); + + teardown(() => { + sinon.restore(); + }); + + suite('processDiscovery', () => { + test('should handle null payload gracefully', () => { + discoveryHandler.processDiscovery( + null as any, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Should not throw and should not call populateTestTree + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.never()); + }); + + test('should call populateTestTree with correct params on success', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + const populateTestTreeStub = sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + + // Setup map getters for populateTestTree + const mockRunIdMap = new Map(); + const mockVSidMap = new Map(); + const mockVStoRunMap = new Map(); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => mockRunIdMap); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => mockVSidMap); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => mockVStoRunMap); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.once()); + assert.ok(populateTestTreeStub.calledOnce); + sinon.assert.calledWith( + populateTestTreeStub, + testControllerMock.object, + tests, + undefined, + sinon.match.any, + cancelationToken, + ); + }); + + test('should clear index before populating', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + sinon.stub(utils, 'populateTestTree'); + + const clearSpy = sinon.spy(); + testItemIndexMock.setup((x) => x.clear()).callback(clearSpy); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + assert.ok(clearSpy.calledOnce); + }); + + test('should handle error status and create error node', () => { + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: ['Error message 1', 'Error message 2'], + }; + + const createErrorNodeSpy = sinon.spy(discoveryHandler, 'createErrorNode'); + + // Mock createTestItem to return a proper TestItem + const mockErrorItem = ({ + id: 'error_id', + error: null, + canResolveChildren: false, + tags: [], + } as unknown) as TestItem; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockErrorItem); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + assert.ok(createErrorNodeSpy.calledOnce); + assert.ok( + createErrorNodeSpy.calledWith(testControllerMock.object, workspaceUri, payload.error, testProvider), + ); + }); + + test('should handle both errors and tests in same payload', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: ['Partial error'], + tests, + }; + + sinon.stub(utils, 'populateTestTree'); + const createErrorNodeSpy = sinon.spy(discoveryHandler, 'createErrorNode'); + + // Mock createTestItem to return a proper TestItem + const mockErrorItem = ({ + id: 'error_id', + error: null, + canResolveChildren: false, + tags: [], + } as unknown) as TestItem; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockErrorItem); + + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Should create error node AND populate test tree + assert.ok(createErrorNodeSpy.calledOnce); + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.once()); + }); + + test('should delete error node on successful discovery', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + const deleteSpy = sinon.spy(); + // Reset and reconfigure the collection mock to capture delete call + testItemCollectionMock.reset(); + testItemCollectionMock + .setup((x) => x.delete(typemoq.It.isAny())) + .callback(deleteSpy) + .returns(() => undefined); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.reset(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + assert.ok(deleteSpy.calledOnce); + assert.ok(deleteSpy.calledWith(`DiscoveryError:${workspaceUri.fsPath}`)); + }); + + test('should respect cancellation token', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + const populateTestTreeStub = sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Verify token was passed to populateTestTree + assert.ok(populateTestTreeStub.calledOnce); + const lastArg = populateTestTreeStub.getCall(0).args[4]; + assert.strictEqual(lastArg, cancelationToken); + }); + + test('should handle null tests in payload', () => { + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests: null as any, + }; + + const populateTestTreeStub = sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Should still call populateTestTree with null + assert.ok(populateTestTreeStub.calledOnce); + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.once()); + }); + }); + + suite('createErrorNode', () => { + test('should create error with correct message for pytest', () => { + const error = ['Error line 1', 'Error line 2']; + testProvider = 'pytest'; + + const buildErrorNodeOptionsStub = sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + assert.ok(buildErrorNodeOptionsStub.calledOnce); + assert.ok(createErrorTestItemStub.calledOnce); + assert.ok(mockErrorItem.error !== null); + }); + + test('should create error with correct message for unittest', () => { + const error = ['Unittest error']; + testProvider = 'unittest'; + + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + assert.ok(mockErrorItem.error !== null); + }); + + test('should set markdown error label correctly', () => { + const error = ['Test error']; + + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + assert.ok(mockErrorItem.error); + assert.strictEqual( + (mockErrorItem.error as any).value, + '[Show output](command:python.viewOutput) to view error logs', + ); + assert.strictEqual((mockErrorItem.error as any).isTrusted, true); + }); + + test('should handle undefined error array', () => { + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, undefined, testProvider); + + // Should not throw + assert.ok(mockErrorItem.error !== null); + }); + + test('should reuse existing error node if present', () => { + const error = ['Error']; + + // Create a proper object with settable error property + const existingErrorItem: any = { + id: `DiscoveryError:${workspaceUri.fsPath}`, + error: null, + canResolveChildren: false, + tags: [], + }; + + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: `DiscoveryError:${workspaceUri.fsPath}`, + label: 'Error Label', + error: 'Error Message', + }); + + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem'); + + // Reset and setup collection to return existing item + testItemCollectionMock.reset(); + testItemCollectionMock + .setup((x) => x.get(`DiscoveryError:${workspaceUri.fsPath}`)) + .returns(() => existingErrorItem); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.reset(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + // Should not create a new error item + assert.ok(createErrorTestItemStub.notCalled); + // Should still update the error property + assert.ok(existingErrorItem.error !== null); + }); + + test('should handle multiple error messages', () => { + const error = ['Error 1', 'Error 2', 'Error 3']; + + const buildStub = sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + // Verify the error messages are joined + const expectedMessage = sinon.match((value: string) => { + return value.includes('Error 1') && value.includes('Error 2') && value.includes('Error 3'); + }); + sinon.assert.calledWith(buildStub, workspaceUri, expectedMessage, testProvider); + }); + }); +}); diff --git a/src/test/testing/testController/common/testExecutionHandler.unit.test.ts b/src/test/testing/testController/common/testExecutionHandler.unit.test.ts new file mode 100644 index 000000000000..c6be4548c192 --- /dev/null +++ b/src/test/testing/testController/common/testExecutionHandler.unit.test.ts @@ -0,0 +1,922 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem, TestRun, TestMessage, Uri, Range, TestItemCollection, MarkdownString } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { TestExecutionHandler } from '../../../../client/testing/testController/common/testExecutionHandler'; +import { TestItemIndex } from '../../../../client/testing/testController/common/testItemIndex'; +import { ExecutionTestPayload } from '../../../../client/testing/testController/common/types'; + +suite('TestExecutionHandler', () => { + let executionHandler: TestExecutionHandler; + let testControllerMock: typemoq.IMock; + let testItemIndexMock: typemoq.IMock; + let runInstanceMock: typemoq.IMock; + let mockTestItem: TestItem; + let mockParentItem: TestItem; + + setup(() => { + executionHandler = new TestExecutionHandler(); + testControllerMock = typemoq.Mock.ofType(); + testItemIndexMock = typemoq.Mock.ofType(); + runInstanceMock = typemoq.Mock.ofType(); + + mockTestItem = createMockTestItem('test1', 'Test 1'); + mockParentItem = createMockTestItem('parentTest', 'Parent Test'); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('processExecution', () => { + test('should process empty payload without errors', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: {}, + error: '', + }; + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // No errors should be thrown + }); + + test('should process undefined result without errors', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + error: '', + }; + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // No errors should be thrown + }); + + test('should process multiple test results', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { test: 'test1', outcome: 'success', message: '', traceback: '' }, + test2: { test: 'test2', outcome: 'failure', message: 'Failed', traceback: 'traceback' }, + }, + error: '', + }; + + const mockTestItem2 = createMockTestItem('test2', 'Test 2'); + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + testItemIndexMock + .setup((x) => x.getTestItem('test2', testControllerMock.object)) + .returns(() => mockTestItem2); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockTestItem), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockTestItem2, typemoq.It.isAny()), typemoq.Times.once()); + }); + }); + + suite('handleTestError', () => { + test('should create error message with traceback', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'error', + message: 'Error occurred', + traceback: 'line1\nline2\nline3', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + let capturedMessage: TestMessage | undefined; + runInstanceMock + .setup((r) => r.errored(mockTestItem, typemoq.It.isAny())) + .callback((_, message: TestMessage) => { + capturedMessage = message; + }); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + assert.ok(capturedMessage); + const messageText = + capturedMessage!.message instanceof MarkdownString + ? capturedMessage!.message.value + : capturedMessage!.message; + assert.ok(messageText.includes('Error occurred')); + assert.ok(messageText.includes('line1')); + assert.ok(messageText.includes('line2')); + runInstanceMock.verify((r) => r.errored(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should set location when test item has range', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'error', + message: 'Error', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + let capturedMessage: TestMessage | undefined; + runInstanceMock + .setup((r) => r.errored(mockTestItem, typemoq.It.isAny())) + .callback((_, message: TestMessage) => { + capturedMessage = message; + }); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + assert.ok(capturedMessage); + assert.ok(capturedMessage!.location); + assert.strictEqual(capturedMessage!.location!.uri.fsPath, mockTestItem.uri!.fsPath); + }); + + test('should handle missing traceback', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'error', + message: 'Error', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.errored(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + }); + + suite('handleTestFailure', () => { + test('should create failure message with traceback', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'failure', + message: 'Assertion failed', + traceback: 'AssertionError\nline1', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + let capturedMessage: TestMessage | undefined; + runInstanceMock + .setup((r) => r.failed(mockTestItem, typemoq.It.isAny())) + .callback((_, message: TestMessage) => { + capturedMessage = message; + }); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + assert.ok(capturedMessage); + const messageText = + capturedMessage!.message instanceof MarkdownString + ? capturedMessage!.message.value + : capturedMessage!.message; + assert.ok(messageText.includes('Assertion failed')); + assert.ok(messageText.includes('AssertionError')); + runInstanceMock.verify((r) => r.failed(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should handle passed-unexpected outcome', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'passed-unexpected', + message: 'Unexpected pass', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.failed(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + }); + + suite('handleTestSuccess', () => { + test('should mark test as passed', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'success', + message: '', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockTestItem), typemoq.Times.once()); + }); + + test('should handle expected-failure outcome', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'expected-failure', + message: '', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockTestItem), typemoq.Times.once()); + }); + + test('should not call passed when test item not found', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'success', + message: '', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock.setup((x) => x.getTestItem('test1', testControllerMock.object)).returns(() => undefined); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.never()); + }); + }); + + suite('handleTestSkipped', () => { + test('should mark test as skipped', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'skipped', + message: 'Test skipped', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.skipped(mockTestItem), typemoq.Times.once()); + }); + }); + + suite('handleSubtestFailure', () => { + test('should create child test item for subtest', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Subtest failed', + traceback: 'traceback', + subtest: 'subtest1', + }, + }, + error: '', + }; + + const mockSubtestItem = createMockTestItem('subtest1', 'Subtest 1'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify stats were set correctly + testItemIndexMock.verify( + (x) => + x.setSubtestStats( + 'parentTest', + typemoq.It.is((stats) => stats.failed === 1 && stats.passed === 0), + ), + typemoq.Times.once(), + ); + + runInstanceMock.verify((r) => r.started(mockSubtestItem), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockSubtestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should update stats correctly for multiple subtests', () => { + const payload1: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + const payload2: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest2)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest2', + }, + }, + error: '', + }; + + const mockSubtest1 = createMockTestItem('subtest1', 'Subtest 1'); + const mockSubtest2 = createMockTestItem('subtest2', 'Subtest 2'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + + // First subtest: no existing stats + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + + // Return different items based on call order + let callCount = 0; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => { + callCount++; + return callCount === 1 ? mockSubtest1 : mockSubtest2; + }); + + executionHandler.processExecution( + payload1, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Second subtest: should have existing stats from first + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => ({ failed: 1, passed: 0 })); + + executionHandler.processExecution( + payload2, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify the first subtest set initial stats + runInstanceMock.verify((r) => r.started(mockSubtest1), typemoq.Times.once()); + runInstanceMock.verify((r) => r.started(mockSubtest2), typemoq.Times.once()); + }); + + test('should throw error when parent test item not found', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => undefined); + + assert.throws(() => { + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + }, /Parent test item not found/); + }); + }); + + suite('handleSubtestSuccess', () => { + test('should create passing subtest', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + const mockSubtestItem = createMockTestItem('subtest1', 'Subtest 1'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify stats were set correctly + testItemIndexMock.verify( + (x) => + x.setSubtestStats( + 'parentTest', + typemoq.It.is((stats) => stats.passed === 1 && stats.failed === 0), + ), + typemoq.Times.once(), + ); + + runInstanceMock.verify((r) => r.started(mockSubtestItem), typemoq.Times.once()); + runInstanceMock.verify((r) => r.passed(mockSubtestItem), typemoq.Times.once()); + }); + + test('should handle subtest with special characters in name', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest [subtest with spaces and [brackets]]': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest with spaces and [brackets]', + }, + }, + error: '', + }; + + const mockSubtestItem = createMockTestItem('[subtest with spaces and [brackets]]', 'Subtest'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockSubtestItem), typemoq.Times.once()); + }); + }); + + suite('Comprehensive Subtest Scenarios', () => { + test('should handle mixed passing and failing subtests in sequence', () => { + // Simulates unittest with subtests like: test_even with i=0,1,2,3,4,5 + const mockSubtest0 = createMockTestItem('(i=0)', '(i=0)'); + const mockSubtest1 = createMockTestItem('(i=1)', '(i=1)'); + const mockSubtest2 = createMockTestItem('(i=2)', '(i=2)'); + const mockSubtest3 = createMockTestItem('(i=3)', '(i=3)'); + const mockSubtest4 = createMockTestItem('(i=4)', '(i=4)'); + const mockSubtest5 = createMockTestItem('(i=5)', '(i=5)'); + + const subtestItems = [mockSubtest0, mockSubtest1, mockSubtest2, mockSubtest3, mockSubtest4, mockSubtest5]; + + testItemIndexMock + .setup((x) => x.getTestItem('test_even', testControllerMock.object)) + .returns(() => mockParentItem); + + let subtestCallCount = 0; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => subtestItems[subtestCallCount++]); + + // First subtest (i=0) - passes + testItemIndexMock.setup((x) => x.getSubtestStats('test_even')).returns(() => undefined); + testItemIndexMock.setup((x) => x.setSubtestStats('test_even', typemoq.It.isAny())).returns(() => undefined); + + const payload0: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'test_even (i=0)': { + test: 'test_even', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: '(i=0)', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload0, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify first subtest created stats + testItemIndexMock.verify( + (x) => + x.setSubtestStats( + 'test_even', + typemoq.It.is((stats) => stats.passed === 1 && stats.failed === 0), + ), + typemoq.Times.once(), + ); + + // Second subtest (i=1) - fails + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('test_even', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('test_even')).returns(() => ({ passed: 1, failed: 0 })); + + const payload1: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'test_even (i=1)': { + test: 'test_even', + outcome: 'subtest-failure', + message: '1 is not even', + traceback: 'AssertionError', + subtest: '(i=1)', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload1, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Third subtest (i=2) - passes + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('test_even', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('test_even')).returns(() => ({ passed: 1, failed: 1 })); + + const payload2: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'test_even (i=2)': { + test: 'test_even', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: '(i=2)', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload2, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify all subtests were started and had outcomes + runInstanceMock.verify((r) => r.started(mockSubtest0), typemoq.Times.once()); + runInstanceMock.verify((r) => r.passed(mockSubtest0), typemoq.Times.once()); + runInstanceMock.verify((r) => r.started(mockSubtest1), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockSubtest1, typemoq.It.isAny()), typemoq.Times.once()); + runInstanceMock.verify((r) => r.started(mockSubtest2), typemoq.Times.once()); + runInstanceMock.verify((r) => r.passed(mockSubtest2), typemoq.Times.once()); + }); + + test('should persist stats across multiple processExecution calls', () => { + // Test that stats persist in TestItemIndex across multiple processExecution calls + const mockSubtest1 = createMockTestItem('subtest1', 'Subtest 1'); + const mockSubtest2 = createMockTestItem('subtest2', 'Subtest 2'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + + let callCount = 0; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => (callCount++ === 0 ? mockSubtest1 : mockSubtest2)); + + const payload1: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + // First call - no existing stats + executionHandler.processExecution( + payload1, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Simulate stats being stored in TestItemIndex + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => ({ passed: 1, failed: 0 })); + + const payload2: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest2)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest2', + }, + }, + error: '', + }; + + // Second call - existing stats should be retrieved and updated + executionHandler.processExecution( + payload2, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify getSubtestStats was called to retrieve existing stats + testItemIndexMock.verify((x) => x.getSubtestStats('parentTest'), typemoq.Times.once()); + + // Verify both subtests were processed + runInstanceMock.verify((r) => r.passed(mockSubtest1), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockSubtest2, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should clear children only on first subtest when no existing stats', () => { + // When first subtest arrives, children should be cleared + // Subsequent subtests should NOT clear children + const mockSubtest1 = createMockTestItem('subtest1', 'Subtest 1'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtest1); + + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify setSubtestStats was called (which happens when creating new stats) + testItemIndexMock.verify((x) => x.setSubtestStats('parentTest', typemoq.It.isAny()), typemoq.Times.once()); + }); + }); +}); + +function createMockTestItem(id: string, label: string): TestItem { + const range = new Range(0, 0, 0, 0); + const mockChildren = typemoq.Mock.ofType(); + mockChildren.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + + const mockTestItem = ({ + id, + label, + canResolveChildren: false, + tags: [], + children: mockChildren.object, + range, + uri: Uri.file('/foo/bar/test.py'), + parent: undefined, + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/testController/common/testItemIndex.unit.test.ts b/src/test/testing/testController/common/testItemIndex.unit.test.ts new file mode 100644 index 000000000000..6712d90ff667 --- /dev/null +++ b/src/test/testing/testController/common/testItemIndex.unit.test.ts @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem, Uri, Range, TestItemCollection } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { TestItemIndex } from '../../../../client/testing/testController/common/testItemIndex'; + +suite('TestItemIndex', () => { + let testItemIndex: TestItemIndex; + let testControllerMock: typemoq.IMock; + let mockTestItem1: TestItem; + let mockTestItem2: TestItem; + let mockParentItem: TestItem; + + setup(() => { + testItemIndex = new TestItemIndex(); + testControllerMock = typemoq.Mock.ofType(); + + // Create mock test items + mockTestItem1 = createMockTestItem('test1', 'Test 1'); + mockTestItem2 = createMockTestItem('test2', 'Test 2'); + mockParentItem = createMockTestItem('parent', 'Parent'); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('registerTestItem', () => { + test('should store all three mappings correctly', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), mockTestItem1); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId), vsId); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId), runId); + }); + + test('should overwrite existing mappings', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + testItemIndex.registerTestItem(runId, vsId, mockTestItem2); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), mockTestItem2); + }); + + test('should handle different runId and vsId', () => { + const runId = 'test_file.py::TestClass::test_method'; + const vsId = 'different_id'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId), vsId); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId), runId); + }); + }); + + suite('getTestItem', () => { + test('should return item on direct lookup when valid', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + // Register the item + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + // Mock the validation to return true + const isValidStub = sinon.stub(testItemIndex, 'isTestItemValid').returns(true); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + assert.strictEqual(result, mockTestItem1); + assert.ok(isValidStub.calledOnce); + }); + + test('should remove stale item and try vsId fallback', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + // Mock validation to fail on first call (stale item) + const isValidStub = sinon.stub(testItemIndex, 'isTestItemValid').returns(false); + + // Setup controller to not find the item + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.forEach(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + // Should have removed the stale item + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), undefined); + assert.strictEqual(result, undefined); + assert.ok(isValidStub.calledOnce); + }); + + test('should perform vsId search when direct lookup is stale', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + // Create test item with correct ID + const searchableTestItem = createMockTestItem(vsId, 'Test Example'); + + testItemIndex.registerTestItem(runId, vsId, searchableTestItem); + + // First validation fails (stale), need to search by vsId + sinon.stub(testItemIndex, 'isTestItemValid').returns(false); + + // Setup controller to find item by vsId + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + callback(searchableTestItem); + }) + .returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + // Should recache the found item + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), searchableTestItem); + assert.strictEqual(result, searchableTestItem); + }); + + test('should return undefined if not found anywhere', () => { + const runId = 'nonexistent'; + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.forEach(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + assert.strictEqual(result, undefined); + }); + }); + + suite('getRunId and getVSId', () => { + test('getRunId should convert VS Code ID to Python run ID', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'vscode_id'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.getRunId(vsId), runId); + }); + + test('getRunId should return undefined for unknown vsId', () => { + assert.strictEqual(testItemIndex.getRunId('unknown'), undefined); + }); + + test('getVSId should convert Python run ID to VS Code ID', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'vscode_id'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.getVSId(runId), vsId); + }); + + test('getVSId should return undefined for unknown runId', () => { + assert.strictEqual(testItemIndex.getVSId('unknown'), undefined); + }); + }); + + suite('clear', () => { + test('should remove all mappings', () => { + testItemIndex.registerTestItem('runId1', 'vsId1', mockTestItem1); + testItemIndex.registerTestItem('runId2', 'vsId2', mockTestItem2); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 2); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 2); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 2); + + testItemIndex.clear(); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 0); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 0); + }); + + test('should handle clearing empty index', () => { + testItemIndex.clear(); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 0); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 0); + }); + }); + + suite('isTestItemValid', () => { + test('should return true for item with valid parent chain leading to controller', () => { + const childItem = createMockTestItem('child', 'Child'); + (childItem as any).parent = mockParentItem; + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(mockParentItem.id)).returns(() => mockParentItem); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(childItem, testControllerMock.object); + + assert.strictEqual(result, true); + }); + + test('should return false for orphaned item', () => { + const orphanedItem = createMockTestItem('orphaned', 'Orphaned'); + (orphanedItem as any).parent = mockParentItem; + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(orphanedItem, testControllerMock.object); + + assert.strictEqual(result, false); + }); + + test('should return true for root item in controller', () => { + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(mockTestItem1.id)).returns(() => mockTestItem1); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(mockTestItem1, testControllerMock.object); + + assert.strictEqual(result, true); + }); + + test('should return false for item not in controller and no parent', () => { + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(mockTestItem1, testControllerMock.object); + + assert.strictEqual(result, false); + }); + }); + + suite('cleanupStaleReferences', () => { + test('should remove items not in controller', () => { + const runId1 = 'test1'; + const runId2 = 'test2'; + const vsId1 = 'vs1'; + const vsId2 = 'vs2'; + + testItemIndex.registerTestItem(runId1, vsId1, mockTestItem1); + testItemIndex.registerTestItem(runId2, vsId2, mockTestItem2); + + // Mock validation: first item invalid, second valid + const isValidStub = sinon.stub(testItemIndex, 'isTestItemValid'); + isValidStub.onFirstCall().returns(false); // mockTestItem1 is invalid + isValidStub.onSecondCall().returns(true); // mockTestItem2 is valid + + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + // First item should be removed + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId1), undefined); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId1), undefined); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId1), undefined); + + // Second item should remain + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId2), mockTestItem2); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId2), vsId2); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId2), runId2); + }); + + test('should keep all valid items', () => { + const runId1 = 'test1'; + const vsId1 = 'vs1'; + + testItemIndex.registerTestItem(runId1, vsId1, mockTestItem1); + + sinon.stub(testItemIndex, 'isTestItemValid').returns(true); + + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + // Item should still be there + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId1), mockTestItem1); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId1), vsId1); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId1), runId1); + }); + + test('should handle empty index', () => { + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + }); + + test('should remove all items when all are invalid', () => { + testItemIndex.registerTestItem('test1', 'vs1', mockTestItem1); + testItemIndex.registerTestItem('test2', 'vs2', mockTestItem2); + + sinon.stub(testItemIndex, 'isTestItemValid').returns(false); + + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 0); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 0); + }); + }); + + suite('Backward compatibility getters', () => { + test('runIdToTestItemMap should return the internal map', () => { + const runId = 'test1'; + testItemIndex.registerTestItem(runId, 'vs1', mockTestItem1); + + const map = testItemIndex.runIdToTestItemMap; + + assert.strictEqual(map.get(runId), mockTestItem1); + }); + + test('runIdToVSidMap should return the internal map', () => { + const runId = 'test1'; + const vsId = 'vs1'; + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + const map = testItemIndex.runIdToVSidMap; + + assert.strictEqual(map.get(runId), vsId); + }); + + test('vsIdToRunIdMap should return the internal map', () => { + const runId = 'test1'; + const vsId = 'vs1'; + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + const map = testItemIndex.vsIdToRunIdMap; + + assert.strictEqual(map.get(vsId), runId); + }); + }); +}); + +function createMockTestItem(id: string, label: string): TestItem { + const range = new Range(0, 0, 0, 0); + const mockChildren = typemoq.Mock.ofType(); + mockChildren.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + + const mockTestItem = ({ + id, + label, + canResolveChildren: false, + tags: [], + children: mockChildren.object, + range, + uri: Uri.file('/foo/bar'), + parent: undefined, + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts index 05d2ee1dd0f3..e4b350a20750 100644 --- a/src/test/testing/testController/resultResolver.unit.test.ts +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -93,12 +93,13 @@ suite('Result Resolver tests', () => { // assert the stub functions were called with the correct parameters // header of populateTestTree is (testController: TestController, testTreeData: DiscoveredTestNode, testRoot: TestItem | undefined, resultResolver: ITestResultResolver, token?: CancellationToken) + // After refactor, an inline object with testItemIndex maps is passed instead of resultResolver sinon.assert.calledWithMatch( populateTestTreeStub, testController, // testController tests, // testTreeData undefined, // testRoot - resultResolver, // resultResolver + sinon.match.has('runIdToTestItem'), // inline object with maps cancelationToken, // token ); }); @@ -182,12 +183,13 @@ suite('Result Resolver tests', () => { sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); // also calls populateTestTree with the discovery test results + // After refactor, an inline object with testItemIndex maps is passed instead of resultResolver sinon.assert.calledWithMatch( populateTestTreeStub, testController, // testController tests, // testTreeData undefined, // testRoot - resultResolver, // resultResolver + sinon.match.has('runIdToTestItem'), // inline object with maps cancelationToken, // token ); }); @@ -327,6 +329,34 @@ suite('Result Resolver tests', () => { sinon.stub(testItemUtilities, 'clearAllChildren').callsFake(() => undefined); testProvider = 'unittest'; workspaceUri = Uri.file('/foo/bar'); + + // Create parent test item with correct ID + const mockParentItem = createMockTestItem('parentTest'); + + // Update testControllerMock to include parent item in its collection + const mockTestItems: [string, TestItem][] = [ + ['1', mockTestItem1], + ['2', mockTestItem2], + ['parentTest', mockParentItem], + ]; + const iterableMock = mockTestItems[Symbol.iterator](); + + const testItemCollectionMock = typemoq.Mock.ofType(); + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + let result = iterableMock.next(); + while (!result.done) { + callback(result.value[1]); + result = iterableMock.next(); + } + }) + .returns(() => mockTestItem1); + testItemCollectionMock.setup((x) => x.get('parentTest')).returns(() => mockParentItem); + + testControllerMock.reset(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + resultResolver = new ResultResolver.PythonResultResolver( testControllerMock.object, testProvider, @@ -334,13 +364,16 @@ suite('Result Resolver tests', () => { ); const subtestName = 'parentTest [subTest with spaces and [brackets]]'; const mockSubtestItem = createMockTestItem(subtestName); + // add a mock test item to the map of known VSCode ids to run ids resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); // creates a mock test item with a space which will be used to split the runId resultResolver.runIdToVSid.set(subtestName, subtestName); + // Register parent test in testItemIndex so it can be found by getTestItem + resultResolver.runIdToVSid.set('parentTest', 'parentTest'); // add this mock test to the map of known test items - resultResolver.runIdToTestItem.set('parentTest', mockTestItem2); + resultResolver.runIdToTestItem.set('parentTest', mockParentItem); resultResolver.runIdToTestItem.set(subtestName, mockSubtestItem); let generatedId: string | undefined; @@ -563,15 +596,15 @@ suite('Result Resolver tests', () => { function createMockTestItem(id: string): TestItem { const range = new Range(0, 0, 0, 0); + const mockChildren = typemoq.Mock.ofType(); + mockChildren.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + mockChildren.setup((x) => x.forEach(typemoq.It.isAny())).returns(() => undefined); + const mockTestItem = ({ id, canResolveChildren: false, tags: [], - children: { - add: () => { - // empty - }, - }, + children: mockChildren.object, range, uri: Uri.file('/foo/bar'), } as unknown) as TestItem; diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index 0605b1718166..3e2816afbbde 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -134,3 +134,17 @@ mockedVSCode.LogLevel = vscodeMocks.LogLevel; (mockedVSCode as any).CancellationError = vscodeMocks.vscMockExtHostedTypes.CancellationError; (mockedVSCode as any).LSPCancellationError = vscodeMocks.vscMockExtHostedTypes.LSPCancellationError; mockedVSCode.TestRunProfileKind = vscodeMocks.TestRunProfileKind; +(mockedVSCode as any).TestCoverageCount = class TestCoverageCount { + constructor(public covered: number, public total: number) {} +}; +(mockedVSCode as any).FileCoverage = class FileCoverage { + constructor( + public uri: any, + public statementCoverage: any, + public branchCoverage?: any, + public declarationCoverage?: any, + ) {} +}; +(mockedVSCode as any).StatementCoverage = class StatementCoverage { + constructor(public executed: number | boolean, public location: any, public branches?: any) {} +}; From 38e1cc667f97c47bf2089cc5f74bc763e835702b Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:02:06 -0800 Subject: [PATCH 1071/1136] perf(pytest): cache path resolution to reduce discovery time for large test suites (#25655) fixes: https://github.com/microsoft/vscode-python/issues/25348 Test discovery for large suites (~150k tests) shows 10x slowdown vs native pytest due to redundant path operations in `pytest_sessionfinish` hook. Profiling indicates repeated `pathlib.Path.cwd()` calls, `os.fspath()` conversions, and exception-based dictionary lookups dominate execution time. ## Changes **Caching infrastructure** - Module-level caches: `_path_cache` (node paths by id), `_path_to_str_cache` (string conversions), `_CACHED_CWD` (working directory) - `cached_fsdecode()`: memoized `os.fspath()` wrapper used for dictionary keys throughout tree building **Modified `get_node_path()`** - Object id-based cache lookup before path resolution - Lazy initialization of cached cwd, eliminates 150k+ redundant syscalls - Store result before return **Control flow optimization** - Replace `try/except KeyError` with `dict.get()` in 5 hotpath locations: `process_parameterized_test()`, `build_test_tree()`, `build_nested_folders()` - 3-5x faster for cache-hit case --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../tests/pytestadapter/test_utils.py | 24 ++++- python_files/vscode_pytest/__init__.py | 93 +++++++++++++------ 2 files changed, 87 insertions(+), 30 deletions(-) diff --git a/python_files/tests/pytestadapter/test_utils.py b/python_files/tests/pytestadapter/test_utils.py index ef0ed2daf4e9..70201db7d097 100644 --- a/python_files/tests/pytestadapter/test_utils.py +++ b/python_files/tests/pytestadapter/test_utils.py @@ -12,7 +12,7 @@ script_dir = pathlib.Path(__file__).parent.parent.parent sys.path.append(os.fspath(script_dir)) -from vscode_pytest import has_symlink_parent # noqa: E402 +from vscode_pytest import cached_fsdecode, has_symlink_parent # noqa: E402 def test_has_symlink_parent_with_symlink(): @@ -33,3 +33,25 @@ def test_has_symlink_parent_without_symlink(): folder_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" # Check that has_symlink_parent correctly identifies that there are no symbolic links assert not has_symlink_parent(folder_path) + + +def test_cached_fsdecode(): + """Test that cached_fsdecode correctly caches path-to-string conversions.""" + # Create a test path + test_path = TEST_DATA_PATH / "simple_pytest.py" + + # First call should compute and cache + result1 = cached_fsdecode(test_path) + assert result1 == os.fspath(test_path) + assert isinstance(result1, str) + + # Second call should return cached value (same object) + result2 = cached_fsdecode(test_path) + assert result2 == result1 + assert result2 is result1 # Should be the same object from cache + + # Different path should be cached independently + test_path2 = TEST_DATA_PATH / "parametrize_tests.py" + result3 = cached_fsdecode(test_path2) + assert result3 == os.fspath(test_path2) + assert result3 != result1 diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index ba8b270403ac..79365ab95db2 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -80,6 +80,11 @@ def __init__(self, message): SYMLINK_PATH = None INCLUDE_BRANCHES = False +# Performance optimization caches for path resolution +_path_cache: dict[int, pathlib.Path] = {} # Cache node paths by object id +_path_to_str_cache: dict[pathlib.Path, str] = {} # Cache path-to-string conversions +_CACHED_CWD: pathlib.Path | None = None + def pytest_load_initial_conftests(early_config, parser, args): # noqa: ARG001 has_pytest_cov = early_config.pluginmanager.hasplugin("pytest_cov") @@ -619,11 +624,10 @@ def process_parameterized_test( class_and_method = second_split[1] + "::" # This has "::" separator at both ends # construct the parent id, so it is absolute path :: any class and method :: parent_part - parent_id = os.fspath(get_node_path(test_case)) + class_and_method + parent_part + parent_id = cached_fsdecode(get_node_path(test_case)) + class_and_method + parent_part try: function_name = test_case.originalname # type: ignore - function_test_node = function_nodes_dict[parent_id] except AttributeError: # actual error has occurred ERRORS.append( f"unable to find original name for {test_case.name} with parameterization detected." @@ -631,8 +635,10 @@ def process_parameterized_test( raise VSCodePytestError( "Unable to find original name for parameterized test case" ) from None - except KeyError: - function_test_node: TestNode = create_parameterized_function_node( + + function_test_node = function_nodes_dict.get(parent_id) + if function_test_node is None: + function_test_node = create_parameterized_function_node( function_name, get_node_path(test_case), parent_id ) function_nodes_dict[parent_id] = function_test_node @@ -644,11 +650,11 @@ def process_parameterized_test( if isinstance(test_case.parent, pytest.File): # calculate the parent path of the test case parent_path = get_node_path(test_case.parent) - try: - parent_test_case = file_nodes_dict[os.fspath(parent_path)] - except KeyError: + parent_path_key = cached_fsdecode(parent_path) + parent_test_case = file_nodes_dict.get(parent_path_key) + if parent_test_case is None: parent_test_case = create_file_node(parent_path) - file_nodes_dict[os.fspath(parent_path)] = parent_test_case + file_nodes_dict[parent_path_key] = parent_test_case if function_test_node not in parent_test_case["children"]: parent_test_case["children"].append(function_test_node) @@ -693,9 +699,8 @@ def build_test_tree(session: pytest.Session) -> TestNode: USES_PYTEST_DESCRIBE and isinstance(case_iter, DescribeBlock) ): # While the given node is a class, create a class and nest the previous node as a child. - try: - test_class_node = class_nodes_dict[case_iter.nodeid] - except KeyError: + test_class_node = class_nodes_dict.get(case_iter.nodeid) + if test_class_node is None: test_class_node = create_class_node(case_iter) class_nodes_dict[case_iter.nodeid] = test_class_node # Check if the class already has the child node. This will occur if the test is parameterized. @@ -712,11 +717,11 @@ def build_test_tree(session: pytest.Session) -> TestNode: break parent_path = get_node_path(parent_module) # Create a file node that has the last class as a child. - try: - test_file_node: TestNode = file_nodes_dict[os.fspath(parent_path)] - except KeyError: + parent_path_key = cached_fsdecode(parent_path) + test_file_node = file_nodes_dict.get(parent_path_key) + if test_file_node is None: test_file_node = create_file_node(parent_path) - file_nodes_dict[os.fspath(parent_path)] = test_file_node + file_nodes_dict[parent_path_key] = test_file_node # Check if the class is already a child of the file node. if test_class_node is not None and test_class_node not in test_file_node["children"]: test_file_node["children"].append(test_class_node) @@ -731,11 +736,11 @@ def build_test_tree(session: pytest.Session) -> TestNode: test_case.parent, ) ) - try: - parent_test_case = file_nodes_dict[os.fspath(parent_path)] - except KeyError: + parent_path_key = cached_fsdecode(parent_path) + parent_test_case = file_nodes_dict.get(parent_path_key) + if parent_test_case is None: parent_test_case = create_file_node(parent_path) - file_nodes_dict[os.fspath(parent_path)] = parent_test_case + file_nodes_dict[parent_path_key] = parent_test_case parent_test_case["children"].append(test_node) # Process all files and construct them into nested folders session_children_dict = construct_nested_folders( @@ -776,11 +781,11 @@ def build_nested_folders( max_iter = 100 while iterator_path != session_node_path: curr_folder_name = iterator_path.name - try: - curr_folder_node: TestNode = created_files_folders_dict[os.fspath(iterator_path)] - except KeyError: - curr_folder_node: TestNode = create_folder_node(curr_folder_name, iterator_path) - created_files_folders_dict[os.fspath(iterator_path)] = curr_folder_node + iterator_path_key = cached_fsdecode(iterator_path) + curr_folder_node = created_files_folders_dict.get(iterator_path_key) + if curr_folder_node is None: + curr_folder_node = create_folder_node(curr_folder_name, iterator_path) + created_files_folders_dict[iterator_path_key] = curr_folder_node if prev_folder_node not in curr_folder_node["children"]: curr_folder_node["children"].append(prev_folder_node) iterator_path = iterator_path.parent @@ -942,6 +947,23 @@ class CoveragePayloadDict(Dict): error: str | None # Currently unused need to check +def cached_fsdecode(path: pathlib.Path) -> str: + """Convert path to string with caching for performance. + + This function caches path-to-string conversions to avoid redundant + os.fsdecode() calls during test tree building. + + Parameters: + path: The pathlib.Path object to convert to string. + + Returns: + str: The string representation of the path. + """ + if path not in _path_to_str_cache: + _path_to_str_cache[path] = os.fspath(path) + return _path_to_str_cache[path] + + def get_node_path( node: pytest.Session | pytest.Item @@ -961,6 +983,10 @@ def get_node_path( Returns: pathlib.Path: The resolved path for the node. """ + cache_key = id(node) + if cache_key in _path_cache: + return _path_cache[cache_key] + node_path = getattr(node, "path", None) if node_path is None: fspath = getattr(node, "fspath", None) @@ -982,19 +1008,28 @@ def get_node_path( common_path = os.path.commonpath([symlink_str, node_path_str]) if common_path == os.fsdecode(SYMLINK_PATH): # The node path is already relative to the SYMLINK_PATH root therefore return - return node_path + result = node_path else: # If the node path is not a symlink, then we need to calculate the equivalent symlink path # get the relative path between the cwd and the node path (as the node path is not a symlink). - rel_path = node_path.relative_to(pathlib.Path.cwd()) + # Use cached cwd to avoid repeated system calls + global _CACHED_CWD + if _CACHED_CWD is None: + _CACHED_CWD = pathlib.Path.cwd() + rel_path = node_path.relative_to(_CACHED_CWD) # combine the difference between the cwd and the node path with the symlink path - return pathlib.Path(SYMLINK_PATH, rel_path) + result = pathlib.Path(SYMLINK_PATH, rel_path) except Exception as e: raise VSCodePytestError( f"Error occurred while calculating symlink equivalent from node path: {e}" - f"\n SYMLINK_PATH: {SYMLINK_PATH}, \n node path: {node_path}, \n cwd: {pathlib.Path.cwd()}" + f"\n SYMLINK_PATH: {SYMLINK_PATH}, \n node path: {node_path}, \n cwd: {_CACHED_CWD if _CACHED_CWD else pathlib.Path.cwd()}" ) from e - return node_path + else: + result = node_path + + # Cache before returning + _path_cache[cache_key] = result + return result __writer = None From 215c1684d103ede8a562e13f45300d5e74e7fcd6 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:32:07 -0800 Subject: [PATCH 1072/1136] Optimize pytest duplicate check from O(n) to O(1) using sets (#25658) switched from list to set for better preformance was first done by copilot but ported over related to https://github.com/microsoft/vscode-python/issues/25348 but about execution instead of discovery --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- python_files/vscode_pytest/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 79365ab95db2..1222a324b232 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -75,7 +75,7 @@ def __init__(self, message): ERRORS = [] IS_DISCOVERY = False map_id_to_path = {} -collected_tests_so_far = [] +collected_tests_so_far = set() TEST_RUN_PIPE = os.getenv("TEST_RUN_PIPE") SYMLINK_PATH = None INCLUDE_BRANCHES = False @@ -175,7 +175,7 @@ def pytest_exception_interact(node, call, report): report_value = "failure" node_id = get_absolute_test_id(node.nodeid, get_node_path(node)) if node_id not in collected_tests_so_far: - collected_tests_so_far.append(node_id) + collected_tests_so_far.add(node_id) item_result = create_test_outcome( node_id, report_value, @@ -300,7 +300,7 @@ def pytest_report_teststatus(report, config): # noqa: ARG001 # Calculate the absolute test id and use this as the ID moving forward. absolute_node_id = get_absolute_test_id(report.nodeid, node_path) if absolute_node_id not in collected_tests_so_far: - collected_tests_so_far.append(absolute_node_id) + collected_tests_so_far.add(absolute_node_id) item_result = create_test_outcome( absolute_node_id, report_value, @@ -334,7 +334,7 @@ def pytest_runtest_protocol(item, nextitem): # noqa: ARG001 report_value = "skipped" cwd = pathlib.Path.cwd() if absolute_node_id not in collected_tests_so_far: - collected_tests_so_far.append(absolute_node_id) + collected_tests_so_far.add(absolute_node_id) item_result = create_test_outcome( absolute_node_id, report_value, From 268e3c18e12e4c0f4a3cca94cf422dee59beddc9 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Sun, 14 Dec 2025 20:47:16 -0800 Subject: [PATCH 1073/1136] Better log ensureTerminalLegacy (#25665) Better logs when we use environment extension when available for creating terminal. --- src/client/envExt/api.legacy.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/client/envExt/api.legacy.ts b/src/client/envExt/api.legacy.ts index d1a9f404d541..6f2e60774033 100644 --- a/src/client/envExt/api.legacy.ts +++ b/src/client/envExt/api.legacy.ts @@ -148,5 +148,24 @@ export async function ensureTerminalLegacy( const terminal = await api.createTerminal(pythonEnv, fixedOptions); return terminal; } + traceError('ensureTerminalLegacy - Did not return terminal successfully.'); + traceError( + 'ensureTerminalLegacy - pythonEnv:', + pythonEnv + ? `id=${pythonEnv.envId.id}, managerId=${pythonEnv.envId.managerId}, name=${pythonEnv.name}, version=${pythonEnv.version}, executable=${pythonEnv.execInfo.run.executable}` + : 'undefined', + ); + traceError( + 'ensureTerminalLegacy - project:', + project ? `name=${project.name}, uri=${project.uri.toString()}` : 'undefined', + ); + traceError( + 'ensureTerminalLegacy - options:', + options + ? `name=${options.name}, cwd=${options.cwd?.toString()}, hideFromUser=${options.hideFromUser}` + : 'undefined', + ); + traceError('ensureTerminalLegacy - resource:', resource?.toString() || 'undefined'); + throw new Error('Invalid arguments to create terminal'); } From c7efb6e64cd402cd4ba02ebbe12170a444aee7c9 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 16 Dec 2025 09:55:00 -0800 Subject: [PATCH 1074/1136] Switch to queue based execution for executing Python code (#25669) Resolves: https://github.com/microsoft/vscode-python-environments/issues/958 Challenge is that sendText would get called when terminal is not ready. And doing `undefined.show()` is the problem. Switching to queue based execution for running REPL commands, which would prevent from us losing the first command as well. --- .../common/application/terminalManager.ts | 3 + src/client/common/application/types.ts | 2 + src/client/common/terminal/service.ts | 102 ++++++++++++++++-- src/client/common/vscodeApis/windowApis.ts | 5 + .../common/terminals/service.unit.test.ts | 69 ++++++++++-- src/test/smoke/smartSend.smoke.test.ts | 7 +- 6 files changed, 165 insertions(+), 23 deletions(-) diff --git a/src/client/common/application/terminalManager.ts b/src/client/common/application/terminalManager.ts index 9d0536e85243..dc2603e84a56 100644 --- a/src/client/common/application/terminalManager.ts +++ b/src/client/common/application/terminalManager.ts @@ -38,6 +38,9 @@ export class TerminalManager implements ITerminalManager { public onDidEndTerminalShellExecution(handler: (e: TerminalShellExecutionEndEvent) => void): Disposable { return window.onDidEndTerminalShellExecution(handler); } + public onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable { + return window.onDidChangeTerminalState(handler); + } } /** diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 65a8833a691c..34a95fb604f0 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -939,6 +939,8 @@ export interface ITerminalManager { onDidChangeTerminalShellIntegration(handler: (e: TerminalShellIntegrationChangeEvent) => void): Disposable; onDidEndTerminalShellExecution(handler: (e: TerminalShellExecutionEndEvent) => void): Disposable; + + onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable; } export const IDebugService = Symbol('IDebugManager'); diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index 54c1fd1f795e..0dffd5615ae1 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -9,7 +9,7 @@ import { IServiceContainer } from '../../ioc/types'; import { captureTelemetry } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { ITerminalAutoActivation } from '../../terminals/types'; -import { ITerminalManager } from '../application/types'; +import { IApplicationShell, ITerminalManager } from '../application/types'; import { _SCRIPTS_DIR } from '../process/internal/scripts/constants'; import { IConfigurationService, IDisposableRegistry } from '../types'; import { @@ -20,9 +20,9 @@ import { TerminalShellType, } from './types'; import { traceVerbose } from '../../logging'; +import { sleep } from '../utils/async'; import { useEnvExtension } from '../../envExt/api.internal'; import { ensureTerminalLegacy } from '../../envExt/api.legacy'; -import { sleep } from '../utils/async'; @injectable() export class TerminalService implements ITerminalService, Disposable { @@ -33,8 +33,13 @@ export class TerminalService implements ITerminalService, Disposable { private terminalHelper: ITerminalHelper; private terminalActivator: ITerminalActivator; private terminalAutoActivator: ITerminalAutoActivation; + private applicationShell: IApplicationShell; private readonly executeCommandListeners: Set = new Set(); private _terminalFirstLaunched: boolean = true; + private pythonReplCommandQueue: string[] = []; + private isReplReady: boolean = false; + private replPromptListener?: Disposable; + private replShellTypeListener?: Disposable; public get onDidCloseTerminal(): Event { return this.terminalClosed.event.bind(this.terminalClosed); } @@ -48,11 +53,13 @@ export class TerminalService implements ITerminalService, Disposable { this.terminalHelper = this.serviceContainer.get(ITerminalHelper); this.terminalManager = this.serviceContainer.get(ITerminalManager); this.terminalAutoActivator = this.serviceContainer.get(ITerminalAutoActivation); + this.applicationShell = this.serviceContainer.get(IApplicationShell); this.terminalManager.onDidCloseTerminal(this.terminalCloseHandler, this, disposableRegistry); this.terminalActivator = this.serviceContainer.get(ITerminalActivator); } public dispose() { this.terminal?.dispose(); + this.disposeReplListener(); if (this.executeCommandListeners && this.executeCommandListeners.size > 0) { this.executeCommandListeners.forEach((d) => { @@ -81,7 +88,86 @@ export class TerminalService implements ITerminalService, Disposable { commandLine: string, isPythonShell: boolean, ): Promise { - const terminal = this.terminal!; + if (isPythonShell) { + if (this.isReplReady) { + this.terminal?.sendText(commandLine); + traceVerbose(`Python REPL sendText: ${commandLine}`); + } else { + // Queue command to run once REPL is ready. + this.pythonReplCommandQueue.push(commandLine); + traceVerbose(`Python REPL queued command: ${commandLine}`); + this.startReplListener(); + } + return undefined; + } + + // Non-REPL code execution + return this.executeCommandInternal(commandLine); + } + + private startReplListener(): void { + if (this.replPromptListener || this.replShellTypeListener) { + return; + } + + this.replShellTypeListener = this.terminalManager.onDidChangeTerminalState((terminal) => { + if (this.terminal && terminal === this.terminal) { + if (terminal.state.shell == 'python') { + traceVerbose('Python REPL ready from terminal shell api'); + this.onReplReady(); + } + } + }); + + let terminalData = ''; + this.replPromptListener = this.applicationShell.onDidWriteTerminalData((e) => { + if (this.terminal && e.terminal === this.terminal) { + terminalData += e.data; + if (/>>>\s*$/.test(terminalData)) { + traceVerbose('Python REPL ready, from >>> prompt detection'); + this.onReplReady(); + } + } + }); + } + + private onReplReady(): void { + if (this.isReplReady) { + return; + } + this.isReplReady = true; + this.flushReplQueue(); + this.disposeReplListener(); + } + + private disposeReplListener(): void { + if (this.replPromptListener) { + this.replPromptListener.dispose(); + this.replPromptListener = undefined; + } + if (this.replShellTypeListener) { + this.replShellTypeListener.dispose(); + this.replShellTypeListener = undefined; + } + } + + private flushReplQueue(): void { + while (this.pythonReplCommandQueue.length > 0) { + const commandLine = this.pythonReplCommandQueue.shift(); + if (commandLine) { + traceVerbose(`Executing queued REPL command: ${commandLine}`); + this.terminal?.sendText(commandLine); + } + } + } + + private async executeCommandInternal(commandLine: string): Promise { + const terminal = this.terminal; + if (!terminal) { + traceVerbose('Terminal not available, cannot execute command'); + return undefined; + } + if (!this.options?.hideFromUser) { terminal.show(true); } @@ -105,11 +191,7 @@ export class TerminalService implements ITerminalService, Disposable { await promise; } - if (isPythonShell) { - // Prevent KeyboardInterrupt in Python REPL: https://github.com/microsoft/vscode-python/issues/25468 - terminal.sendText(commandLine); - traceVerbose(`Python REPL detected, sendText: ${commandLine}`); - } else if (terminal.shellIntegration) { + if (terminal.shellIntegration) { const execution = terminal.shellIntegration.executeCommand(commandLine); traceVerbose(`Shell Integration is enabled, executeCommand: ${commandLine}`); return execution; @@ -138,6 +220,7 @@ export class TerminalService implements ITerminalService, Disposable { name: this.options?.title || 'Python', hideFromUser: this.options?.hideFromUser, }); + return; } else { this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal); this.terminal = this.terminalManager.createTerminal({ @@ -167,6 +250,9 @@ export class TerminalService implements ITerminalService, Disposable { if (terminal === this.terminal) { this.terminalClosed.fire(); this.terminal = undefined; + this.isReplReady = false; + this.disposeReplListener(); + this.pythonReplCommandQueue = []; } } diff --git a/src/client/common/vscodeApis/windowApis.ts b/src/client/common/vscodeApis/windowApis.ts index fade0a028487..90a06e7ed75a 100644 --- a/src/client/common/vscodeApis/windowApis.ts +++ b/src/client/common/vscodeApis/windowApis.ts @@ -25,6 +25,7 @@ import { NotebookDocument, NotebookEditor, NotebookDocumentShowOptions, + Terminal, } from 'vscode'; import { createDeferred, Deferred } from '../utils/async'; import { Resource } from '../types'; @@ -124,6 +125,10 @@ export function onDidStartTerminalShellExecution(handler: (e: TerminalShellExecu return window.onDidStartTerminalShellExecution(handler); } +export function onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable { + return window.onDidChangeTerminalState(handler); +} + export enum MultiStepAction { Back = 'Back', Cancel = 'Cancel', diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts index 246a599f17d6..3a6d54c9390b 100644 --- a/src/test/common/terminals/service.unit.test.ts +++ b/src/test/common/terminals/service.unit.test.ts @@ -14,8 +14,9 @@ import { Uri, Terminal as VSCodeTerminal, WorkspaceConfiguration, + TerminalDataWriteEvent, } from 'vscode'; -import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { IApplicationShell, ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import { IPlatformService } from '../../../client/common/platform/types'; import { TerminalService } from '../../../client/common/terminal/service'; @@ -56,6 +57,9 @@ suite('Terminal Service', () => { let useEnvExtensionStub: sinon.SinonStub; let interpreterService: TypeMoq.IMock; let options: TypeMoq.IMock; + let applicationShell: TypeMoq.IMock; + let onDidWriteTerminalDataEmitter: EventEmitter; + let onDidChangeTerminalStateEmitter: EventEmitter; setup(() => { useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); @@ -118,6 +122,17 @@ suite('Terminal Service', () => { mockServiceContainer.setup((c) => c.get(ITerminalActivator)).returns(() => terminalActivator.object); mockServiceContainer.setup((c) => c.get(ITerminalAutoActivation)).returns(() => terminalAutoActivator.object); mockServiceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); + + applicationShell = TypeMoq.Mock.ofType(); + onDidWriteTerminalDataEmitter = new EventEmitter(); + applicationShell.setup((a) => a.onDidWriteTerminalData).returns(() => onDidWriteTerminalDataEmitter.event); + mockServiceContainer.setup((c) => c.get(IApplicationShell)).returns(() => applicationShell.object); + + onDidChangeTerminalStateEmitter = new EventEmitter(); + terminalManager + .setup((t) => t.onDidChangeTerminalState(TypeMoq.It.isAny())) + .returns((handler) => onDidChangeTerminalStateEmitter.event(handler)); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); isWindowsStub = sinon.stub(platform, 'isWindows'); pythonConfig = TypeMoq.Mock.ofType(); @@ -230,8 +245,10 @@ suite('Terminal Service', () => { terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); - service.ensureTerminal(); - service.executeCommand(textToSend, true); + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); @@ -251,8 +268,10 @@ suite('Terminal Service', () => { terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); - service.ensureTerminal(); - service.executeCommand(textToSend, true); + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); @@ -273,8 +292,10 @@ suite('Terminal Service', () => { terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); - service.ensureTerminal(); - service.executeCommand(textToSend, true); + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); @@ -305,7 +326,9 @@ suite('Terminal Service', () => { terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.ensureTerminal(); - await service.executeCommand(textToSend, true); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.once()); }); @@ -325,13 +348,39 @@ suite('Terminal Service', () => { terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); - service.ensureTerminal(); - service.executeCommand(textToSend, true); + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); }); + test('Ensure REPL ready when onDidChangeTerminalState fires with python shell', async () => { + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + const textToSend = 'Some Text'; + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + + terminal.setup((t) => t.state).returns(() => ({ isInteractedWith: true, shell: 'python' })); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidChangeTerminalStateEmitter.fire(terminal.object); + await executePromise; + + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + test('Ensure terminal is not shown if `hideFromUser` option is set to `true`', async () => { terminalHelper .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) diff --git a/src/test/smoke/smartSend.smoke.test.ts b/src/test/smoke/smartSend.smoke.test.ts index 80eabf356330..cae41cc094d5 100644 --- a/src/test/smoke/smartSend.smoke.test.ts +++ b/src/test/smoke/smartSend.smoke.test.ts @@ -19,11 +19,8 @@ suite('Smoke Test: Run Smart Selection and Advance Cursor', async () => { suiteTeardown(closeActiveWindows); teardown(closeActiveWindows); - // TODO: Re-enable this test once the flakiness on Windows is resolved - test('Smart Send', async function () { - if (process.platform === 'win32') { - return this.skip(); - } + // TODO: Re-enable this test once the flakiness on Windows, linux are resolved + test.skip('Smart Send', async function () { const file = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', From f6e249eda6e8564aec5c29e36445ea134119d6fc Mon Sep 17 00:00:00 2001 From: Stella Huang <100439259+StellaHuang95@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:50:38 -0800 Subject: [PATCH 1075/1136] Add telemetry (#25673) added new pylance specific telemetry --- src/client/telemetry/pylance.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/client/telemetry/pylance.ts b/src/client/telemetry/pylance.ts index 3d1ba05779dd..63bd113893e2 100644 --- a/src/client/telemetry/pylance.ts +++ b/src/client/telemetry/pylance.ts @@ -467,6 +467,11 @@ "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } } */ +/* __GDPR__ + "language_server/copilot_hover" : { + "symbolName" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } +*/ /** * Telemetry event sent when LSP server crashes */ From cd15913dea5274ea17e7c55b22e55733f56e7b59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:51:44 -0800 Subject: [PATCH 1076/1136] Bump actions/upload-artifact from 5 to 6 (#25666) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
Release notes

Sourced from actions/upload-artifact's releases.

v6.0.0

v6 - What's new

[!IMPORTANT] actions/upload-artifact@v6 now runs on Node.js 24 (runs.using: node24) and requires a minimum Actions Runner version of 2.327.1. If you are using self-hosted runners, ensure they are updated before upgrading.

Node.js 24

This release updates the runtime to Node.js 24. v5 had preliminary support for Node.js 24, however this action was by default still running on Node.js 20. Now this action by default will run on Node.js 24.

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0

Commits
  • b7c566a Merge pull request #745 from actions/upload-artifact-v6-release
  • e516bc8 docs: correct description of Node.js 24 support in README
  • ddc45ed docs: update README to correct action name for Node.js 24 support
  • 615b319 chore: release v6.0.0 for Node.js 24 support
  • 017748b Merge pull request #744 from actions/fix-storage-blob
  • 38d4c79 chore: rebuild dist
  • 7d27270 chore: add missing license cache files for @​actions/core, @​actions/io, and mi...
  • 5f643d3 chore: update license files for @​actions/artifact@​5.0.1 dependencies
  • 1df1684 chore: update package-lock.json with @​actions/artifact@​5.0.1
  • b5b1a91 fix: update @​actions/artifact to ^5.0.0 for Node.js 24 punycode fix
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 892d5d56f4fc..bd46877eb802 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -680,7 +680,7 @@ jobs: run: npm run test:cover:report - name: Upload HTML report - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ${{ runner.os }}-coverage-report-html path: ./coverage From 00b37cb0ecba48433d3d108db5fff75a0428ab26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:51:51 -0800 Subject: [PATCH 1077/1136] Bump dessant/lock-threads from 5.0.1 to 6.0.0 (#25667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dessant/lock-threads](https://github.com/dessant/lock-threads) from 5.0.1 to 6.0.0.
Release notes

Sourced from dessant/lock-threads's releases.

v6.0.0

Learn more about this release from the changelog.

Changelog

Sourced from dessant/lock-threads's changelog.

Changelog

All notable changes to this project will be documented in this file. See commit-and-tag-version for commit guidelines.

6.0.0 (2025-12-12)

⚠ BREAKING CHANGES

  • the action now requires Node.js 24

Bug Fixes

5.0.1 (2023-11-22)

Bug Fixes

  • support filtering threads by labels with spaces (0a63678), closes #40

5.0.0 (2023-11-14)

⚠ BREAKING CHANGES

  • Discussions are also processed by default, set the process-only input parameter to preserve the old behavior
    steps:
      - uses: dessant/lock-threads@v5
        with:
          process-only: 'issues, prs'
  • the action now requires Node.js 20

Features

Bug Fixes

4.0.1 (2023-06-12)

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dessant/lock-threads&package-manager=github_actions&previous-version=5.0.1&new-version=6.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lock-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock-issues.yml b/.github/workflows/lock-issues.yml index cb6ed2e9d54e..544d04ee185e 100644 --- a/.github/workflows/lock-issues.yml +++ b/.github/workflows/lock-issues.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Lock Issues' - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 + uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0 with: github-token: ${{ github.token }} issue-inactive-days: '30' From 1530637a1d69886e6722525a483bb2bd1656496b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:51:58 -0800 Subject: [PATCH 1078/1136] Bump actions/upload-artifact from 5 to 6 in /.github/actions/build-vsix (#25668) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
Release notes

Sourced from actions/upload-artifact's releases.

v6.0.0

v6 - What's new

[!IMPORTANT] actions/upload-artifact@v6 now runs on Node.js 24 (runs.using: node24) and requires a minimum Actions Runner version of 2.327.1. If you are using self-hosted runners, ensure they are updated before upgrading.

Node.js 24

This release updates the runtime to Node.js 24. v5 had preliminary support for Node.js 24, however this action was by default still running on Node.js 20. Now this action by default will run on Node.js 24.

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0

Commits
  • b7c566a Merge pull request #745 from actions/upload-artifact-v6-release
  • e516bc8 docs: correct description of Node.js 24 support in README
  • ddc45ed docs: update README to correct action name for Node.js 24 support
  • 615b319 chore: release v6.0.0 for Node.js 24 support
  • 017748b Merge pull request #744 from actions/fix-storage-blob
  • 38d4c79 chore: rebuild dist
  • 7d27270 chore: add missing license cache files for @​actions/core, @​actions/io, and mi...
  • 5f643d3 chore: update license files for @​actions/artifact@​5.0.1 dependencies
  • 1df1684 chore: update package-lock.json with @​actions/artifact@​5.0.1
  • b5b1a91 fix: update @​actions/artifact to ^5.0.0 for Node.js 24 punycode fix
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-vsix/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index 1b665363b34f..95fec979b08e 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -93,7 +93,7 @@ runs: VSIX_NAME: ${{ inputs.vsix_name }} - name: Upload VSIX - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ${{ inputs.artifact_name }} path: ${{ inputs.vsix_name }} From 05405197f2fb9b8d70c6db088edbcdad1b6dbc0a Mon Sep 17 00:00:00 2001 From: apuly Date: Tue, 16 Dec 2025 22:14:52 +0000 Subject: [PATCH 1079/1136] Don't re-run pytest when an exception in a test occurs (#25588) The general `except` around the pytest run was causing tests to run twice if the exception handling of pytest is disabled. From the comment in the code it seems the exception handling is only there for when reading the test IDs break, so it shouldn't be required around the pytest main call. Disabling the exception handling can be practical for debugging tests, as this starts up the python debugger within vscode. Currently however, this requires manually patching the run_pytest_script.py, which needs to be re-done every vscode update. fixes: https://github.com/microsoft/vscode-python/issues/25656 Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- python_files/vscode_pytest/run_pytest_script.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/python_files/vscode_pytest/run_pytest_script.py b/python_files/vscode_pytest/run_pytest_script.py index 8d30ba7e4399..50ab12a35423 100644 --- a/python_files/vscode_pytest/run_pytest_script.py +++ b/python_files/vscode_pytest/run_pytest_script.py @@ -55,12 +55,13 @@ def run_pytest(args): try: # Read the test ids from the file and run pytest. ids = ids_path.read_text(encoding="utf-8").splitlines() - arg_array = ["-p", "vscode_pytest", *args, *ids] - print("Running pytest with args: " + str(arg_array)) - pytest.main(arg_array) except Exception as e: print("Error[vscode-pytest]: unable to read testIds from temp file" + str(e)) run_pytest(args) + else: + arg_array = ["-p", "vscode_pytest", *args, *ids] + print("Running pytest with args: " + str(arg_array)) + pytest.main(arg_array) finally: # Delete the test ids temp file. try: From 832a9aa34b16eec6878e7f1b232e0db1ff98c781 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:01:02 -0800 Subject: [PATCH 1080/1136] fix(reportIssue): duplication of data (#25676) fixes https://github.com/microsoft/vscode-python/issues/25369 --- src/client/common/application/commands.ts | 2 +- .../application/commands/reportIssueCommand.ts | 2 +- .../commands/reportIssueCommand.unit.test.ts | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 402025ee38db..b43dc0a1e4a4 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -87,7 +87,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu ['jupyter.opennotebook']: [undefined | Uri, undefined | CommandSource]; ['jupyter.runallcells']: [Uri]; ['extension.open']: [string]; - ['workbench.action.openIssueReporter']: [{ extensionId: string; issueBody: string }]; + ['workbench.action.openIssueReporter']: [{ extensionId: string; issueBody: string; extensionData?: string }]; [Commands.GetSelectedInterpreterPath]: [{ workspaceFolder: string } | string[]]; [Commands.TriggerEnvironmentSelection]: [undefined | Uri]; [Commands.Start_Native_REPL]: [undefined | Uri]; diff --git a/src/client/common/application/commands/reportIssueCommand.ts b/src/client/common/application/commands/reportIssueCommand.ts index e5633f4a4389..9ae099e44b4f 100644 --- a/src/client/common/application/commands/reportIssueCommand.ts +++ b/src/client/common/application/commands/reportIssueCommand.ts @@ -121,7 +121,7 @@ export class ReportIssueCommandHandler implements IExtensionSingleActivationServ await this.commandManager.executeCommand('workbench.action.openIssueReporter', { extensionId: 'ms-python.python', issueBody: template, - data: userTemplate.format( + extensionData: userTemplate.format( pythonVersion, virtualEnvKind, languageServer, diff --git a/src/test/common/application/commands/reportIssueCommand.unit.test.ts b/src/test/common/application/commands/reportIssueCommand.unit.test.ts index b1884fa83c21..175a43d14007 100644 --- a/src/test/common/application/commands/reportIssueCommand.unit.test.ts +++ b/src/test/common/application/commands/reportIssueCommand.unit.test.ts @@ -126,17 +126,17 @@ suite('Report Issue Command', () => { ); const expectedData = fs.readFileSync(userDataTemplatePath, 'utf8'); - const args: [string, { extensionId: string; issueBody: string; data: string }] = capture< + const args: [string, { extensionId: string; issueBody: string; extensionData: string }] = capture< AllCommands, - { extensionId: string; issueBody: string; data: string } + { extensionId: string; issueBody: string; extensionData: string } >(cmdManager.executeCommand).last(); verify(cmdManager.registerCommand(Commands.ReportIssue, anything(), anything())).once(); verify(cmdManager.executeCommand('workbench.action.openIssueReporter', anything())).once(); expect(args[0]).to.be.equal('workbench.action.openIssueReporter'); - const { issueBody, data } = args[1]; + const { issueBody, extensionData } = args[1]; expect(issueBody).to.be.equal(expectedIssueBody); - expect(data).to.be.equal(expectedData); + expect(extensionData).to.be.equal(expectedData); }); test('Test if issue body is filled when only including settings which are explicitly set', async () => { @@ -167,16 +167,16 @@ suite('Report Issue Command', () => { ); const expectedData = fs.readFileSync(userDataTemplatePath, 'utf8'); - const args: [string, { extensionId: string; issueBody: string; data: string }] = capture< + const args: [string, { extensionId: string; issueBody: string; extensionData: string }] = capture< AllCommands, - { extensionId: string; issueBody: string; data: string } + { extensionId: string; issueBody: string; extensionData: string } >(cmdManager.executeCommand).last(); verify(cmdManager.executeCommand('workbench.action.openIssueReporter', anything())).once(); expect(args[0]).to.be.equal('workbench.action.openIssueReporter'); - const { issueBody, data } = args[1]; + const { issueBody, extensionData } = args[1]; expect(issueBody).to.be.equal(expectedIssueBody); - expect(data).to.be.equal(expectedData); + expect(extensionData).to.be.equal(expectedData); }); test('Should send telemetry event when run Report Issue Command', async () => { const sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent'); From dc7f9bf8823522d14def05596b54d8ad0f5a3578 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 18:43:26 +0000 Subject: [PATCH 1081/1136] Register test commands before first await to prevent activation race condition (#25654) fixes https://github.com/microsoft/vscode-python/issues/22783 and https://github.com/microsoft/vscode-python/issues/21865 The `python.configureTests` command was registered after multiple async operations during extension activation, creating a race condition where users could invoke the command before it was registered. ## Changes - **Extract command registration**: Created standalone `registerTestCommands()` function in `testing/main.ts` containing all test command handlers (`Tests_Configure`, `Tests_CopilotSetup`, `CopyTestId`) - **Register synchronously before first await**: Moved `unitTestsRegisterTypes()` and `registerTestCommands()` to `extension.ts` immediately after `initializeStandard()`, before `experimentService.activate()` - **Remove duplicate registrations**: Cleaned up original registration in `UnitTestManagementService.activate()` and `activateLegacy()` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- src/client/extension.ts | 7 ++ src/client/extensionActivation.ts | 2 - src/client/testing/main.ts | 155 +++++++++++++++++------------- 3 files changed, 93 insertions(+), 71 deletions(-) diff --git a/src/client/extension.ts b/src/client/extension.ts index af26a5657330..c3fb2a3ab3b0 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -45,6 +45,8 @@ import { buildProposedApi } from './proposedApi'; import { GLOBAL_PERSISTENT_KEYS } from './common/persistentState'; import { registerTools } from './chat'; import { IRecommendedEnvironmentService } from './interpreter/configuration/types'; +import { registerTypes as unitTestsRegisterTypes } from './testing/serviceRegistry'; +import { registerTestCommands } from './testing/main'; durations.codeLoadingTime = stopWatch.elapsedTime; @@ -121,6 +123,11 @@ async function activateUnsafe( // Note standard utils especially experiment and platform code are fundamental to the extension // and should be available before we activate anything else.Hence register them first. initializeStandard(ext); + + // Register test services and commands early to prevent race conditions. + unitTestsRegisterTypes(ext.legacyIOC.serviceManager); + registerTestCommands(activatedServiceContainer); + // We need to activate experiments before initializing components as objects are created or not created based on experiments. const experimentService = activatedServiceContainer.get(IExperimentService); // This guarantees that all experiment information has loaded & all telemetry will contain experiment info. diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 8330d5010f7a..6e870e37ef3e 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -33,7 +33,6 @@ import { setExtensionInstallTelemetryProperties } from './telemetry/extensionIns import { registerTypes as tensorBoardRegisterTypes } from './tensorBoard/serviceRegistry'; import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry'; import { ICodeExecutionHelper, ICodeExecutionManager, ITerminalAutoActivation } from './terminals/types'; -import { registerTypes as unitTestsRegisterTypes } from './testing/serviceRegistry'; // components import * as pythonEnvironments from './pythonEnvironments'; @@ -144,7 +143,6 @@ async function activateLegacy(ext: ExtensionState, startupStopWatch: StopWatch): const { enableProposedApi } = applicationEnv.packageJson; serviceManager.addSingletonInstance(UseProposedApi, enableProposedApi); // Feature specific registrations. - unitTestsRegisterTypes(serviceManager); installerRegisterTypes(serviceManager); commonRegisterTerminalTypes(serviceManager); debugConfigurationRegisterTypes(serviceManager); diff --git a/src/client/testing/main.ts b/src/client/testing/main.ts index e794d5711f2a..eed4d70e852c 100644 --- a/src/client/testing/main.ts +++ b/src/client/testing/main.ts @@ -18,7 +18,7 @@ import { IDisposableRegistry, Product } from '../common/types'; import { IInterpreterService } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; import { EventName } from '../telemetry/constants'; -import { captureTelemetry, sendTelemetryEvent } from '../telemetry/index'; +import { sendTelemetryEvent } from '../telemetry/index'; import { selectTestWorkspace } from './common/testUtils'; import { TestSettingsPropertyNames } from './configuration/types'; import { ITestConfigurationService, ITestsHelper } from './common/types'; @@ -42,6 +42,91 @@ export class TestingService implements ITestingService { } } +/** + * Registers command handlers but defers service resolution until the commands are actually invoked, + * allowing registration to happen before all services are fully initialized. + */ +export function registerTestCommands(serviceContainer: IServiceContainer): void { + // Resolve only the essential services needed for command registration itself + const disposableRegistry = serviceContainer.get(IDisposableRegistry); + const commandManager = serviceContainer.get(ICommandManager); + + // Helper function to configure tests - services are resolved when invoked, not at registration time + const configureTestsHandler = async (resource?: Uri) => { + sendTelemetryEvent(EventName.UNITTEST_CONFIGURE); + + // Resolve services lazily when the command is invoked + const workspaceService = serviceContainer.get(IWorkspaceService); + + let wkspace: Uri | undefined; + if (resource) { + const wkspaceFolder = workspaceService.getWorkspaceFolder(resource); + wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; + } else { + const appShell = serviceContainer.get(IApplicationShell); + wkspace = await selectTestWorkspace(appShell); + } + if (!wkspace) { + return; + } + const interpreterService = serviceContainer.get(IInterpreterService); + const cmdManager = serviceContainer.get(ICommandManager); + if (!(await interpreterService.getActiveInterpreter(wkspace))) { + cmdManager.executeCommand(constants.Commands.TriggerEnvironmentSelection, wkspace); + return; + } + const configurationService = serviceContainer.get(ITestConfigurationService); + await configurationService.promptToEnableAndConfigureTestFramework(wkspace); + }; + + disposableRegistry.push( + // Command: python.configureTests - prompts user to configure test framework + commandManager.registerCommand( + constants.Commands.Tests_Configure, + (_, _cmdSource: constants.CommandSource = constants.CommandSource.commandPalette, resource?: Uri) => { + // Invoke configuration handler (errors are ignored as this can be called from multiple places) + configureTestsHandler(resource).ignoreErrors(); + traceVerbose('Testing: Trigger refresh after config change'); + // Refresh test data if test controller is available (resolved lazily) + if (tests && !!tests.createTestController) { + const testController = serviceContainer.get(ITestController); + testController?.refreshTestData(resource, { forceRefresh: true }); + } + }, + ), + // Command: python.tests.copilotSetup - Copilot integration for test setup + commandManager.registerCommand(constants.Commands.Tests_CopilotSetup, (resource?: Uri): + | { message: string; command: Command } + | undefined => { + // Resolve services lazily when the command is invoked + const workspaceService = serviceContainer.get(IWorkspaceService); + const wkspaceFolder = + workspaceService.getWorkspaceFolder(resource) || workspaceService.workspaceFolders?.at(0); + if (!wkspaceFolder) { + return undefined; + } + + const configurationService = serviceContainer.get(ITestConfigurationService); + if (configurationService.hasConfiguredTests(wkspaceFolder.uri)) { + return undefined; + } + + return { + message: Testing.copilotSetupMessage, + command: { + title: Testing.configureTests, + command: constants.Commands.Tests_Configure, + arguments: [undefined, constants.CommandSource.ui, resource], + }, + }; + }), + // Command: python.copyTestId - copies test ID to clipboard + commandManager.registerCommand(constants.Commands.CopyTestId, async (testItem: TestItem) => { + writeTestIdToClipboard(testItem); + }), + ); +} + @injectable() export class UnitTestManagementService implements IExtensionActivationService { private activatedOnce: boolean = false; @@ -80,7 +165,6 @@ export class UnitTestManagementService implements IExtensionActivationService { this.activatedOnce = true; this.registerHandlers(); - this.registerCommands(); if (!!tests.testResults) { await this.updateTestUIButtons(); @@ -130,73 +214,6 @@ export class UnitTestManagementService implements IExtensionActivationService { await Promise.all(changedWorkspaces.map((u) => this.testController?.refreshTestData(u))); } - @captureTelemetry(EventName.UNITTEST_CONFIGURE, undefined, false) - private async configureTests(resource?: Uri) { - let wkspace: Uri | undefined; - if (resource) { - const wkspaceFolder = this.workspaceService.getWorkspaceFolder(resource); - wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; - } else { - const appShell = this.serviceContainer.get(IApplicationShell); - wkspace = await selectTestWorkspace(appShell); - } - if (!wkspace) { - return; - } - const interpreterService = this.serviceContainer.get(IInterpreterService); - const commandManager = this.serviceContainer.get(ICommandManager); - if (!(await interpreterService.getActiveInterpreter(wkspace))) { - commandManager.executeCommand(constants.Commands.TriggerEnvironmentSelection, wkspace); - return; - } - const configurationService = this.serviceContainer.get(ITestConfigurationService); - await configurationService.promptToEnableAndConfigureTestFramework(wkspace!); - } - - private registerCommands(): void { - const commandManager = this.serviceContainer.get(ICommandManager); - this.disposableRegistry.push( - commandManager.registerCommand( - constants.Commands.Tests_Configure, - (_, _cmdSource: constants.CommandSource = constants.CommandSource.commandPalette, resource?: Uri) => { - // Ignore the exceptions returned. - // This command will be invoked from other places of the extension. - this.configureTests(resource).ignoreErrors(); - traceVerbose('Testing: Trigger refresh after config change'); - this.testController?.refreshTestData(resource, { forceRefresh: true }); - }, - ), - commandManager.registerCommand(constants.Commands.Tests_CopilotSetup, (resource?: Uri): - | { message: string; command: Command } - | undefined => { - const wkspaceFolder = - this.workspaceService.getWorkspaceFolder(resource) || this.workspaceService.workspaceFolders?.at(0); - if (!wkspaceFolder) { - return undefined; - } - - const configurationService = this.serviceContainer.get( - ITestConfigurationService, - ); - if (configurationService.hasConfiguredTests(wkspaceFolder.uri)) { - return undefined; - } - - return { - message: Testing.copilotSetupMessage, - command: { - title: Testing.configureTests, - command: constants.Commands.Tests_Configure, - arguments: [undefined, constants.CommandSource.ui, resource], - }, - }; - }), - commandManager.registerCommand(constants.Commands.CopyTestId, async (testItem: TestItem) => { - writeTestIdToClipboard(testItem); - }), - ); - } - private registerHandlers() { const interpreterService = this.serviceContainer.get(IInterpreterService); this.disposableRegistry.push( From be293a9d76a0c78aab884552954dd6f641d405ed Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:35:09 -0800 Subject: [PATCH 1082/1136] fix(tests): enhance pytest-cov detection (#25683) fixes https://github.com/microsoft/vscode-python/issues/25590 --- python_files/tests/pytestadapter/helpers.py | 39 +++++++++++++------ .../tests/pytestadapter/test_coverage.py | 20 ++++++++++ python_files/vscode_pytest/__init__.py | 4 +- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/python_files/tests/pytestadapter/helpers.py b/python_files/tests/pytestadapter/helpers.py index 25e6187e2efa..03f1187149df 100644 --- a/python_files/tests/pytestadapter/helpers.py +++ b/python_files/tests/pytestadapter/helpers.py @@ -264,17 +264,34 @@ def runner_with_cwd_env( pipe_name = generate_random_pipe_name("pytest-discovery-test") if "COVERAGE_ENABLED" in env_add and "_TEST_VAR_UNITTEST" not in env_add: - process_args = [ - sys.executable, - "-m", - "pytest", - "-p", - "vscode_pytest", - "--cov=.", - "--cov-branch", - "-s", - *args, - ] + if "_PYTEST_MANUAL_PLUGIN_LOAD" in env_add: + # Test manual plugin loading scenario for issue #25590 + process_args = [ + sys.executable, + "-m", + "pytest", + "--disable-plugin-autoload", + "-p", + "pytest_cov.plugin", + "-p", + "vscode_pytest", + "--cov=.", + "--cov-branch", + "-s", + *args, + ] + else: + process_args = [ + sys.executable, + "-m", + "pytest", + "-p", + "vscode_pytest", + "--cov=.", + "--cov-branch", + "-s", + *args, + ] # Generate pipe name, pipe name specific per OS type. diff --git a/python_files/tests/pytestadapter/test_coverage.py b/python_files/tests/pytestadapter/test_coverage.py index d2d276172a8d..f2387527698f 100644 --- a/python_files/tests/pytestadapter/test_coverage.py +++ b/python_files/tests/pytestadapter/test_coverage.py @@ -142,3 +142,23 @@ def test_coverage_w_omit_config(): assert results # assert one file is reported and one file (as specified in pyproject.toml) is omitted assert len(results) == 1 + + +def test_pytest_cov_manual_plugin_loading(): + """ + Test that pytest-cov is detected when loaded manually via -p pytest_cov.plugin. + + This test verifies the fix for issue #25590, where pytest-cov detection failed + when using --disable-plugin-autoload with -p pytest_cov.plugin. The plugin is + registered under its module name (pytest_cov.plugin) instead of entry point name + (pytest_cov) in this scenario. + """ + args = ["--collect-only"] + env_add = {"COVERAGE_ENABLED": "True", "_PYTEST_MANUAL_PLUGIN_LOAD": "True"} + cov_folder_path = TEST_DATA_PATH / "coverage_gen" + + # Should NOT raise VSCodePytestError about pytest-cov not being installed + actual = runner_with_cwd_env(args, cov_folder_path, env_add) + assert actual is not None + # Verify discovery succeeded (status != "error") + assert actual[0].get("status") != "error" diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 1222a324b232..89565dab1264 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -87,7 +87,9 @@ def __init__(self, message): def pytest_load_initial_conftests(early_config, parser, args): # noqa: ARG001 - has_pytest_cov = early_config.pluginmanager.hasplugin("pytest_cov") + has_pytest_cov = early_config.pluginmanager.hasplugin( + "pytest_cov" + ) or early_config.pluginmanager.hasplugin("pytest_cov.plugin") has_cov_arg = any("--cov" in arg for arg in args) if has_cov_arg and not has_pytest_cov: raise VSCodePytestError( From 79035c8d008e937860a2b7eb4a4dbbb862ee259b Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:40:14 -0800 Subject: [PATCH 1083/1136] fix(execution): redirect error messages to stderr and clean up print statements (#25684) fixes https://github.com/microsoft/vscode-python/issues/24743 --- python_files/unittestadapter/execution.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/python_files/unittestadapter/execution.py b/python_files/unittestadapter/execution.py index a0a48c61470a..951289850884 100644 --- a/python_files/unittestadapter/execution.py +++ b/python_files/unittestadapter/execution.py @@ -284,10 +284,10 @@ def send_run_data(raw_data, test_run_pipe): run_test_ids_pipe = os.environ.get("RUN_TEST_IDS_PIPE") test_run_pipe = os.getenv("TEST_RUN_PIPE") if not run_test_ids_pipe: - print("Error[vscode-unittest]: RUN_TEST_IDS_PIPE env var is not set.") + print("Error[vscode-unittest]: RUN_TEST_IDS_PIPE env var is not set.", file=sys.stderr) raise VSCodeUnittestError("Error[vscode-unittest]: RUN_TEST_IDS_PIPE env var is not set.") if not test_run_pipe: - print("Error[vscode-unittest]: TEST_RUN_PIPE env var is not set.") + print("Error[vscode-unittest]: TEST_RUN_PIPE env var is not set.", file=sys.stderr) raise VSCodeUnittestError("Error[vscode-unittest]: TEST_RUN_PIPE env var is not set.") test_ids = [] cwd = pathlib.Path(start_dir).absolute() @@ -295,11 +295,10 @@ def send_run_data(raw_data, test_run_pipe): # Read the test ids from the file, attempt to delete file afterwords. ids_path = pathlib.Path(run_test_ids_pipe) test_ids = ids_path.read_text(encoding="utf-8").splitlines() - print("Received test ids from temp file.") try: ids_path.unlink() except Exception as e: - print("Error[vscode-pytest]: unable to delete temp file" + str(e)) + print(f"Error[vscode-unittest]: unable to delete temp file: {e}", file=sys.stderr) except Exception as e: # No test ids received from buffer, return error payload @@ -318,10 +317,6 @@ def send_run_data(raw_data, test_run_pipe): is_coverage_run = os.environ.get("COVERAGE_ENABLED") is not None include_branches = False if is_coverage_run: - print( - "COVERAGE_ENABLED env var set, starting coverage. workspace_root used as parent dir:", - workspace_root, - ) import coverage # insert "python_files/lib/python" into the path so packaging can be imported @@ -350,7 +345,6 @@ def send_run_data(raw_data, test_run_pipe): # If no error occurred, we will have test ids to run. if manage_py_path := os.environ.get("MANAGE_PY_PATH"): - print("MANAGE_PY_PATH env var set, running Django test suite.") args = argv[index + 1 :] or [] django_execution_runner(manage_py_path, test_ids, args) else: From 5109c79331e337de7d4c159aeac082dcdae0fce7 Mon Sep 17 00:00:00 2001 From: iBug Date: Mon, 5 Jan 2026 15:47:18 +0800 Subject: [PATCH 1084/1136] Add __repr__ to custom PS1 class (#25568) During my debugging and implementation of #25521, I find a descriptive `__repr__()` for the custom PS1 object very helpful, so this PR comes as an addition for #25521. --------- Co-authored-by: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> --- python_files/pythonrc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python_files/pythonrc.py b/python_files/pythonrc.py index 63c52bc009da..3042ffb7a309 100644 --- a/python_files/pythonrc.py +++ b/python_files/pythonrc.py @@ -75,6 +75,9 @@ def __str__(self): return result + def __repr__(self): + return "" + if sys.platform != "win32" and (not is_wsl): sys.ps1 = PS1() From ee981bd7ecb488bf17e870cc87b593d4c82c62b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 08:46:53 -0800 Subject: [PATCH 1085/1136] Bump qs from 6.12.1 to 6.14.1 (#25705) Bumps [qs](https://github.com/ljharb/qs) from 6.12.1 to 6.14.1.
Changelog

Sourced from qs's changelog.

6.14.1

  • [Fix] ensure arrayLength applies to [] notation as well
  • [Fix] parse: when a custom decoder returns null for a key, ignore that key
  • [Refactor] parse: extract key segment splitting helper
  • [meta] add threat model
  • [actions] add workflow permissions
  • [Tests] stringify: increase coverage
  • [Dev Deps] update eslint, @ljharb/eslint-config, npmignore, es-value-fixtures, for-each, object-inspect

6.14.0

  • [New] parse: add throwOnParameterLimitExceeded option (#517)
  • [Refactor] parse: use utils.combine more
  • [patch] parse: add explicit throwOnLimitExceeded default
  • [actions] use shared action; re-add finishers
  • [meta] Fix changelog formatting bug
  • [Deps] update side-channel
  • [Dev Deps] update es-value-fixtures, has-bigints, has-proto, has-symbols
  • [Tests] increase coverage

6.13.1

  • [Fix] stringify: avoid a crash when a filter key is null
  • [Fix] utils.merge: functions should not be stringified into keys
  • [Fix] parse: avoid a crash with interpretNumericEntities: true, comma: true, and iso charset
  • [Fix] stringify: ensure a non-string filter does not crash
  • [Refactor] use __proto__ syntax instead of Object.create for null objects
  • [Refactor] misc cleanup
  • [Tests] utils.merge: add some coverage
  • [Tests] fix a test case
  • [actions] split out node 10-20, and 20+
  • [Dev Deps] update es-value-fixtures, mock-property, object-inspect, tape

6.13.0

  • [New] parse: add strictDepth option (#511)
  • [Tests] use npm audit instead of aud

6.12.3

  • [Fix] parse: properly account for strictNullHandling when allowEmptyArrays
  • [meta] fix changelog indentation

6.12.2

  • [Fix] parse: parse encoded square brackets (#506)
  • [readme] add CII best practices badge
Commits
  • 3fa11a5 v6.14.1
  • a626704 [Dev Deps] update npmignore
  • 3086902 [Fix] ensure arrayLength applies to [] notation as well
  • fc7930e [Dev Deps] update eslint, @ljharb/eslint-config
  • 0b06aac [Dev Deps] update @ljharb/eslint-config
  • 64951f6 [Refactor] parse: extract key segment splitting helper
  • e1bd259 [Dev Deps] update @ljharb/eslint-config
  • f4b3d39 [eslint] add eslint 9 optional peer dep
  • 6e94d95 [Dev Deps] update eslint, @ljharb/eslint-config, npmignore
  • 973dc3c [actions] add workflow permissions
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=qs&package-manager=npm_and_yarn&previous-version=6.12.1&new-version=6.14.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 145 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 119 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9d0c761b8e5f..0429833baad9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10907,10 +10907,13 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11802,12 +11805,12 @@ } }, "node_modules/qs": { - "version": "6.12.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", - "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "dev": true, "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -12531,15 +12534,69 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -23396,9 +23453,9 @@ "dev": true }, "object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true }, "object-is": { @@ -24070,12 +24127,12 @@ "dev": true }, "qs": { - "version": "6.12.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", - "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "dev": true, "requires": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" } }, "query-string": { @@ -24607,15 +24664,51 @@ } }, "side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "requires": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" } }, "signal-exit": { From 0e9282bdb1c0fc619e40f3cd43dd8f0588f61a4e Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:58:23 -0800 Subject: [PATCH 1086/1136] update PET version to 2026.0 (#25714) --- build/azure-pipeline.stable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 1815605b278d..237ba08dbc99 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -128,7 +128,7 @@ extends: project: 'Monaco' definition: 593 buildVersionToDownload: 'latestFromBranch' - branchName: 'refs/heads/release/2025.16' + branchName: 'refs/heads/release/2026.0' targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' artifactName: 'bin-$(buildTarget)' itemPattern: | From 72bb7214e21b097aa542a20be318fe24cf28213c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:32:52 -0800 Subject: [PATCH 1087/1136] update version to 2026.0.0 release version (#25715) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0429833baad9..1a27113ad897 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2025.21.0-dev", + "version": "2026.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2025.21.0-dev", + "version": "2026.0.0", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/package.json b/package.json index 286e80baa2df..7c0c8e7f8c0a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2025.21.0-dev", + "version": "2026.0.0", "featureFlags": { "usingNewInterpreterStorage": true }, From f14d68fa0be480a3409fbd2c426450fd4de2a43c Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:46:01 -0800 Subject: [PATCH 1088/1136] update version to 2026.1.0-dev in package.json and package-lock.json (#25716) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a27113ad897..38324ae70a98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2026.0.0", + "version": "2026.1.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2026.0.0", + "version": "2026.1.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/package.json b/package.json index 7c0c8e7f8c0a..e1ccbd1ceefd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2026.0.0", + "version": "2026.1.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 92fbff4f08dc0a4f9835afb090d7c0cb2fdfb795 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:46:30 -0800 Subject: [PATCH 1089/1136] Bump importlib-metadata from 8.7.0 to 8.7.1 (#25693) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [importlib-metadata](https://github.com/python/importlib_metadata) from 8.7.0 to 8.7.1.
Changelog

Sourced from importlib-metadata's changelog.

v8.7.1

Bugfixes

  • Fixed errors in FastPath under fork-multiprocessing. (#520)
  • Removed cruft from Python 3.8. (#524)
Commits
  • 84e9028 Finalize
  • 36ed6f6 Merge pull request #521 from 2xB/fix520
  • f6eee56 Rely on passthrough to designate a wrapper for its side effect.
  • 3c9510b Prefer noop for degenerate behavior.
  • a36bab9 Avoid if block.
  • 8dd2937 Decouple clear_after_fork from lru_cache and then compose.
  • 1da3f45 Add news fragment.
  • a1c25d8 🧎‍♀️ Genuflect to the types.
  • 4e962a8 👹 Feed the hobgoblins (delint).
  • 6a30ab9 Allow initial currsize to be greater than one (as happens when running the te...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=importlib-metadata&package-manager=pip&previous-version=8.7.0&new-version=8.7.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1e5f673f43db..ae747359d4e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ # This file was autogenerated by uv via the following command: # uv pip compile --generate-hashes requirements.in -o requirements.txt -importlib-metadata==8.7.0 \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd +importlib-metadata==8.7.1 \ + --hash=sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb \ + --hash=sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151 # via -r requirements.in microvenv==2025.0 \ --hash=sha256:568155ec18af01c89f270d35d123ab803b09672b480c3702d15fd69e9cc5bd1e \ From 336d699667166d7a2742954d6f9c1a32d47c5de1 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 8 Jan 2026 09:53:31 -0800 Subject: [PATCH 1090/1136] enhance error handling and user notifications for Python Locator failures (#25717) fixes https://github.com/microsoft/vscode-python/issues/25689 --- src/client/common/utils/localize.ts | 12 ++++ .../locators/common/nativePythonFinder.ts | 68 ++++++++++++++++++- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 067275ad732c..a084fc647025 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -516,3 +516,15 @@ export namespace CreateEnv { ); } } + +export namespace PythonLocator { + export const startupFailedNotification = l10n.t( + 'Python Locator failed to start. Python environment discovery may not work correctly.', + ); + export const windowsRuntimeMissing = l10n.t( + 'Missing Windows runtime dependencies detected. The Python Locator requires the Microsoft Visual C++ Redistributable. This is often missing on clean Windows installations.', + ); + export const windowsStartupFailed = l10n.t( + 'Python Locator failed to start on Windows. This might be due to missing system dependencies such as the Microsoft Visual C++ Redistributable.', + ); +} diff --git a/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts b/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts index cb5ab63077c9..ea0d63cd7552 100644 --- a/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts +++ b/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts @@ -15,18 +15,24 @@ import { noop } from '../../../../common/utils/misc'; import { getConfiguration, getWorkspaceFolderPaths, isTrusted } from '../../../../common/vscodeApis/workspaceApis'; import { CONDAPATH_SETTING_KEY } from '../../../common/environmentManagers/conda'; import { VENVFOLDERS_SETTING_KEY, VENVPATH_SETTING_KEY } from '../lowLevel/customVirtualEnvLocator'; -import { createLogOutputChannel } from '../../../../common/vscodeApis/windowApis'; +import { createLogOutputChannel, showWarningMessage } from '../../../../common/vscodeApis/windowApis'; import { sendNativeTelemetry, NativePythonTelemetry } from './nativePythonTelemetry'; import { NativePythonEnvironmentKind } from './nativePythonUtils'; import type { IExtensionContext } from '../../../../common/types'; import { StopWatch } from '../../../../common/utils/stopWatch'; import { untildify } from '../../../../common/helpers'; import { traceError } from '../../../../logging'; +import { Common, PythonLocator } from '../../../../common/utils/localize'; +import { Commands } from '../../../../common/constants'; +import { executeCommand } from '../../../../common/vscodeApis/commandApis'; +import { getGlobalStorage, IPersistentStorage } from '../../../../common/persistentState'; const PYTHON_ENV_TOOLS_PATH = isWindows() ? path.join(EXTENSION_ROOT_DIR, 'python-env-tools', 'bin', 'pet.exe') : path.join(EXTENSION_ROOT_DIR, 'python-env-tools', 'bin', 'pet'); +const DONT_SHOW_SPAWN_ERROR_AGAIN = 'DONT_SHOW_NATIVE_FINDER_SPAWN_ERROR_AGAIN'; + export interface NativeEnvInfo { displayName?: string; name?: string; @@ -106,8 +112,13 @@ class NativePythonFinderImpl extends DisposableBase implements NativePythonFinde timeToRefresh: 0, }; - constructor(private readonly cacheDirectory?: Uri) { + private readonly suppressErrorNotification: IPersistentStorage; + + constructor(private readonly cacheDirectory?: Uri, private readonly context?: IExtensionContext) { super(); + this.suppressErrorNotification = this.context + ? getGlobalStorage(this.context, DONT_SHOW_SPAWN_ERROR_AGAIN, false) + : ({ get: () => false, set: async () => {} } as IPersistentStorage); this.connection = this.start(); void this.configure(); this.firstRefreshResults = this.refreshFirstTime(); @@ -212,6 +223,30 @@ class NativePythonFinderImpl extends DisposableBase implements NativePythonFinde proc.stderr.on('data', (data) => this.outputChannel.error(data.toString())); writable.pipe(proc.stdin, { end: false }); + // Handle spawn errors (e.g., missing DLLs on Windows) + proc.on('error', (error) => { + this.outputChannel.error(`Python Locator process error: ${error.message}`); + this.outputChannel.error(`Error details: ${JSON.stringify(error)}`); + this.handleSpawnError(error.message); + }); + + // Handle immediate exits with error codes + let hasStarted = false; + setTimeout(() => { + hasStarted = true; + }, 1000); + + proc.on('exit', (code, signal) => { + if (!hasStarted && code !== null && code !== 0) { + const errorMessage = `Python Locator process exited immediately with code ${code}`; + this.outputChannel.error(errorMessage); + if (signal) { + this.outputChannel.error(`Exit signal: ${signal}`); + } + this.handleSpawnError(errorMessage); + } + }); + disposables.push({ dispose: () => { try { @@ -397,6 +432,33 @@ class NativePythonFinderImpl extends DisposableBase implements NativePythonFinde async getCondaInfo(): Promise { return this.connection.sendRequest('condaInfo'); } + + private async handleSpawnError(errorMessage: string): Promise { + // Check if user has chosen to not see this error again + if (this.suppressErrorNotification.get()) { + return; + } + + // Check for Windows runtime DLL issues + if (isWindows() && errorMessage.toLowerCase().includes('vcruntime')) { + this.outputChannel.error(PythonLocator.windowsRuntimeMissing); + } else if (isWindows()) { + this.outputChannel.error(PythonLocator.windowsStartupFailed); + } + + // Show notification to user + const selection = await showWarningMessage( + PythonLocator.startupFailedNotification, + Common.openOutputPanel, + Common.doNotShowAgain, + ); + + if (selection === Common.openOutputPanel) { + await executeCommand(Commands.ViewOutput); + } else if (selection === Common.doNotShowAgain) { + await this.suppressErrorNotification.set(true); + } + } } type ConfigurationOptions = { @@ -461,7 +523,7 @@ export function getNativePythonFinder(context?: IExtensionContext): NativePython } if (!_finder) { const cacheDirectory = context ? getCacheDirectory(context) : undefined; - _finder = new NativePythonFinderImpl(cacheDirectory); + _finder = new NativePythonFinderImpl(cacheDirectory, context); if (context) { context.subscriptions.push(_finder); } From eb6df9a3459af81223df451a8502b834e19f0777 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 8 Jan 2026 09:57:31 -0800 Subject: [PATCH 1091/1136] feat(tasks): add shell task for check-python with venv activation (#25686) fixes https://github.com/microsoft/vscode-python/issues/24121 since extension developers are frequently changing their shell activation settings, this provides a way to activate the venv then run the task to allow for consistent checks by developers that mirror CI --- .vscode/tasks.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 0e33420c11db..c5a054ed43cf 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -39,6 +39,24 @@ "problemMatcher": ["$python"], "label": "npm: check-python", "detail": "npm run check-python:ruff && npm run check-python:pyright" + }, + { + "label": "npm: check-python (venv)", + "type": "shell", + "command": "bash", + "args": ["-lc", "source .venv/bin/activate && npm run check-python"], + "problemMatcher": [], + "detail": "Activates the repo .venv first (avoids pyenv/shim Python) then runs: npm run check-python", + "windows": { + "command": "pwsh", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + ".\\.venv\\Scripts\\Activate.ps1; npm run check-python" + ] + } } ] } From e2681d5925fb8ef6cb810d191048bd56f56b3e3e Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 8 Jan 2026 23:51:57 +0530 Subject: [PATCH 1092/1136] bump Node.js version to 22.21.1 across multiple configuration files (#25612) Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- .devcontainer/Dockerfile | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/pr-check.yml | 2 +- .nvmrc | 2 +- build/azure-pipelines/pipeline.yml | 6 ++-- package-lock.json | 34 +++++++++++---------- package.json | 2 +- pythonExtensionApi/package.json | 2 +- src/client/common/process/rawProcessApis.ts | 3 +- 9 files changed, 29 insertions(+), 26 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 3e7e9e9cf091..ffc0150ebac5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/typescript-node:18-bookworm +FROM mcr.microsoft.com/devcontainers/typescript-node:22-bookworm RUN apt-get install -y wget bzip2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45bd02d29733..74f5d5a58a3a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: permissions: {} env: - NODE_VERSION: 22.17.0 + NODE_VERSION: 22.21.1 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 # Force a path with spaces and to test extension works in these scenarios # Unicode characters are causing 2.7 failures so skip that for now. diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index bd46877eb802..d7d8d3869505 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -10,7 +10,7 @@ on: permissions: {} env: - NODE_VERSION: 22.17.0 + NODE_VERSION: 22.21.1 PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). Also enables a reporter which exits the process running the tests if it haven't already. ARTIFACT_NAME_VSIX: ms-python-insiders-vsix diff --git a/.nvmrc b/.nvmrc index 67e145bf0f9d..c6a66a6e6a68 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.18.0 +v22.21.1 diff --git a/build/azure-pipelines/pipeline.yml b/build/azure-pipelines/pipeline.yml index 46302aa6ff90..0796e38ca598 100644 --- a/build/azure-pipelines/pipeline.yml +++ b/build/azure-pipelines/pipeline.yml @@ -37,13 +37,13 @@ extends: testPlatforms: - name: Linux nodeVersions: - - 22.17.0 + - 22.21.1 - name: MacOS nodeVersions: - - 22.17.0 + - 22.21.1 - name: Windows nodeVersions: - - 22.17.0 + - 22.21.1 testSteps: - template: /build/azure-pipelines/templates/test-steps.yml@self parameters: diff --git a/package-lock.json b/package-lock.json index 38324ae70a98..ba516d3a59eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "@types/glob": "^7.2.0", "@types/lodash": "^4.14.104", "@types/mocha": "^9.1.0", - "@types/node": "^22.5.0", + "@types/node": "^22.19.1", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^17.0.3", @@ -1894,12 +1894,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", - "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "node_modules/@types/semver": { @@ -14090,10 +14091,11 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" }, "node_modules/unicode": { "version": "14.0.0", @@ -16670,12 +16672,12 @@ "dev": true }, "@types/node": { - "version": "22.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", - "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "dev": true, "requires": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "@types/semver": { @@ -25816,9 +25818,9 @@ "dev": true }, "undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true }, "unicode": { diff --git a/package.json b/package.json index e1ccbd1ceefd..544c72dba023 100644 --- a/package.json +++ b/package.json @@ -1746,7 +1746,7 @@ "@types/glob": "^7.2.0", "@types/lodash": "^4.14.104", "@types/mocha": "^9.1.0", - "@types/node": "^22.5.0", + "@types/node": "^22.19.1", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^17.0.3", diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json index e4e956ff6065..11e0445aa8da 100644 --- a/pythonExtensionApi/package.json +++ b/pythonExtensionApi/package.json @@ -13,7 +13,7 @@ "main": "./out/main.js", "types": "./out/main.d.ts", "engines": { - "node": ">=22.17.0", + "node": ">=22.21.1", "vscode": "^1.93.0" }, "license": "MIT", diff --git a/src/client/common/process/rawProcessApis.ts b/src/client/common/process/rawProcessApis.ts index 5e3641328b69..864191851c91 100644 --- a/src/client/common/process/rawProcessApis.ts +++ b/src/client/common/process/rawProcessApis.ts @@ -58,7 +58,8 @@ export function shellExec( const shellOptions = getDefaultOptions(options, defaultEnv); if (!options.doNotLog) { const processLogger = new ProcessLogger(new WorkspaceService()); - processLogger.logProcess(command, undefined, shellOptions); + const loggingOptions = { ...shellOptions, encoding: shellOptions.encoding ?? undefined }; + processLogger.logProcess(command, undefined, loggingOptions); } return new Promise((resolve, reject) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any From c8b6f505c09fcd9bbb1c0f154cc71cdab51063d3 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 14 Jan 2026 04:42:41 -0800 Subject: [PATCH 1093/1136] Add conditional for Python agent tools (#25677) fixes https://github.com/microsoft/vscode-python/issues/25644 --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 544c72dba023..0f7958b0e263 100644 --- a/package.json +++ b/package.json @@ -1509,7 +1509,7 @@ "name": "get_python_environment_details", "displayName": "Get Python Environment Info", "userDescription": "%python.languageModelTools.get_python_environment_details.userDescription%", - "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Python Environment (conda, venv, etec), 2. Version of Python, 3. List of all installed Python packages with their versions. ALWAYS call configure_python_environment before using this tool.", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Python Environment (conda, venv, etc), 2. Version of Python, 3. List of all installed Python packages with their versions. ALWAYS call configure_python_environment before using this tool. IMPORTANT: This tool is only for Python environments (venv, virtualenv, conda, pipenv, poetry, pyenv, pixi, or any other Python environment manager). Do not use this tool for npm packages, system packages, Ruby gems, or any other non-Python dependencies.", "toolReferenceName": "getPythonEnvironmentInfo", "tags": [ "python", @@ -1534,7 +1534,7 @@ "name": "get_python_executable_details", "displayName": "Get Python Executable", "userDescription": "%python.languageModelTools.get_python_executable_details.userDescription%", - "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. ALWAYS use this tool before executing any Python command in the terminal. This tool returns the details of how to construct the fully qualified path and or command including details such as arguments required to run Python in a terminal. Note: Instead of executing `python --version` or `python -c 'import sys; print(sys.executable)'`, use this tool to get the Python executable path to replace the `python` command. E.g. instead of using `python -c 'import sys; print(sys.executable)'`, use this tool to build the command `conda run -n -c 'import sys; print(sys.executable)'`. ALWAYS call configure_python_environment before using this tool.", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. ALWAYS use this tool before executing any Python command in the terminal. This tool returns the details of how to construct the fully qualified path and or command including details such as arguments required to run Python in a terminal. Note: Instead of executing `python --version` or `python -c 'import sys; print(sys.executable)'`, use this tool to get the Python executable path to replace the `python` command. E.g. instead of using `python -c 'import sys; print(sys.executable)'`, use this tool to build the command `conda run -n -c 'import sys; print(sys.executable)'`. ALWAYS call configure_python_environment before using this tool. IMPORTANT: This tool is only for Python environments (venv, virtualenv, conda, pipenv, poetry, pyenv, pixi, or any other Python environment manager). Do not use this tool for npm packages, system packages, Ruby gems, or any other non-Python dependencies.", "toolReferenceName": "getPythonExecutableCommand", "tags": [ "python", @@ -1559,7 +1559,7 @@ "name": "install_python_packages", "displayName": "Install Python Package", "userDescription": "%python.languageModelTools.install_python_packages.userDescription%", - "modelDescription": "Installs Python packages in the given workspace. Use this tool to install Python packages in the user's chosen Python environment. ALWAYS call configure_python_environment before using this tool.", + "modelDescription": "Installs Python packages in the given workspace. Use this tool to install Python packages in the user's chosen Python environment. ALWAYS call configure_python_environment before using this tool. IMPORTANT: This tool should only be used to install Python packages using package managers like pip or conda (works with any Python environment: venv, virtualenv, pipenv, poetry, pyenv, pixi, conda, etc.). Do not use this tool to install npm packages, system packages (apt/brew/yum), Ruby gems, or any other non-Python dependencies.", "toolReferenceName": "installPythonPackage", "tags": [ "python", @@ -1593,7 +1593,7 @@ { "name": "configure_python_environment", "displayName": "Configure Python Environment", - "modelDescription": "This tool configures a Python environment in the given workspace. ALWAYS Use this tool to set up the user's chosen environment and ALWAYS call this tool before using any other Python related tools or running any Python command in the terminal.", + "modelDescription": "This tool configures a Python environment in the given workspace. ALWAYS Use this tool to set up the user's chosen environment and ALWAYS call this tool before using any other Python related tools or running any Python command in the terminal. IMPORTANT: This tool is only for Python environments (venv, virtualenv, conda, pipenv, poetry, pyenv, pixi, or any other Python environment manager). Do not use this tool for npm packages, system packages, Ruby gems, or any other non-Python dependencies.", "userDescription": "%python.languageModelTools.configure_python_environment.userDescription%", "toolReferenceName": "configurePythonEnvironment", "tags": [ From cbcf5e17341c7e0f5c71c16c3262852f643d48b8 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Wed, 21 Jan 2026 22:47:59 -0800 Subject: [PATCH 1094/1136] Fix python_server.py infinite loop on EOF (fixes #25620) (#25746) ## Summary Fixes #25620 - Leftover process `python_server.py` with 100% CPU after closing VS Code. ## The Problem When VS Code closes, the STDIN stream to `python_server.py` is closed. The `readline()` method returns empty bytes (`b''`) to signal EOF. However, the previous code incorrectly treated this as an empty line separator, causing: 1. `get_headers()` to return an empty headers dict 2. `content_length` to be 0 3. The main loop to continue immediately back to `get_headers()` 4. An infinite loop consuming 100% CPU ## The Fix This PR properly detects EOF by checking for `b''` (empty bytes) vs `b'\r\n'` or `b'\n'` (actual empty line with newline characters): **In `get_headers()`:** ```python raw = STDIN.buffer.readline() # Detect EOF: readline() returns empty bytes when input stream is closed if raw == b"": raise EOFError("EOF reached while reading headers") ``` **In all callers (main loop, `handle_response()`, `custom_input()`):** - Catch `EOFError` and exit gracefully with `sys.exit(0)` ## Key Insight ```python # EOF: readline() returns empty bytes io.BytesIO(b"").readline() # Returns b'' # Empty line: readline() returns newline bytes io.BytesIO(b"\r\n").readline() # Returns b'\r\n' ``` ## Testing Added comprehensive unit tests in `python_files/tests/test_python_server.py`: - `test_get_headers_normal` - verifies normal header parsing still works - `test_get_headers_eof_raises_error` - verifies EOF detection - `test_get_headers_eof_mid_headers_raises_error` - EOF during header reading - `test_get_headers_empty_line_terminates` - empty line still terminates headers - `test_custom_input_exits_on_eof` - graceful exit from `custom_input()` - `test_handle_response_exits_on_eof` - graceful exit from `handle_response()` - `test_main_loop_exits_on_eof` - simulates the main loop behavior - `test_readline_eof_vs_empty_line` - documents the EOF vs empty line distinction All 8 tests pass. ## How to Verify 1. Open a Python file in VS Code 2. Use Shift+Enter to start a Native REPL session and run some commands 3. Close VS Code 4. Check for leftover processes: `ps aux | grep python_server` 5. With this fix, no leftover processes should remain --- python_files/python_server.py | 25 +++- python_files/tests/test_python_server.py | 162 +++++++++++++++++++++++ 2 files changed, 180 insertions(+), 7 deletions(-) create mode 100644 python_files/tests/test_python_server.py diff --git a/python_files/python_server.py b/python_files/python_server.py index 77b43c692dc3..e7ee92794a21 100644 --- a/python_files/python_server.py +++ b/python_files/python_server.py @@ -64,6 +64,9 @@ def custom_input(prompt=""): message_text = STDIN.buffer.read(content_length).decode() message_json = json.loads(message_text) return message_json["result"]["userInput"] + except EOFError: + # Input stream closed, exit gracefully + sys.exit(0) except Exception: print_log(traceback.format_exc()) @@ -74,7 +77,7 @@ def custom_input(prompt=""): def handle_response(request_id): - while not STDIN.closed: + while True: try: headers = get_headers() # Content-Length is the data size in bytes. @@ -88,8 +91,10 @@ def handle_response(request_id): send_response(our_user_input, message_json["id"]) elif message_json["method"] == "exit": sys.exit(0) - - except Exception: # noqa: PERF203 + except EOFError: # noqa: PERF203 + # Input stream closed, exit gracefully + sys.exit(0) + except Exception: print_log(traceback.format_exc()) @@ -164,7 +169,11 @@ def get_value(self) -> str: def get_headers(): headers = {} while True: - line = STDIN.buffer.readline().decode().strip() + raw = STDIN.buffer.readline() + # Detect EOF: readline() returns empty bytes when input stream is closed + if raw == b"": + raise EOFError("EOF reached while reading headers") + line = raw.decode().strip() if not line: break name, value = line.split(":", 1) @@ -183,7 +192,7 @@ def get_headers(): while "" in sys.path: sys.path.remove("") sys.path.insert(0, "") - while not STDIN.closed: + while True: try: headers = get_headers() # Content-Length is the data size in bytes. @@ -198,6 +207,8 @@ def get_headers(): check_valid_command(request_json) elif request_json["method"] == "exit": sys.exit(0) - - except Exception: # noqa: PERF203 + except EOFError: # noqa: PERF203 + # Input stream closed (VS Code terminated), exit gracefully + sys.exit(0) + except Exception: print_log(traceback.format_exc()) diff --git a/python_files/tests/test_python_server.py b/python_files/tests/test_python_server.py new file mode 100644 index 000000000000..ca542b8ea292 --- /dev/null +++ b/python_files/tests/test_python_server.py @@ -0,0 +1,162 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Tests for python_server.py, specifically EOF handling to prevent infinite loops.""" + +import io +from unittest import mock + +import pytest + + +class TestGetHeaders: + """Tests for the get_headers function.""" + + def test_get_headers_normal(self): + """Test get_headers with valid headers.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin with valid headers + mock_input = b"Content-Length: 100\r\nContent-Type: application/json\r\n\r\n" + mock_stdin = io.BytesIO(mock_input) + + # Act + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)): + headers = python_server.get_headers() + + # Assert + assert headers == {"Content-Length": "100", "Content-Type": "application/json"} + + def test_get_headers_eof_raises_error(self): + """Test that get_headers raises EOFError when stdin is closed (EOF).""" + # Arrange: Import the module + import python_server + + # Create a mock stdin that returns empty bytes (EOF) + mock_stdin = io.BytesIO(b"") + + # Act & Assert + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)), pytest.raises( + EOFError, match="EOF reached while reading headers" + ): + python_server.get_headers() + + def test_get_headers_eof_mid_headers_raises_error(self): + """Test that get_headers raises EOFError when EOF occurs mid-headers.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin with partial headers then EOF + mock_input = b"Content-Length: 100\r\n" # No terminating empty line + mock_stdin = io.BytesIO(mock_input) + + # Act & Assert + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)), pytest.raises( + EOFError, match="EOF reached while reading headers" + ): + python_server.get_headers() + + def test_get_headers_empty_line_terminates(self): + """Test that an empty line (not EOF) properly terminates header reading.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin with headers followed by empty line + mock_input = b"Content-Length: 50\r\n\r\nsome body content" + mock_stdin = io.BytesIO(mock_input) + + # Act + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)): + headers = python_server.get_headers() + + # Assert + assert headers == {"Content-Length": "50"} + + +class TestEOFHandling: + """Tests for EOF handling in various functions that use get_headers.""" + + def test_custom_input_exits_on_eof(self): + """Test that custom_input exits gracefully on EOF.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin that returns empty bytes (EOF) + mock_stdin = io.BytesIO(b"") + mock_stdout = io.BytesIO() + + # Act & Assert + with mock.patch.object( + python_server, "STDIN", mock.Mock(buffer=mock_stdin) + ), mock.patch.object(python_server, "STDOUT", mock.Mock(buffer=mock_stdout)), pytest.raises( + SystemExit + ) as exc_info: + python_server.custom_input("prompt> ") + + # Should exit with code 0 (graceful exit) + assert exc_info.value.code == 0 + + def test_handle_response_exits_on_eof(self): + """Test that handle_response exits gracefully on EOF.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin that returns empty bytes (EOF) + mock_stdin = io.BytesIO(b"") + + # Act & Assert + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)), pytest.raises( + SystemExit + ) as exc_info: + python_server.handle_response("test-request-id") + + # Should exit with code 0 (graceful exit) + assert exc_info.value.code == 0 + + +class TestMainLoopEOFHandling: + """Tests that simulate the main loop EOF scenario.""" + + def test_main_loop_exits_on_eof(self): + """Test that the main loop pattern exits gracefully on EOF. + + This test verifies the fix for GitHub issue #25620 where the server + would spin at 100% CPU instead of exiting when VS Code closes. + """ + # Arrange: Import the module + import python_server + + # Create a mock stdin that returns empty bytes (EOF) + mock_stdin = io.BytesIO(b"") + + # Simulate what happens in the main loop + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)): + try: + python_server.get_headers() + # If we get here without raising EOFError, the fix isn't working + pytest.fail("Expected EOFError to be raised on EOF") + except EOFError: + # This is the expected behavior - the fix is working + pass + + def test_readline_eof_vs_empty_line(self): + """Test that we correctly distinguish between EOF and empty line. + + EOF: readline() returns b'' (empty bytes) + Empty line: readline() returns b'\\r\\n' or b'\\n' (newline bytes) + """ + # Test EOF case + eof_stream = io.BytesIO(b"") + result = eof_stream.readline() + assert result == b"", "EOF should return empty bytes" + + # Test empty line case + empty_line_stream = io.BytesIO(b"\r\n") + result = empty_line_stream.readline() + assert result == b"\r\n", "Empty line should return newline bytes" + + # Test empty line with just newline + empty_line_stream2 = io.BytesIO(b"\n") + result = empty_line_stream2.readline() + assert result == b"\n", "Empty line should return newline bytes" From 1965191db8467a48d3bd3594d44e111747bd0406 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:39:32 -0800 Subject: [PATCH 1095/1136] Bump lodash from 4.17.21 to 4.17.23 (#25745) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=lodash&package-manager=npm_and_yarn&previous-version=4.17.21&new-version=4.17.23)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba516d3a59eb..863c2a720678 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "iconv-lite": "^0.6.3", "inversify": "^6.0.2", "jsonc-parser": "^3.0.0", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "minimatch": "^5.0.1", "named-js-regexp": "^1.3.3", "node-stream-zip": "^1.6.0", @@ -9518,9 +9518,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" }, "node_modules/lodash.flattendeep": { "version": "4.4.0", @@ -22393,9 +22393,9 @@ } }, "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" }, "lodash.flattendeep": { "version": "4.4.0", diff --git a/package.json b/package.json index 0f7958b0e263..bc9131276c59 100644 --- a/package.json +++ b/package.json @@ -1713,7 +1713,7 @@ "iconv-lite": "^0.6.3", "inversify": "^6.0.2", "jsonc-parser": "^3.0.0", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "minimatch": "^5.0.1", "named-js-regexp": "^1.3.3", "node-stream-zip": "^1.6.0", From 0b477a3baa6a64c3e0ebc9a58ee277b358bb93c1 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:19:59 -0800 Subject: [PATCH 1096/1136] remove method to update defaultInterpreterPath on env ext environment change (#25736) fixes https://github.com/microsoft/vscode-python-environments/issues/1082 --- src/client/envExt/api.internal.ts | 32 +------------------------------ src/client/extensionActivation.ts | 14 +------------- 2 files changed, 2 insertions(+), 44 deletions(-) diff --git a/src/client/envExt/api.internal.ts b/src/client/envExt/api.internal.ts index c4247f63a3c5..07bc58ffc11e 100644 --- a/src/client/envExt/api.internal.ts +++ b/src/client/envExt/api.internal.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { EventEmitter, Terminal, Uri, Disposable, ConfigurationTarget } from 'vscode'; +import { EventEmitter, Terminal, Uri, Disposable } from 'vscode'; import { getExtension } from '../common/vscodeApis/extensionsApi'; import { GetEnvironmentScope, @@ -13,7 +13,6 @@ import { DidChangeEnvironmentEventArgs, } from './types'; import { executeCommand } from '../common/vscodeApis/commandApis'; -import { IInterpreterPathService } from '../common/types'; import { getConfiguration } from '../common/vscodeApis/workspaceApis'; export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; @@ -128,32 +127,3 @@ export async function clearCache(): Promise { await executeCommand('python-envs.clearCache'); } } - -export function registerEnvExtFeatures( - disposables: Disposable[], - interpreterPathService: IInterpreterPathService, -): void { - if (useEnvExtension()) { - disposables.push( - onDidChangeEnvironmentEnvExt(async (e: DidChangeEnvironmentEventArgs) => { - const previousPath = interpreterPathService.get(e.uri); - - if (previousPath !== e.new?.environmentPath.fsPath) { - if (e.uri) { - await interpreterPathService.update( - e.uri, - ConfigurationTarget.WorkspaceFolder, - e.new?.environmentPath.fsPath, - ); - } else { - await interpreterPathService.update( - undefined, - ConfigurationTarget.Global, - e.new?.environmentPath.fsPath, - ); - } - } - }), - ); - } -} diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 6e870e37ef3e..57bcb8237eeb 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -13,14 +13,7 @@ import { IApplicationEnvironment, ICommandManager, IWorkspaceService } from './c import { Commands, PYTHON_LANGUAGE, UseProposedApi } from './common/constants'; import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; import { IFileSystem } from './common/platform/types'; -import { - IConfigurationService, - IDisposableRegistry, - IExtensions, - IInterpreterPathService, - ILogOutputChannel, - IPathUtils, -} from './common/types'; +import { IConfigurationService, IDisposableRegistry, IExtensions, ILogOutputChannel, IPathUtils } from './common/types'; import { noop } from './common/utils/misc'; import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; import { IDebugConfigurationService } from './debugger/extension/types'; @@ -55,7 +48,6 @@ import { registerTriggerForTerminalREPL } from './terminals/codeExecution/termin import { registerPythonStartup } from './terminals/pythonStartup'; import { registerPixiFeatures } from './pythonEnvironments/common/environmentManagers/pixi'; import { registerCustomTerminalLinkProvider } from './terminals/pythonStartupLinkProvider'; -import { registerEnvExtFeatures } from './envExt/api.internal'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -95,13 +87,9 @@ export function activateFeatures(ext: ExtensionState, _components: Components): const interpreterQuickPick: IInterpreterQuickPick = ext.legacyIOC.serviceContainer.get( IInterpreterQuickPick, ); - const interpreterPathService: IInterpreterPathService = ext.legacyIOC.serviceContainer.get( - IInterpreterPathService, - ); const interpreterService: IInterpreterService = ext.legacyIOC.serviceContainer.get( IInterpreterService, ); - registerEnvExtFeatures(ext.disposables, interpreterPathService); const pathUtils = ext.legacyIOC.serviceContainer.get(IPathUtils); registerPixiFeatures(ext.disposables); registerAllCreateEnvironmentFeatures( From c840796d4cf21ddd02085ed876231d48399209b7 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:51:04 -0800 Subject: [PATCH 1097/1136] Fix native repl not using env extension (#25763) Resolves: https://github.com/microsoft/vscode-python-environments/issues/992 We now also handle interpreter switch mid-repl session. --- python_files/vscode_pytest/__init__.py | 8 +-- src/client/repl/nativeRepl.ts | 73 ++++++++++++++++++++++++-- src/client/repl/pythonServer.ts | 57 +++++++++++++++----- src/client/repl/replCommands.ts | 6 +-- src/client/repl/replUtils.ts | 7 +-- src/test/repl/nativeRepl.test.ts | 2 + src/test/repl/replCommand.test.ts | 47 ++++++++++++++++- 7 files changed, 173 insertions(+), 27 deletions(-) diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 89565dab1264..cb0fcd69a00e 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -190,7 +190,7 @@ def pytest_exception_interact(node, call, report): send_execution_message( os.fsdecode(cwd), "success", - collected_test if collected_test else None, + collected_test or None, ) @@ -314,7 +314,7 @@ def pytest_report_teststatus(report, config): # noqa: ARG001 send_execution_message( os.fsdecode(cwd), "success", - collected_test if collected_test else None, + collected_test or None, ) yield @@ -348,7 +348,7 @@ def pytest_runtest_protocol(item, nextitem): # noqa: ARG001 send_execution_message( os.fsdecode(cwd), "success", - collected_test if collected_test else None, + collected_test or None, ) yield @@ -1024,7 +1024,7 @@ def get_node_path( except Exception as e: raise VSCodePytestError( f"Error occurred while calculating symlink equivalent from node path: {e}" - f"\n SYMLINK_PATH: {SYMLINK_PATH}, \n node path: {node_path}, \n cwd: {_CACHED_CWD if _CACHED_CWD else pathlib.Path.cwd()}" + f"\n SYMLINK_PATH: {SYMLINK_PATH}, \n node path: {node_path}, \n cwd: {_CACHED_CWD or pathlib.Path.cwd()}" ) from e else: result = node_path diff --git a/src/client/repl/nativeRepl.ts b/src/client/repl/nativeRepl.ts index 62e314172da6..3f8a085da467 100644 --- a/src/client/repl/nativeRepl.ts +++ b/src/client/repl/nativeRepl.ts @@ -4,9 +4,10 @@ // Native Repl class that holds instance of pythonServer and replController import { NotebookController, NotebookDocument, QuickPickItem, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import * as path from 'path'; import { Disposable } from 'vscode-jsonrpc'; import { PVSC_EXTENSION_ID } from '../common/constants'; -import { showQuickPick } from '../common/vscodeApis/windowApis'; +import { showNotebookDocument, showQuickPick } from '../common/vscodeApis/windowApis'; import { getWorkspaceFolders, onDidCloseNotebookDocument } from '../common/vscodeApis/workspaceApis'; import { PythonEnvironment } from '../pythonEnvironments/info'; import { createPythonServer, PythonServer } from './pythonServer'; @@ -18,6 +19,8 @@ import { VariablesProvider } from './variables/variablesProvider'; import { VariableRequester } from './variables/variableRequester'; import { getTabNameForUri } from './replUtils'; import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../common/persistentState'; +import { onDidChangeEnvironmentEnvExt, useEnvExtension } from '../envExt/api.internal'; +import { getActiveInterpreterLegacy } from '../envExt/api.legacy'; export const NATIVE_REPL_URI_MEMENTO = 'nativeReplUri'; let nativeRepl: NativeRepl | undefined; @@ -37,6 +40,10 @@ export class NativeRepl implements Disposable { public newReplSession: boolean | undefined = true; + private envChangeListenerRegistered = false; + + private pendingInterpreterChange?: { resource?: Uri }; + // TODO: In the future, could also have attribute of URI for file specific REPL. private constructor() { this.watchNotebookClosed(); @@ -48,7 +55,9 @@ export class NativeRepl implements Disposable { nativeRepl.interpreter = interpreter; await nativeRepl.setReplDirectory(); nativeRepl.pythonServer = createPythonServer([interpreter.path as string], nativeRepl.cwd); + nativeRepl.disposables.push(nativeRepl.pythonServer); nativeRepl.setReplController(); + nativeRepl.registerInterpreterChangeHandler(); return nativeRepl; } @@ -116,8 +125,8 @@ export class NativeRepl implements Disposable { /** * Function that check if NotebookController for REPL exists, and returns it in Singleton manner. */ - public setReplController(): NotebookController { - if (!this.replController) { + public setReplController(force: boolean = false): NotebookController { + if (!this.replController || force) { this.replController = createReplController(this.interpreter!.path, this.disposables, this.cwd); this.replController.variableProvider = new VariablesProvider( new VariableRequester(this.pythonServer), @@ -128,6 +137,64 @@ export class NativeRepl implements Disposable { return this.replController; } + private registerInterpreterChangeHandler(): void { + if (!useEnvExtension() || this.envChangeListenerRegistered) { + return; + } + this.envChangeListenerRegistered = true; + this.disposables.push( + onDidChangeEnvironmentEnvExt((event) => { + this.updateInterpreterForChange(event.uri).catch(() => undefined); + }), + ); + this.disposables.push( + this.pythonServer.onCodeExecuted(() => { + if (this.pendingInterpreterChange) { + const { resource } = this.pendingInterpreterChange; + this.pendingInterpreterChange = undefined; + this.updateInterpreterForChange(resource).catch(() => undefined); + } + }), + ); + } + + private async updateInterpreterForChange(resource?: Uri): Promise { + if (this.pythonServer?.isExecuting) { + this.pendingInterpreterChange = { resource }; + return; + } + if (!this.shouldApplyInterpreterChange(resource)) { + return; + } + const scope = resource ?? (this.cwd ? Uri.file(this.cwd) : undefined); + const interpreter = await getActiveInterpreterLegacy(scope); + if (!interpreter || interpreter.path === this.interpreter?.path) { + return; + } + + this.interpreter = interpreter; + this.pythonServer.dispose(); + this.pythonServer = createPythonServer([interpreter.path as string], this.cwd); + this.disposables.push(this.pythonServer); + if (this.replController) { + this.replController.dispose(); + } + this.setReplController(true); + + if (this.notebookDocument) { + const notebookEditor = await showNotebookDocument(this.notebookDocument, { preserveFocus: true }); + await selectNotebookKernel(notebookEditor, this.replController.id, PVSC_EXTENSION_ID); + } + } + + private shouldApplyInterpreterChange(resource?: Uri): boolean { + if (!resource || !this.cwd) { + return true; + } + const relative = path.relative(this.cwd, resource.fsPath); + return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)); + } + /** * Function that checks if native REPL's text input box contains complete code. * @returns Promise - True if complete/Valid code is present, False otherwise. diff --git a/src/client/repl/pythonServer.ts b/src/client/repl/pythonServer.ts index 74e2d6ae7251..c4b1722b5079 100644 --- a/src/client/repl/pythonServer.ts +++ b/src/client/repl/pythonServer.ts @@ -16,6 +16,8 @@ export interface ExecutionResult { export interface PythonServer extends Disposable { onCodeExecuted: Event; + readonly isExecuting: boolean; + readonly isDisposed: boolean; execute(code: string): Promise; executeSilently(code: string): Promise; interrupt(): void; @@ -30,6 +32,18 @@ class PythonServerImpl implements PythonServer, Disposable { onCodeExecuted = this._onCodeExecuted.event; + private inFlightRequests = 0; + + private disposed = false; + + public get isExecuting(): boolean { + return this.inFlightRequests > 0; + } + + public get isDisposed(): boolean { + return this.disposed; + } + constructor(private connection: rpc.MessageConnection, private pythonServer: ch.ChildProcess) { this.initialize(); this.input(); @@ -41,6 +55,14 @@ class PythonServerImpl implements PythonServer, Disposable { traceLog('Log:', message); }), ); + this.pythonServer.on('exit', (code) => { + traceError(`Python server exited with code ${code}`); + this.markDisposed(); + }); + this.pythonServer.on('error', (err) => { + traceError(err); + this.markDisposed(); + }); this.connection.listen(); } @@ -75,12 +97,15 @@ class PythonServerImpl implements PythonServer, Disposable { } private async executeCode(code: string): Promise { + this.inFlightRequests += 1; try { const result = await this.connection.sendRequest('execute', code); return result as ExecutionResult; } catch (err) { const error = err as Error; traceError(`Error getting response from REPL server:`, error); + } finally { + this.inFlightRequests -= 1; } return undefined; } @@ -93,39 +118,47 @@ class PythonServerImpl implements PythonServer, Disposable { } public async checkValidCommand(code: string): Promise { - const completeCode: ExecutionResult = await this.connection.sendRequest('check_valid_command', code); - if (completeCode.output === 'True') { - return new Promise((resolve) => resolve(true)); + this.inFlightRequests += 1; + try { + const completeCode: ExecutionResult = await this.connection.sendRequest('check_valid_command', code); + return completeCode.output === 'True'; + } finally { + this.inFlightRequests -= 1; } - return new Promise((resolve) => resolve(false)); } public dispose(): void { + if (this.disposed) { + return; + } + this.disposed = true; this.connection.sendNotification('exit'); this.disposables.forEach((d) => d.dispose()); this.connection.dispose(); serverInstance = undefined; } + + private markDisposed(): void { + if (this.disposed) { + return; + } + this.disposed = true; + this.connection.dispose(); + serverInstance = undefined; + } } export function createPythonServer(interpreter: string[], cwd?: string): PythonServer { - if (serverInstance) { + if (serverInstance && !serverInstance.isDisposed) { return serverInstance; } const pythonServer = ch.spawn(interpreter[0], [...interpreter.slice(1), SERVER_PATH], { cwd, // Launch with correct workspace directory }); - pythonServer.stderr.on('data', (data) => { traceError(data.toString()); }); - pythonServer.on('exit', (code) => { - traceError(`Python server exited with code ${code}`); - }); - pythonServer.on('error', (err) => { - traceError(err); - }); const connection = rpc.createMessageConnection( new rpc.StreamMessageReader(pythonServer.stdout), new rpc.StreamMessageWriter(pythonServer.stdin), diff --git a/src/client/repl/replCommands.ts b/src/client/repl/replCommands.ts index 993a0cc91b19..1171e9466ee8 100644 --- a/src/client/repl/replCommands.ts +++ b/src/client/repl/replCommands.ts @@ -2,7 +2,6 @@ import { commands, Uri, window } from 'vscode'; import { Disposable } from 'vscode-jsonrpc'; import { ICommandManager } from '../common/application/types'; import { Commands } from '../common/constants'; -import { noop } from '../common/utils/misc'; import { IInterpreterService } from '../interpreter/contracts'; import { ICodeExecutionHelper } from '../terminals/types'; import { getNativeRepl } from './nativeRepl'; @@ -102,14 +101,13 @@ export async function registerReplExecuteOnEnter( } async function onInputEnter( - uri: Uri, + uri: Uri | undefined, commandName: string, interpreterService: IInterpreterService, disposables: Disposable[], ): Promise { - const interpreter = await interpreterService.getActiveInterpreter(uri); + const interpreter = await getActiveInterpreter(uri, interpreterService); if (!interpreter) { - commands.executeCommand(Commands.TriggerEnvironmentSelection, uri).then(noop, noop); return; } diff --git a/src/client/repl/replUtils.ts b/src/client/repl/replUtils.ts index 8e23218c2870..93ae6f2a4573 100644 --- a/src/client/repl/replUtils.ts +++ b/src/client/repl/replUtils.ts @@ -66,12 +66,13 @@ export function isMultiLineText(textEditor: TextEditor): boolean { * Function will also return undefined or active interpreter */ export async function getActiveInterpreter( - uri: Uri, + uri: Uri | undefined, interpreterService: IInterpreterService, ): Promise { - const interpreter = await interpreterService.getActiveInterpreter(uri); + const resource = uri ?? getActiveResource(); + const interpreter = await interpreterService.getActiveInterpreter(resource); if (!interpreter) { - commands.executeCommand(Commands.TriggerEnvironmentSelection, uri).then(noop, noop); + commands.executeCommand(Commands.TriggerEnvironmentSelection, resource).then(noop, noop); return undefined; } return interpreter; diff --git a/src/test/repl/nativeRepl.test.ts b/src/test/repl/nativeRepl.test.ts index c05bb311a839..2cf18cefe1f7 100644 --- a/src/test/repl/nativeRepl.test.ts +++ b/src/test/repl/nativeRepl.test.ts @@ -129,6 +129,8 @@ suite('REPL - Native REPL', () => { input: sinon.stub(), checkValidCommand: sinon.stub().resolves(true), dispose: sinon.stub(), + isExecuting: false, + isDisposed: false, }; // Track the number of times createPythonServer was called diff --git a/src/test/repl/replCommand.test.ts b/src/test/repl/replCommand.test.ts index 7c26ebd69c80..0b5edda863f9 100644 --- a/src/test/repl/replCommand.test.ts +++ b/src/test/repl/replCommand.test.ts @@ -1,6 +1,6 @@ // Create test suite and test cases for the `replUtils` module import * as TypeMoq from 'typemoq'; -import { Disposable } from 'vscode'; +import { commands, Disposable, Uri } from 'vscode'; import * as sinon from 'sinon'; import { expect } from 'chai'; import { IInterpreterService } from '../../client/interpreter/contracts'; @@ -9,6 +9,7 @@ import { ICodeExecutionHelper } from '../../client/terminals/types'; import * as replCommands from '../../client/repl/replCommands'; import * as replUtils from '../../client/repl/replUtils'; import * as nativeRepl from '../../client/repl/nativeRepl'; +import * as windowApis from '../../client/common/vscodeApis/windowApis'; import { Commands } from '../../client/common/constants'; import { PythonEnvironment } from '../../client/pythonEnvironments/info'; @@ -203,3 +204,47 @@ suite('REPL - register native repl command', () => { sinon.assert.notCalled(getNativeReplStub); }); }); + +suite('Native REPL getActiveInterpreter', () => { + let interpreterService: TypeMoq.IMock; + let executeCommandStub: sinon.SinonStub; + let getActiveResourceStub: sinon.SinonStub; + + setup(() => { + interpreterService = TypeMoq.Mock.ofType(); + executeCommandStub = sinon.stub(commands, 'executeCommand').resolves(undefined); + getActiveResourceStub = sinon.stub(windowApis, 'getActiveResource'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Uses active resource when uri is undefined', async () => { + const resource = Uri.file('/workspace/app.py'); + const expected = ({ path: 'ps' } as unknown) as PythonEnvironment; + getActiveResourceStub.returns(resource); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(expected)); + + const result = await replUtils.getActiveInterpreter(undefined, interpreterService.object); + + expect(result).to.equal(expected); + interpreterService.verify((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource)), TypeMoq.Times.once()); + sinon.assert.notCalled(executeCommandStub); + }); + + test('Triggers environment selection using active resource when interpreter is missing', async () => { + const resource = Uri.file('/workspace/app.py'); + getActiveResourceStub.returns(resource); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(undefined)); + + const result = await replUtils.getActiveInterpreter(undefined, interpreterService.object); + + expect(result).to.equal(undefined); + sinon.assert.calledWith(executeCommandStub, Commands.TriggerEnvironmentSelection, resource); + }); +}); From bd9b4122f19ba652bbd50f4c92630d26bd160ad5 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 6 Feb 2026 13:25:56 -0800 Subject: [PATCH 1098/1136] Add ai-artifacts to .gitignore to exclude generated files (#25770) This adds a place within the github workspace that developers can store any AI related artifacts they create that could be useful but they don't want to commit. Things like issue summarization, plans for features, code analysis etc --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1b47f15705bb..2fa056f84fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,5 @@ python_files/tests/*/.data/.coverage* python_files/tests/*/.data/*/.coverage* src/testTestingRootWkspc/coverageWorkspace/.coverage +# ignore ai artifacts generated and placed in this folder +ai-artifacts/* From f3a6dbe3dd56550679b2ccb960e706f831098ccf Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:02:07 -0800 Subject: [PATCH 1099/1136] Refactor error handling to extract missing module names from error messages (#25785) fixes https://github.com/microsoft/vscode-python/issues/25786 --- .../testing/testController/common/utils.ts | 37 +++++++----- .../common/buildErrorNodeOptions.unit.test.ts | 56 +++++++++++++++---- 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 606865e5ad7e..7c2a3da42696 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -175,27 +175,36 @@ export async function startDiscoveryNamedPipe( } /** - * Detects if an error message indicates that pytest is not installed. - * @param message The error message to check - * @returns True if the error indicates pytest is not installed + * Extracts the missing module name from a ModuleNotFoundError or ImportError message. + * @param message The error message to parse + * @returns The module name if found, undefined otherwise */ -function isPytestNotInstalledError(message: string): boolean { - return ( - (message.includes('ModuleNotFoundError') && message.includes('pytest')) || - (message.includes('No module named') && message.includes('pytest')) || - (message.includes('ImportError') && message.includes('pytest')) - ); +function extractMissingModuleName(message: string): string | undefined { + // Match patterns like: + // - No module named 'requests' + // - No module named "requests" + // - ModuleNotFoundError: No module named 'requests' + // - ImportError: No module named requests + const patterns = [/No module named ['"]([^'"]+)['"]/, /No module named (\S+)/]; + + for (const pattern of patterns) { + const match = message.match(pattern); + if (match) { + return match[1]; + } + } + return undefined; } export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { let labelText = testType === 'pytest' ? 'pytest Discovery Error' : 'Unittest Discovery Error'; let errorMessage = message; - // Provide more specific error message if pytest is not installed - if (testType === 'pytest' && isPytestNotInstalledError(message)) { - labelText = 'pytest Not Installed'; - errorMessage = - 'pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.'; + // Check for missing module errors and provide specific messaging + const missingModule = extractMissingModuleName(message); + if (missingModule) { + labelText = `Missing Module: ${missingModule}`; + errorMessage = `The module '${missingModule}' is not installed in the selected Python environment. Please install it to enable test discovery.`; } return { diff --git a/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts index cf41136db697..e2133f5c767b 100644 --- a/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts +++ b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts @@ -5,33 +5,56 @@ import { expect } from 'chai'; import { Uri } from 'vscode'; import { buildErrorNodeOptions } from '../../../../client/testing/testController/common/utils'; -suite('buildErrorNodeOptions - pytest not installed detection', () => { +suite('buildErrorNodeOptions - missing module detection', () => { const workspaceUri = Uri.file('/test/workspace'); - test('Should detect pytest ModuleNotFoundError and provide specific message', () => { + test('Should detect pytest ModuleNotFoundError and show missing module label', () => { const errorMessage = 'Traceback (most recent call last):\n File "", line 1, in \n import pytest\nModuleNotFoundError: No module named \'pytest\''; const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); - expect(result.label).to.equal('pytest Not Installed [workspace]'); + expect(result.label).to.equal('Missing Module: pytest [workspace]'); expect(result.error).to.equal( - 'pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.', + "The module 'pytest' is not installed in the selected Python environment. Please install it to enable test discovery.", ); }); - test('Should detect pytest ImportError and provide specific message', () => { + test('Should detect pytest ImportError and show missing module label', () => { const errorMessage = 'ImportError: No module named pytest'; const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); - expect(result.label).to.equal('pytest Not Installed [workspace]'); + expect(result.label).to.equal('Missing Module: pytest [workspace]'); expect(result.error).to.equal( - 'pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.', + "The module 'pytest' is not installed in the selected Python environment. Please install it to enable test discovery.", ); }); - test('Should use generic error for non-pytest-related errors', () => { + test('Should detect other missing modules and show module name in label', () => { + const errorMessage = + "bob\\test_bob.py:3: in \n import requests\nE ModuleNotFoundError: No module named 'requests'\n=========================== short test summary info"; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('Missing Module: requests [workspace]'); + expect(result.error).to.equal( + "The module 'requests' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should detect missing module with double quotes', () => { + const errorMessage = 'ModuleNotFoundError: No module named "numpy"'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('Missing Module: numpy [workspace]'); + expect(result.error).to.equal( + "The module 'numpy' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should use generic error for non-module-related errors', () => { const errorMessage = 'Some other error occurred'; const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); @@ -40,12 +63,23 @@ suite('buildErrorNodeOptions - pytest not installed detection', () => { expect(result.error).to.equal('Some other error occurred'); }); - test('Should use generic error for unittest errors', () => { - const errorMessage = "ModuleNotFoundError: No module named 'pytest'"; + test('Should detect missing module for unittest errors', () => { + const errorMessage = "ModuleNotFoundError: No module named 'pandas'"; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest'); + + expect(result.label).to.equal('Missing Module: pandas [workspace]'); + expect(result.error).to.equal( + "The module 'pandas' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should use generic error for unittest non-module errors', () => { + const errorMessage = 'Some other error occurred'; const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest'); expect(result.label).to.equal('Unittest Discovery Error [workspace]'); - expect(result.error).to.equal("ModuleNotFoundError: No module named 'pytest'"); + expect(result.error).to.equal('Some other error occurred'); }); }); From 85d920203a275d2d1a90a503bddb83b8f74999c9 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:18:16 -0800 Subject: [PATCH 1100/1136] Fix env var warning description (#25758) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves: https://github.com/microsoft/vscode-python/issues/25733 Now description show when contributing.. but not when deleting? See below Screenshot 2026-02-02 at 4 26 05 PM --- src/client/common/utils/localize.ts | 6 ++++++ src/client/terminals/pythonStartup.ts | 11 +++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index a084fc647025..d108dfddb54b 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -199,6 +199,12 @@ export namespace Interpreters { export const terminalEnvVarCollectionPrompt = l10n.t( '{0} environment was successfully activated, even though {1} indicator may not be present in the terminal prompt. [Learn more](https://aka.ms/vscodePythonTerminalActivation).', ); + export const shellIntegrationEnvVarCollectionDescription = l10n.t( + 'Enables `python.terminal.shellIntegration.enabled` by modifying `PYTHONSTARTUP` and `PYTHON_BASIC_REPL`', + ); + export const shellIntegrationDisabledEnvVarCollectionDescription = l10n.t( + 'Disables `python.terminal.shellIntegration.enabled` by unsetting `PYTHONSTARTUP` and `PYTHON_BASIC_REPL`', + ); export const terminalDeactivateProgress = l10n.t('Editing {0}...'); export const restartingTerminal = l10n.t('Restarting terminal and deactivating...'); export const terminalDeactivatePrompt = l10n.t( diff --git a/src/client/terminals/pythonStartup.ts b/src/client/terminals/pythonStartup.ts index f0c3bf89c3b4..b6f68c860b46 100644 --- a/src/client/terminals/pythonStartup.ts +++ b/src/client/terminals/pythonStartup.ts @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ExtensionContext, Uri } from 'vscode'; +import { ExtensionContext, MarkdownString, Uri } from 'vscode'; import * as path from 'path'; import { copy, createDirectory, getConfiguration, onDidChangeConfiguration } from '../common/vscodeApis/workspaceApis'; import { EXTENSION_ROOT_DIR } from '../constants'; +import { Interpreters } from '../common/utils/localize'; async function applyPythonStartupSetting(context: ExtensionContext): Promise { const config = getConfiguration('python'); @@ -21,11 +22,17 @@ async function applyPythonStartupSetting(context: ExtensionContext): Promise Date: Tue, 10 Feb 2026 13:05:50 -0800 Subject: [PATCH 1101/1136] Add support for Projects in Testing (#25780) fixes https://github.com/microsoft/vscode-python-environments/issues/987 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../testing-workflow.instructions.md | 1 + .../testing_feature_area.instructions.md | 76 ++ .github/workflows/pr-check.yml | 2 + .../expected_discovery_test_output.py | 211 +++++ .../tests/pytestadapter/test_discovery.py | 94 +++ .../tests/unittestadapter/test_discovery.py | 120 +++ .../tests/unittestadapter/test_execution.py | 131 ++++ python_files/unittestadapter/discovery.py | 26 +- python_files/unittestadapter/execution.py | 35 +- python_files/vscode_pytest/__init__.py | 42 +- src/client/testing/common/debugLauncher.ts | 143 +++- src/client/testing/common/types.ts | 3 + .../testController/common/projectAdapter.ts | 88 +++ .../common/projectTestExecution.ts | 296 +++++++ .../testController/common/projectUtils.ts | 91 +++ .../testController/common/resultResolver.ts | 35 +- .../common/testDiscoveryHandler.ts | 49 +- .../common/testProjectRegistry.ts | 330 ++++++++ .../testing/testController/common/types.ts | 13 +- .../testing/testController/common/utils.ts | 45 +- .../testing/testController/controller.ts | 497 ++++++++++-- .../pytest/pytestDiscoveryAdapter.ts | 21 +- .../pytest/pytestExecutionAdapter.ts | 18 +- .../unittest/testDiscoveryAdapter.ts | 12 +- .../unittest/testExecutionAdapter.ts | 19 +- .../testController/workspaceTestAdapter.ts | 3 + .../testing/common/debugLauncher.unit.test.ts | 276 ++++++- .../common/buildErrorNodeOptions.unit.test.ts | 26 + .../common/projectTestExecution.unit.test.ts | 740 ++++++++++++++++++ .../common/projectUtils.unit.test.ts | 241 ++++++ .../common/testProjectRegistry.unit.test.ts | 440 +++++++++++ .../testController/controller.unit.test.ts | 344 ++++++++ .../pytestExecutionAdapter.unit.test.ts | 207 +++++ src/test/testing/testController/testMocks.ts | 152 ++++ .../testDiscoveryAdapter.unit.test.ts | 95 +++ .../testExecutionAdapter.unit.test.ts | 257 ++++++ .../testing/testController/utils.unit.test.ts | 21 +- src/test/vscode-mock.ts | 27 + 38 files changed, 5052 insertions(+), 175 deletions(-) create mode 100644 src/client/testing/testController/common/projectAdapter.ts create mode 100644 src/client/testing/testController/common/projectTestExecution.ts create mode 100644 src/client/testing/testController/common/projectUtils.ts create mode 100644 src/client/testing/testController/common/testProjectRegistry.ts create mode 100644 src/test/testing/testController/common/projectTestExecution.unit.test.ts create mode 100644 src/test/testing/testController/common/projectUtils.unit.test.ts create mode 100644 src/test/testing/testController/common/testProjectRegistry.unit.test.ts create mode 100644 src/test/testing/testController/controller.unit.test.ts create mode 100644 src/test/testing/testController/testMocks.ts diff --git a/.github/instructions/testing-workflow.instructions.md b/.github/instructions/testing-workflow.instructions.md index 948886a59635..844946404328 100644 --- a/.github/instructions/testing-workflow.instructions.md +++ b/.github/instructions/testing-workflow.instructions.md @@ -578,3 +578,4 @@ envConfig.inspect - When mocking `testController.createTestItem()` in unit tests, use `typemoq.It.isAny()` for parameters when testing handler behavior (not ID/label generation logic), but consider using specific matchers (e.g., `It.is((id: string) => id.startsWith('_error_'))`) when the actual values being passed are important for correctness - this balances test precision with maintainability (2) - Remove unused variables from test code immediately - leftover tracking variables like `validationCallCount` that aren't referenced indicate dead code that should be simplified (1) - Use `Uri.file(path).fsPath` for both sides of path comparisons in tests to ensure cross-platform compatibility - Windows converts forward slashes to backslashes automatically (1) +- When tests fail with "Cannot stub non-existent property", the method likely moved to a different class during refactoring - find the class that owns the method and test that class directly instead of stubbing on the original class (1) diff --git a/.github/instructions/testing_feature_area.instructions.md b/.github/instructions/testing_feature_area.instructions.md index 038dc1025ea5..a4e11523d7c8 100644 --- a/.github/instructions/testing_feature_area.instructions.md +++ b/.github/instructions/testing_feature_area.instructions.md @@ -26,6 +26,10 @@ This document maps the testing support in the extension: discovery, execution (r - `src/client/testing/serviceRegistry.ts` — DI/wiring for testing services. - Workspace orchestration - `src/client/testing/testController/workspaceTestAdapter.ts` — `WorkspaceTestAdapter` (provider-agnostic entry used by controller). +- **Project-based testing (multi-project workspaces)** + - `src/client/testing/testController/common/testProjectRegistry.ts` — `TestProjectRegistry` (manages project lifecycle, discovery, and nested project handling). + - `src/client/testing/testController/common/projectAdapter.ts` — `ProjectAdapter` interface (represents a single Python project with its own test infrastructure). + - `src/client/testing/testController/common/projectUtils.ts` — utilities for project ID generation, display names, and shared adapter creation. - Provider adapters - Unittest - `src/client/testing/testController/unittest/testDiscoveryAdapter.ts` @@ -151,6 +155,78 @@ The adapters in the extension don't implement test discovery/run logic themselve - Settings are consumed by `src/client/testing/common/testConfigurationManager.ts`, `src/client/testing/configurationFactory.ts`, and adapters under `src/client/testing/testController/*` which read settings to build CLI args and env for subprocesses. - The setting definitions and descriptions are in `package.json` and localized strings in `package.nls.json`. +## Project-based testing (multi-project workspaces) + +Project-based testing enables multi-project workspace support where each Python project gets its own test tree root with its own Python environment. + +### Architecture + +- **TestProjectRegistry** (`testProjectRegistry.ts`): Central registry that: + + - Discovers Python projects via the Python Environments API + - Creates and manages `ProjectAdapter` instances per workspace + - Computes nested project relationships and configures ignore lists + - Falls back to "legacy" single-adapter mode when API unavailable + +- **ProjectAdapter** (`projectAdapter.ts`): Interface representing a single project with: + - Project identity (ID, name, URI from Python Environments API) + - Python environment with execution details + - Test framework adapters (discovery/execution) + - Nested project ignore paths (for parent projects) + +### How it works + +1. **Activation**: When the extension activates, `PythonTestController` checks if the Python Environments API is available. +2. **Project discovery**: `TestProjectRegistry.discoverAndRegisterProjects()` queries the API for all Python projects in each workspace. +3. **Nested handling**: `configureNestedProjectIgnores()` identifies child projects and adds their paths to parent projects' ignore lists. +4. **Test discovery**: For each project, the controller calls `project.discoveryAdapter.discoverTests()` with the project's URI. The adapter sets `PROJECT_ROOT_PATH` environment variable for the Python runner. +5. **Python side**: + - For pytest: `get_test_root_path()` in `vscode_pytest/__init__.py` returns `PROJECT_ROOT_PATH` (if set) or falls back to `cwd`. + - For unittest: `discovery.py` uses `PROJECT_ROOT_PATH` as `top_level_dir` and `project_root_path` to root the test tree at the project directory. +6. **Test tree**: Each project gets its own root node in the Test Explorer, with test IDs scoped by project ID using the `@@vsc@@` separator (defined in `projectUtils.ts`). + +### Nested project handling: pytest vs unittest + +**pytest** supports the `--ignore` flag to exclude paths during test collection. When nested projects are detected, parent projects automatically receive `--ignore` flags for child project paths. This ensures each test appears under exactly one project in the test tree. + +**unittest** does not support path exclusion during `discover()`. Therefore, tests in nested project directories may appear under multiple project roots (both the parent and the child project). This is **expected behavior** for unittest: + +- Each project discovers and displays all tests it finds within its directory structure +- There is no deduplication or collision detection +- Users may see the same test file under multiple project roots if their project structure has nesting + +This approach was chosen because: + +1. unittest's `TestLoader.discover()` has no built-in path exclusion mechanism +2. Implementing custom exclusion would add significant complexity with minimal benefit +3. The existing approach is transparent and predictable - each project shows what it finds + +### Empty projects and root nodes + +If a project discovers zero tests, its root node will still appear in the Test Explorer as an empty folder. This ensures consistent behavior and makes it clear which projects were discovered, even if they have no tests yet. + +### Logging prefix + +All project-based testing logs use the `[test-by-project]` prefix for easy filtering in the output channel. + +### Key files + +- Python side: + - `python_files/vscode_pytest/__init__.py` — `get_test_root_path()` function and `PROJECT_ROOT_PATH` environment variable for pytest. + - `python_files/unittestadapter/discovery.py` — `discover_tests()` with `project_root_path` parameter and `PROJECT_ROOT_PATH` handling for unittest discovery. + - `python_files/unittestadapter/execution.py` — `run_tests()` with `project_root_path` parameter and `PROJECT_ROOT_PATH` handling for unittest execution. +- TypeScript: `testProjectRegistry.ts`, `projectAdapter.ts`, `projectUtils.ts`, and the discovery/execution adapters. + +### Tests + +- `src/test/testing/testController/common/testProjectRegistry.unit.test.ts` — TestProjectRegistry tests +- `src/test/testing/testController/common/projectUtils.unit.test.ts` — Project utility function tests +- `python_files/tests/pytestadapter/test_discovery.py` — pytest PROJECT_ROOT_PATH tests (see `test_project_root_path_env_var()` and `test_symlink_with_project_root_path()`) +- `python_files/tests/unittestadapter/test_discovery.py` — unittest `project_root_path` / PROJECT_ROOT_PATH discovery tests +- `python_files/tests/unittestadapter/test_execution.py` — unittest `project_root_path` / PROJECT_ROOT_PATH execution tests +- `src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts` — unittest discovery adapter PROJECT_ROOT_PATH tests +- `src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts` — unittest execution adapter PROJECT_ROOT_PATH tests + ## Coverage support (how it works) - Coverage is supported by running the Python helper scripts with coverage enabled and then collecting a coverage payload from the runner. diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index d7d8d3869505..95024788d915 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -477,6 +477,8 @@ jobs: ### Coverage run coverage: name: Coverage + # TEMPORARILY DISABLED - hanging in CI, needs investigation + if: false # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. runs-on: ${{ matrix.os }} needs: [lint, check-types, python-tests, tests, native-tests] diff --git a/python_files/tests/pytestadapter/expected_discovery_test_output.py b/python_files/tests/pytestadapter/expected_discovery_test_output.py index b6f0779cf982..047f1c72ad17 100644 --- a/python_files/tests/pytestadapter/expected_discovery_test_output.py +++ b/python_files/tests/pytestadapter/expected_discovery_test_output.py @@ -1870,3 +1870,214 @@ ], "id_": TEST_DATA_PATH_STR, } + +# ===================================================================================== +# PROJECT_ROOT_PATH environment variable tests +# These test the project-based testing feature where PROJECT_ROOT_PATH changes +# the test tree root from cwd to the specified project path. +# ===================================================================================== + +# This is the expected output for unittest_folder when PROJECT_ROOT_PATH is set to unittest_folder. +# The root of the tree is unittest_folder (not .data), simulating project-based testing. +# +# **Project Configuration:** +# In the VS Code Python extension, projects are defined by the Python Environments extension. +# Each project has a root directory (identified by pyproject.toml, setup.py, etc.). +# When PROJECT_ROOT_PATH is set, pytest uses that path as the test tree root instead of cwd. +# +# **Test Tree Structure:** +# Without PROJECT_ROOT_PATH (legacy mode): +# └── .data (cwd = workspace root) +# └── unittest_folder +# └── test_add.py, test_subtract.py... +# +# With PROJECT_ROOT_PATH set to unittest_folder (project-based mode): +# └── unittest_folder (ROOT - set via PROJECT_ROOT_PATH env var) +# ├── test_add.py +# │ └── TestAddFunction +# │ ├── test_add_negative_numbers +# │ └── test_add_positive_numbers +# │ └── TestDuplicateFunction +# │ └── test_dup_a +# └── test_subtract.py +# └── TestSubtractFunction +# ├── test_subtract_negative_numbers +# └── test_subtract_positive_numbers +# └── TestDuplicateFunction +# └── test_dup_s +# +# Note: This reuses the unittest_folder paths defined earlier in this file. +project_root_unittest_folder_expected_output = { + "name": "unittest_folder", + "path": os.fspath(unittest_folder_path), + "type_": "folder", + "children": [ + { + "name": "test_add.py", + "path": os.fspath(test_add_path), + "type_": "file", + "id_": os.fspath(test_add_path), + "children": [ + { + "name": "TestAddFunction", + "path": os.fspath(test_add_path), + "type_": "class", + "children": [ + { + "name": "test_add_negative_numbers", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_add_negative_numbers", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), + }, + { + "name": "test_add_positive_numbers", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_add_positive_numbers", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), + }, + ], + "id_": get_absolute_test_id( + "test_add.py::TestAddFunction", + test_add_path, + ), + "lineno": find_class_line_number("TestAddFunction", test_add_path), + }, + { + "name": "TestDuplicateFunction", + "path": os.fspath(test_add_path), + "type_": "class", + "children": [ + { + "name": "test_dup_a", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_dup_a", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), + "runID": get_absolute_test_id( + "test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), + }, + ], + "id_": get_absolute_test_id( + "test_add.py::TestDuplicateFunction", + test_add_path, + ), + "lineno": find_class_line_number("TestDuplicateFunction", test_add_path), + }, + ], + }, + { + "name": "test_subtract.py", + "path": os.fspath(test_subtract_path), + "type_": "file", + "id_": os.fspath(test_subtract_path), + "children": [ + { + "name": "TestSubtractFunction", + "path": os.fspath(test_subtract_path), + "type_": "class", + "children": [ + { + "name": "test_subtract_negative_numbers", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_subtract_negative_numbers", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), + }, + { + "name": "test_subtract_positive_numbers", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_subtract_positive_numbers", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), + }, + ], + "id_": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction", + test_subtract_path, + ), + "lineno": find_class_line_number("TestSubtractFunction", test_subtract_path), + }, + { + "name": "TestDuplicateFunction", + "path": os.fspath(test_subtract_path), + "type_": "class", + "children": [ + { + "name": "test_dup_s", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_dup_s", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), + }, + ], + "id_": get_absolute_test_id( + "test_subtract.py::TestDuplicateFunction", + test_subtract_path, + ), + "lineno": find_class_line_number("TestDuplicateFunction", test_subtract_path), + }, + ], + }, + ], + "id_": os.fspath(unittest_folder_path), +} diff --git a/python_files/tests/pytestadapter/test_discovery.py b/python_files/tests/pytestadapter/test_discovery.py index 842ee3c7c707..cf777399fed9 100644 --- a/python_files/tests/pytestadapter/test_discovery.py +++ b/python_files/tests/pytestadapter/test_discovery.py @@ -386,3 +386,97 @@ def test_plugin_collect(file, expected_const, extra_arg): ), ( f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_const, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" ) + + +def test_project_root_path_env_var(): + """Test pytest discovery with PROJECT_ROOT_PATH environment variable set. + + This simulates project-based testing where the test tree root should be + the project root (PROJECT_ROOT_PATH) rather than the workspace cwd. + + When PROJECT_ROOT_PATH is set: + - The test tree root (name, path, id_) should match PROJECT_ROOT_PATH + - The cwd in the response should match PROJECT_ROOT_PATH + - Test files should be direct children of the root (not nested under a subfolder) + """ + # Use unittest_folder as our "project" subdirectory + project_path = helpers.TEST_DATA_PATH / "unittest_folder" + + actual = helpers.runner_with_cwd_env( + [os.fspath(project_path), "--collect-only"], + helpers.TEST_DATA_PATH, # cwd is parent of project + {"PROJECT_ROOT_PATH": os.fspath(project_path)}, # Set project root + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) + # cwd in response should be PROJECT_ROOT_PATH + assert actual_item.get("cwd") == os.fspath(project_path), ( + f"Expected cwd '{os.fspath(project_path)}', got '{actual_item.get('cwd')}'" + ) + assert is_same_tree( + actual_item.get("tests"), + expected_discovery_test_output.project_root_unittest_folder_expected_output, + ["id_", "lineno", "name", "runID"], + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.project_root_unittest_folder_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="Symlinks require elevated privileges on Windows", +) +def test_symlink_with_project_root_path(): + """Test pytest discovery with both symlink and PROJECT_ROOT_PATH set. + + This tests the combination of: + 1. A symlinked test directory (--rootdir points to symlink) + 2. PROJECT_ROOT_PATH set to the symlink path + + This simulates project-based testing where the project root is a symlink, + ensuring test IDs and paths are correctly resolved through the symlink. + """ + with helpers.create_symlink(helpers.TEST_DATA_PATH, "root", "symlink_folder") as ( + source, + destination, + ): + assert destination.is_symlink() + + # Run pytest with: + # - cwd being the resolved symlink path (simulating subprocess from node) + # - PROJECT_ROOT_PATH set to the symlink destination + actual = helpers.runner_with_cwd_env( + ["--collect-only", f"--rootdir={os.fspath(destination)}"], + source, # cwd is the resolved (non-symlink) path + {"PROJECT_ROOT_PATH": os.fspath(destination)}, # Project root is the symlink + ) + + expected = expected_discovery_test_output.symlink_expected_discovery_output + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + try: + assert all(item in actual_item for item in ("status", "cwd", "error")), ( + "Required keys are missing" + ) + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) + # cwd should be the PROJECT_ROOT_PATH (the symlink destination) + assert actual_item.get("cwd") == os.fspath(destination), ( + f"CWD does not match symlink path: expected {os.fspath(destination)}, got {actual_item.get('cwd')}" + ) + assert actual_item.get("tests") == expected, "Tests do not match expected value" + except AssertionError as e: + # Print the actual_item in JSON format if an assertion fails + print(json.dumps(actual_item, indent=4)) + pytest.fail(str(e)) diff --git a/python_files/tests/unittestadapter/test_discovery.py b/python_files/tests/unittestadapter/test_discovery.py index a10b5c406680..ab028ef176c3 100644 --- a/python_files/tests/unittestadapter/test_discovery.py +++ b/python_files/tests/unittestadapter/test_discovery.py @@ -325,3 +325,123 @@ def test_simple_django_collect(): assert ( len(actual_item["tests"]["children"][0]["children"][0]["children"][0]["children"]) == 3 ) + + +def test_project_root_path_with_cwd_override() -> None: + """Test unittest discovery with project_root_path parameter. + + This simulates project-based testing where the cwd in the payload should be + the project root (project_root_path) rather than the start_dir. + + When project_root_path is provided: + - The cwd in the response should match project_root_path + - The test tree root should still be built correctly based on top_level_dir + """ + # Use unittest_skip folder as our "project" directory + project_path = TEST_DATA_PATH / "unittest_skip" + start_dir = os.fsdecode(project_path) + pattern = "unittest_*" + + # Call discover_tests with project_root_path to simulate PROJECT_ROOT_PATH + actual = discover_tests(start_dir, pattern, None, project_root_path=start_dir) + + assert actual["status"] == "success" + # cwd in response should match the project_root_path (project root) + assert actual["cwd"] == os.fsdecode(project_path), ( + f"Expected cwd '{os.fsdecode(project_path)}', got '{actual['cwd']}'" + ) + assert "tests" in actual + # Verify the test tree structure matches expected output + assert is_same_tree( + actual.get("tests"), + expected_discovery_test_output.skip_unittest_folder_discovery_output, + ["id_", "lineno", "name"], + ) + assert "error" not in actual + + +def test_project_root_path_with_different_cwd_and_start_dir() -> None: + """Test unittest discovery where project_root_path differs from start_dir. + + This simulates the scenario where: + - start_dir points to a subfolder where tests are located + - project_root_path (PROJECT_ROOT_PATH) points to the project root + + The cwd in the response should be the project root, while discovery + still runs from the start_dir. + """ + # Use utils_complex_tree as our test case - discovery from a subfolder + project_path = TEST_DATA_PATH / "utils_complex_tree" + start_dir = os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + ) + ) + pattern = "test_*.py" + top_level_dir = os.fsdecode(project_path) + + # Call discover_tests with project_root_path set to project root + actual = discover_tests(start_dir, pattern, top_level_dir, project_root_path=top_level_dir) + + assert actual["status"] == "success" + # cwd should be the project root (project_root_path), not the start_dir + assert actual["cwd"] == os.fsdecode(project_path), ( + f"Expected cwd '{os.fsdecode(project_path)}', got '{actual['cwd']}'" + ) + assert "error" not in actual + # Test tree should still be structured correctly with top_level_dir as root + assert is_same_tree( + actual.get("tests"), + expected_discovery_test_output.complex_tree_expected_output, + ["id_", "lineno", "name"], + ) + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="Symlinks require elevated privileges on Windows", +) +def test_symlink_with_project_root_path() -> None: + """Test unittest discovery with both symlink and PROJECT_ROOT_PATH set. + + This tests the combination of: + 1. A symlinked test directory + 2. project_root_path (PROJECT_ROOT_PATH) set to the symlink path + + This simulates project-based testing where the project root is a symlink, + ensuring test IDs and paths are correctly resolved through the symlink. + """ + with helpers.create_symlink(TEST_DATA_PATH, "unittest_skip", "symlink_unittest") as ( + _source, + destination, + ): + assert destination.is_symlink() + + # Run discovery with: + # - start_dir pointing to the symlink destination + # - project_root_path set to the symlink destination (simulating PROJECT_ROOT_PATH) + start_dir = os.fsdecode(destination) + pattern = "unittest_*" + + actual = discover_tests(start_dir, pattern, None, project_root_path=start_dir) + + assert actual["status"] == "success", ( + f"Status is not 'success', error is: {actual.get('error')}" + ) + # cwd should be the symlink path (project_root_path) + assert actual["cwd"] == os.fsdecode(destination), ( + f"CWD does not match symlink path: expected {os.fsdecode(destination)}, got {actual['cwd']}" + ) + assert "tests" in actual + assert actual["tests"] is not None + # The test tree root should be named after the symlink directory + assert actual["tests"]["name"] == "symlink_unittest", ( + f"Expected root name 'symlink_unittest', got '{actual['tests']['name']}'" + ) + # The test tree root path should use the symlink path + assert actual["tests"]["path"] == os.fsdecode(destination), ( + f"Expected root path to be symlink, got '{actual['tests']['path']}'" + ) diff --git a/python_files/tests/unittestadapter/test_execution.py b/python_files/tests/unittestadapter/test_execution.py index f369c6d770b0..cab03f0b5dc4 100644 --- a/python_files/tests/unittestadapter/test_execution.py +++ b/python_files/tests/unittestadapter/test_execution.py @@ -341,3 +341,134 @@ def test_basic_run_django(): assert id_result["outcome"] == "failure" else: assert id_result["outcome"] == "success" + + +def test_project_root_path_with_cwd_override(mock_send_run_data) -> None: # noqa: ARG001 + """Test unittest execution with project_root_path parameter. + + This simulates project-based testing where the cwd in the payload should be + the project root (project_root_path) rather than the start_dir. + + When project_root_path is provided: + - The cwd in the response should match project_root_path + - Test execution should still work correctly with start_dir + """ + # Use unittest_folder as our "project" directory + project_path = TEST_DATA_PATH / "unittest_folder" + start_dir = os.fsdecode(project_path) + pattern = "test_add*" + test_ids = [ + "test_add.TestAddFunction.test_add_positive_numbers", + ] + + os.environ["TEST_RUN_PIPE"] = "fake" + + # Call run_tests with project_root_path to simulate PROJECT_ROOT_PATH + actual = run_tests( + start_dir, + test_ids, + pattern, + None, + 1, + None, + project_root_path=start_dir, + ) + + assert actual["status"] == "success" + # cwd in response should match the project_root_path (project root) + assert actual["cwd"] == os.fsdecode(project_path), ( + f"Expected cwd '{os.fsdecode(project_path)}', got '{actual['cwd']}'" + ) + assert actual["result"] is not None + assert test_ids[0] in actual["result"] + assert actual["result"][test_ids[0]]["outcome"] == "success" + + +def test_project_root_path_with_different_cwd_and_start_dir(mock_send_run_data) -> None: # noqa: ARG001 + """Test unittest execution where project_root_path differs from start_dir. + + This simulates the scenario where: + - start_dir points to a subfolder where tests are located + - project_root_path (PROJECT_ROOT_PATH) points to the project root + + The cwd in the response should be the project root, while execution + still runs from the start_dir. + """ + # Use utils_nested_cases as our test case + project_path = TEST_DATA_PATH / "utils_nested_cases" + start_dir = os.fsdecode(project_path) + pattern = "*" + test_ids = [ + "file_one.CaseTwoFileOne.test_one", + ] + + os.environ["TEST_RUN_PIPE"] = "fake" + + # Call run_tests with project_root_path set to project root + actual = run_tests( + start_dir, + test_ids, + pattern, + None, + 1, + None, + project_root_path=os.fsdecode(project_path), + ) + + assert actual["status"] == "success" + # cwd should be the project root (project_root_path) + assert actual["cwd"] == os.fsdecode(project_path), ( + f"Expected cwd '{os.fsdecode(project_path)}', got '{actual['cwd']}'" + ) + assert actual["result"] is not None + assert test_ids[0] in actual["result"] + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="Symlinks require elevated privileges on Windows", +) +def test_symlink_with_project_root_path(mock_send_run_data) -> None: # noqa: ARG001 + """Test unittest execution with both symlink and project_root_path set. + + This tests the combination of: + 1. A symlinked test directory + 2. project_root_path (PROJECT_ROOT_PATH) set to the symlink path + + This simulates project-based testing where the project root is a symlink, + ensuring execution payloads correctly use the symlink path. + """ + with helpers.create_symlink(TEST_DATA_PATH, "unittest_folder", "symlink_unittest_exec") as ( + _source, + destination, + ): + assert destination.is_symlink() + + # Run execution with: + # - start_dir pointing to the symlink destination + # - project_root_path set to the symlink destination (simulating PROJECT_ROOT_PATH) + start_dir = os.fsdecode(destination) + pattern = "test_add*" + test_ids = [ + "test_add.TestAddFunction.test_add_positive_numbers", + ] + + os.environ["TEST_RUN_PIPE"] = "fake" + + actual = run_tests( + start_dir, + test_ids, + pattern, + None, + 1, + None, + project_root_path=start_dir, + ) + + assert actual["status"] == "success", ( + f"Status is not 'success', error is: {actual.get('error')}" + ) + # cwd should be the symlink path (project_root_path) + assert actual["cwd"] == os.fsdecode(destination), ( + f"CWD does not match symlink path: expected {os.fsdecode(destination)}, got {actual['cwd']}" + ) diff --git a/python_files/unittestadapter/discovery.py b/python_files/unittestadapter/discovery.py index ce8251218743..b3086d92b102 100644 --- a/python_files/unittestadapter/discovery.py +++ b/python_files/unittestadapter/discovery.py @@ -27,12 +27,13 @@ def discover_tests( start_dir: str, pattern: str, top_level_dir: Optional[str], + project_root_path: Optional[str] = None, ) -> DiscoveryPayloadDict: """Returns a dictionary containing details of the discovered tests. The returned dict has the following keys: - - cwd: Absolute path to the test start directory; + - cwd: Absolute path to the test start directory (or project_root_path if provided); - status: Test discovery status, can be "success" or "error"; - tests: Discoverered tests if any, not present otherwise. Note that the status can be "error" but the payload can still contain tests; - error: Discovery error if any, not present otherwise. @@ -56,8 +57,15 @@ def discover_tests( "": [list of errors] "status": "error", } + + Args: + start_dir: Directory where test discovery starts + pattern: Pattern to match test files (e.g., "test*.py") + top_level_dir: Top-level directory for the test tree hierarchy + project_root_path: Optional project root path for the cwd in the response payload + (used for project-based testing to root test tree at project) """ - cwd = os.path.abspath(start_dir) # noqa: PTH100 + cwd = os.path.abspath(project_root_path or start_dir) # noqa: PTH100 if "/" in start_dir: # is a subdir parent_dir = os.path.dirname(start_dir) # noqa: PTH120 sys.path.insert(0, parent_dir) @@ -133,7 +141,19 @@ def discover_tests( print(error_msg, file=sys.stderr) raise VSCodeUnittestError(error_msg) # noqa: B904 else: + # Check for PROJECT_ROOT_PATH environment variable (project-based testing). + # When set, this overrides top_level_dir to root the test tree at the project directory. + project_root_path = os.environ.get("PROJECT_ROOT_PATH") + if project_root_path: + top_level_dir = project_root_path + print( + f"PROJECT_ROOT_PATH is set, using {project_root_path} as top_level_dir for discovery" + ) + # Perform regular unittest test discovery. - payload = discover_tests(start_dir, pattern, top_level_dir) + # Pass project_root_path so the payload's cwd matches the project root. + payload = discover_tests( + start_dir, pattern, top_level_dir, project_root_path=project_root_path + ) # Post this discovery payload. send_post_request(payload, test_run_pipe) diff --git a/python_files/unittestadapter/execution.py b/python_files/unittestadapter/execution.py index 951289850884..e031138b6f75 100644 --- a/python_files/unittestadapter/execution.py +++ b/python_files/unittestadapter/execution.py @@ -36,6 +36,9 @@ ErrorType = Union[Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None]] test_run_pipe = "" START_DIR = "" +# PROJECT_ROOT_PATH: Used for project-based testing to override cwd in payload +# When set, this should be used as the cwd in all execution payloads +PROJECT_ROOT_PATH = None # type: Optional[str] class TestOutcomeEnum(str, enum.Enum): @@ -191,8 +194,22 @@ def run_tests( verbosity: int, failfast: Optional[bool], # noqa: FBT001 locals_: Optional[bool] = None, # noqa: FBT001 + project_root_path: Optional[str] = None, ) -> ExecutionPayloadDict: - cwd = os.path.abspath(start_dir) # noqa: PTH100 + """Run unittests and return the execution payload. + + Args: + start_dir: Directory where test discovery starts + test_ids: List of test IDs to run + pattern: Pattern to match test files + top_level_dir: Top-level directory for test tree hierarchy + verbosity: Verbosity level for test output + failfast: Stop on first failure + locals_: Show local variables in tracebacks + project_root_path: Optional project root path for the cwd in the response payload + (used for project-based testing to root test tree at project) + """ + cwd = os.path.abspath(project_root_path or start_dir) # noqa: PTH100 if "/" in start_dir: # is a subdir parent_dir = os.path.dirname(start_dir) # noqa: PTH120 sys.path.insert(0, parent_dir) @@ -259,7 +276,8 @@ def run_tests( def send_run_data(raw_data, test_run_pipe): status = raw_data["outcome"] - cwd = os.path.abspath(START_DIR) # noqa: PTH100 + # Use PROJECT_ROOT_PATH if set (project-based testing), otherwise use START_DIR + cwd = os.path.abspath(PROJECT_ROOT_PATH or START_DIR) # noqa: PTH100 test_id = raw_data["subtest"] or raw_data["test"] test_dict = {} test_dict[test_id] = raw_data @@ -348,7 +366,19 @@ def send_run_data(raw_data, test_run_pipe): args = argv[index + 1 :] or [] django_execution_runner(manage_py_path, test_ids, args) else: + # Check for PROJECT_ROOT_PATH environment variable (project-based testing). + # When set, this overrides the cwd in the payload to match the project root. + project_root_path = os.environ.get("PROJECT_ROOT_PATH") + if project_root_path: + # Update the module-level variable for send_run_data to use + # pylint: disable=global-statement + globals()["PROJECT_ROOT_PATH"] = project_root_path + print( + f"PROJECT_ROOT_PATH is set, using {project_root_path} as cwd for execution payload" + ) + # Perform regular unittest execution. + # Pass project_root_path so the payload's cwd matches the project root. payload = run_tests( start_dir, test_ids, @@ -357,6 +387,7 @@ def send_run_data(raw_data, test_run_pipe): verbosity, failfast, locals_, + project_root_path=project_root_path, ) if is_coverage_run: diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index cb0fcd69a00e..be4e3daaa843 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -77,6 +77,9 @@ def __init__(self, message): map_id_to_path = {} collected_tests_so_far = set() TEST_RUN_PIPE = os.getenv("TEST_RUN_PIPE") +PROJECT_ROOT_PATH = os.getenv( + "PROJECT_ROOT_PATH" +) # Path to project root for multi-project workspaces SYMLINK_PATH = None INCLUDE_BRANCHES = False @@ -86,6 +89,20 @@ def __init__(self, message): _CACHED_CWD: pathlib.Path | None = None +def get_test_root_path() -> pathlib.Path: + """Get the root path for the test tree. + + For project-based testing, this returns PROJECT_ROOT_PATH (the project root). + For legacy mode, this returns the current working directory. + + Returns: + pathlib.Path: The root path to use for the test tree. + """ + if PROJECT_ROOT_PATH: + return pathlib.Path(PROJECT_ROOT_PATH) + return pathlib.Path.cwd() + + def pytest_load_initial_conftests(early_config, parser, args): # noqa: ARG001 has_pytest_cov = early_config.pluginmanager.hasplugin( "pytest_cov" @@ -409,21 +426,23 @@ def pytest_sessionfinish(session, exitstatus): Exit code 4: pytest command line usage error Exit code 5: No tests were collected """ - cwd = pathlib.Path.cwd() + # Get the root path for the test tree structure (not the CWD for test execution) + # This is PROJECT_ROOT_PATH in project-based mode, or cwd in legacy mode + test_root_path = get_test_root_path() if SYMLINK_PATH: - print("Plugin warning[vscode-pytest]: SYMLINK set, adjusting cwd.") - cwd = pathlib.Path(SYMLINK_PATH) + print("Plugin warning[vscode-pytest]: SYMLINK set, adjusting test root path.") + test_root_path = pathlib.Path(SYMLINK_PATH) if IS_DISCOVERY: if not (exitstatus == 0 or exitstatus == 1 or exitstatus == 5): error_node: TestNode = { "name": "", - "path": cwd, + "path": test_root_path, "type_": "error", "children": [], "id_": "", } - send_discovery_message(os.fsdecode(cwd), error_node) + send_discovery_message(os.fsdecode(test_root_path), error_node) try: session_node: TestNode | None = build_test_tree(session) if not session_node: @@ -431,19 +450,19 @@ def pytest_sessionfinish(session, exitstatus): "Something went wrong following pytest finish, \ no session node was created" ) - send_discovery_message(os.fsdecode(cwd), session_node) + send_discovery_message(os.fsdecode(test_root_path), session_node) except Exception as e: ERRORS.append( f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" ) error_node: TestNode = { "name": "", - "path": cwd, + "path": test_root_path, "type_": "error", "children": [], "id_": "", } - send_discovery_message(os.fsdecode(cwd), error_node) + send_discovery_message(os.fsdecode(test_root_path), error_node) else: if exitstatus == 0 or exitstatus == 1: exitstatus_bool = "success" @@ -454,7 +473,7 @@ def pytest_sessionfinish(session, exitstatus): exitstatus_bool = "error" send_execution_message( - os.fsdecode(cwd), + os.fsdecode(test_root_path), exitstatus_bool, None, ) @@ -540,7 +559,7 @@ def pytest_sessionfinish(session, exitstatus): payload: CoveragePayloadDict = CoveragePayloadDict( coverage=True, - cwd=os.fspath(cwd), + cwd=os.fspath(test_root_path), result=file_coverage_map, error=None, ) @@ -832,7 +851,8 @@ def create_session_node(session: pytest.Session) -> TestNode: Keyword arguments: session -- the pytest session. """ - node_path = get_node_path(session) + # Use PROJECT_ROOT_PATH if set (project-based testing), otherwise use session path (legacy) + node_path = pathlib.Path(PROJECT_ROOT_PATH) if PROJECT_ROOT_PATH else get_node_path(session) return { "name": node_path.name, "path": node_path, diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index c28535b30644..037bfb265088 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -1,6 +1,6 @@ import { inject, injectable, named } from 'inversify'; import * as path from 'path'; -import { DebugConfiguration, l10n, Uri, WorkspaceFolder, DebugSession, DebugSessionOptions } from 'vscode'; +import { DebugConfiguration, l10n, Uri, WorkspaceFolder, DebugSession, DebugSessionOptions, Disposable } from 'vscode'; import { IApplicationShell, IDebugService } from '../../common/application/types'; import { EXTENSION_ROOT_DIR } from '../../common/constants'; import * as internalScripts from '../../common/process/internal/scripts'; @@ -17,6 +17,14 @@ import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis import { showErrorMessage } from '../../common/vscodeApis/windowApis'; import { createDeferred } from '../../common/utils/async'; import { addPathToPythonpath } from './helpers'; +import * as envExtApi from '../../envExt/api.internal'; + +/** + * Key used to mark debug configurations with a unique session identifier. + * This allows us to track which debug session belongs to which launchDebugger() call + * when multiple debug sessions are launched in parallel. + */ +const TEST_SESSION_MARKER_KEY = '__vscodeTestSessionMarker'; @injectable() export class DebugLauncher implements ITestDebugLauncher { @@ -31,6 +39,10 @@ export class DebugLauncher implements ITestDebugLauncher { this.configService = this.serviceContainer.get(IConfigurationService); } + /** + * Launches a debug session for test execution. + * Handles cancellation, multi-session support via unique markers, and cleanup. + */ public async launchDebugger( options: LaunchOptions, callback?: () => void, @@ -38,18 +50,35 @@ export class DebugLauncher implements ITestDebugLauncher { ): Promise { const deferred = createDeferred(); let hasCallbackBeenCalled = false; + + // Collect disposables for cleanup when debugging completes + const disposables: Disposable[] = []; + + // Ensure callback is only invoked once, even if multiple termination paths fire + const callCallbackOnce = () => { + if (!hasCallbackBeenCalled) { + hasCallbackBeenCalled = true; + callback?.(); + } + }; + + // Early exit if already cancelled before we start if (options.token && options.token.isCancellationRequested) { - hasCallbackBeenCalled = true; - return undefined; + callCallbackOnce(); deferred.resolve(); - callback?.(); + return deferred.promise; } - options.token?.onCancellationRequested(() => { - deferred.resolve(); - callback?.(); - hasCallbackBeenCalled = true; - }); + // Listen for cancellation from the test run (e.g., user clicks stop in Test Explorer) + // This allows the caller to clean up resources even if the debug session is still running + if (options.token) { + disposables.push( + options.token.onCancellationRequested(() => { + deferred.resolve(); + callCallbackOnce(); + }), + ); + } const workspaceFolder = DebugLauncher.resolveWorkspaceFolder(options.cwd); const launchArgs = await this.getLaunchArgs( @@ -59,23 +88,57 @@ export class DebugLauncher implements ITestDebugLauncher { ); const debugManager = this.serviceContainer.get(IDebugService); - let activatedDebugSession: DebugSession | undefined; - debugManager.startDebugging(workspaceFolder, launchArgs, sessionOptions).then(() => { - // Save the debug session after it is started so we can check if it is the one that was terminated. - activatedDebugSession = debugManager.activeDebugSession; - }); - debugManager.onDidTerminateDebugSession((session) => { - traceVerbose(`Debug session terminated. sessionId: ${session.id}`); - // Only resolve no callback has been made and the session is the one that was started. - if ( - !hasCallbackBeenCalled && - activatedDebugSession !== undefined && - session.id === activatedDebugSession?.id - ) { - deferred.resolve(); - callback?.(); - } + // Unique marker to identify this session among concurrent debug sessions + const sessionMarker = `test-${Date.now()}-${Math.random().toString(36).slice(2)}`; + launchArgs[TEST_SESSION_MARKER_KEY] = sessionMarker; + + let ourSession: DebugSession | undefined; + + // Capture our specific debug session when it starts by matching the marker. + // This fires for ALL debug sessions, so we filter to only our marker. + disposables.push( + debugManager.onDidStartDebugSession((session) => { + if (session.configuration[TEST_SESSION_MARKER_KEY] === sessionMarker) { + ourSession = session; + traceVerbose(`[test-debug] Debug session started: ${session.name} (${session.id})`); + } + }), + ); + + // Handle debug session termination (user stops debugging, or tests complete). + // Only react to OUR session terminating - other parallel sessions should + // continue running independently. + disposables.push( + debugManager.onDidTerminateDebugSession((session) => { + if (ourSession && session.id === ourSession.id) { + traceVerbose(`[test-debug] Debug session terminated: ${session.name} (${session.id})`); + deferred.resolve(); + callCallbackOnce(); + } + }), + ); + + // Clean up event subscriptions when debugging completes (success, failure, or cancellation) + deferred.promise.finally(() => { + disposables.forEach((d) => d.dispose()); }); + + // Start the debug session + let started = false; + try { + started = await debugManager.startDebugging(workspaceFolder, launchArgs, sessionOptions); + } catch (error) { + traceError('Error starting debug session', error); + deferred.reject(error); + callCallbackOnce(); + return deferred.promise; + } + if (!started) { + traceError('Failed to start debug session'); + deferred.resolve(); + callCallbackOnce(); + } + return deferred.promise; } @@ -108,6 +171,12 @@ export class DebugLauncher implements ITestDebugLauncher { subProcess: true, }; } + + // Use project name in debug session name if provided + if (options.project) { + debugConfig.name = `Debug Tests: ${options.project.name}`; + } + if (!debugConfig.rules) { debugConfig.rules = []; } @@ -116,7 +185,7 @@ export class DebugLauncher implements ITestDebugLauncher { include: false, }); - DebugLauncher.applyDefaults(debugConfig!, workspaceFolder, configSettings); + DebugLauncher.applyDefaults(debugConfig!, workspaceFolder, configSettings, options.cwd); return this.convertConfigToArgs(debugConfig!, workspaceFolder, options); } @@ -163,6 +232,7 @@ export class DebugLauncher implements ITestDebugLauncher { cfg: LaunchRequestArguments, workspaceFolder: WorkspaceFolder, configSettings: IPythonSettings, + optionsCwd?: string, ) { // cfg.pythonPath is handled by LaunchConfigurationResolver. @@ -170,7 +240,9 @@ export class DebugLauncher implements ITestDebugLauncher { cfg.console = 'internalConsole'; } if (!cfg.cwd) { - cfg.cwd = configSettings.testing.cwd || workspaceFolder.uri.fsPath; + // For project-based testing, use the project's cwd (optionsCwd) if provided. + // Otherwise fall back to settings.testing.cwd or the workspace folder. + cfg.cwd = optionsCwd || configSettings.testing.cwd || workspaceFolder.uri.fsPath; } if (!cfg.env) { cfg.env = {}; @@ -257,6 +329,23 @@ export class DebugLauncher implements ITestDebugLauncher { // run via F5 style debugging. launchArgs.purpose = []; + // For project-based execution, get the Python path from the project's environment. + // Fallback: if env API unavailable or fails, LaunchConfigurationResolver already set + // launchArgs.python from the active interpreter, so debugging still works. + if (options.project && envExtApi.useEnvExtension()) { + try { + const pythonEnv = await envExtApi.getEnvironment(options.project.uri); + if (pythonEnv?.execInfo?.run?.executable) { + launchArgs.python = pythonEnv.execInfo.run.executable; + traceVerbose( + `[test-by-project] Debug session using Python path from project: ${launchArgs.python}`, + ); + } + } catch (error) { + traceVerbose(`[test-by-project] Could not get environment for project, using default: ${error}`); + } + } + return launchArgs; } diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts index 562005386633..e2fa2d6d2e5a 100644 --- a/src/client/testing/common/types.ts +++ b/src/client/testing/common/types.ts @@ -2,6 +2,7 @@ import { CancellationToken, DebugSessionOptions, OutputChannel, Uri } from 'vsco import { Product } from '../../common/types'; import { TestSettingsPropertyNames } from '../configuration/types'; import { TestProvider } from '../types'; +import { PythonProject } from '../../envExt/types'; export type UnitTestProduct = Product.pytest | Product.unittest; @@ -26,6 +27,8 @@ export type LaunchOptions = { pytestPort?: string; pytestUUID?: string; runTestIdsPort?: string; + /** Optional Python project for project-based execution. */ + project?: PythonProject; }; export enum TestFilter { diff --git a/src/client/testing/testController/common/projectAdapter.ts b/src/client/testing/testController/common/projectAdapter.ts new file mode 100644 index 000000000000..cfffbf439ca6 --- /dev/null +++ b/src/client/testing/testController/common/projectAdapter.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestItem, Uri } from 'vscode'; +import { TestProvider } from '../../types'; +import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './types'; +import { PythonEnvironment, PythonProject } from '../../../envExt/types'; + +/** + * Represents a single Python project with its own test infrastructure. + * A project is defined as a combination of a Python executable + URI (folder/file). + * Projects are uniquely identified by their projectUri (use projectUri.toString() for map keys). + */ +export interface ProjectAdapter { + // === IDENTITY === + /** + * Display name for the project (e.g., "alice (Python 3.11)"). + */ + projectName: string; + + /** + * URI of the project root folder or file. + * This is the unique identifier for the project. + */ + projectUri: Uri; + + /** + * Parent workspace URI containing this project. + */ + workspaceUri: Uri; + + // === API OBJECTS (from vscode-python-environments extension) === + /** + * The PythonProject object from the environment API. + */ + pythonProject: PythonProject; + + /** + * The resolved PythonEnvironment with execution details. + * Contains execInfo.run.executable for running tests. + */ + pythonEnvironment: PythonEnvironment; + + // === TEST INFRASTRUCTURE === + /** + * Test framework provider ('pytest' | 'unittest'). + */ + testProvider: TestProvider; + + /** + * Adapter for test discovery. + */ + discoveryAdapter: ITestDiscoveryAdapter; + + /** + * Adapter for test execution. + */ + executionAdapter: ITestExecutionAdapter; + + /** + * Result resolver for this project (maps test IDs and handles results). + */ + resultResolver: ITestResultResolver; + + /** + * Absolute paths of nested projects to ignore during discovery. + * Used to pass --ignore flags to pytest or exclusion filters to unittest. + * Only populated for parent projects that contain nested child projects. + */ + nestedProjectPathsToIgnore?: string[]; + + // === LIFECYCLE === + /** + * Whether discovery is currently running for this project. + */ + isDiscovering: boolean; + + /** + * Whether tests are currently executing for this project. + */ + isExecuting: boolean; + + /** + * Root TestItem for this project in the VS Code test tree. + * All project tests are children of this item. + */ + projectRootTestItem?: TestItem; +} diff --git a/src/client/testing/testController/common/projectTestExecution.ts b/src/client/testing/testController/common/projectTestExecution.ts new file mode 100644 index 000000000000..fe3b4f91491a --- /dev/null +++ b/src/client/testing/testController/common/projectTestExecution.ts @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, FileCoverageDetail, TestItem, TestRun, TestRunProfileKind, TestRunRequest } from 'vscode'; +import { traceError, traceInfo, traceVerbose, traceWarn } from '../../../logging'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { IPythonExecutionFactory } from '../../../common/process/types'; +import { ITestDebugLauncher } from '../../common/types'; +import { ProjectAdapter } from './projectAdapter'; +import { TestProjectRegistry } from './testProjectRegistry'; +import { getProjectId } from './projectUtils'; +import { getEnvExtApi, useEnvExtension } from '../../../envExt/api.internal'; +import { isParentPath } from '../../../pythonEnvironments/common/externalDependencies'; + +/** Dependencies for project-based test execution. */ +export interface ProjectExecutionDependencies { + projectRegistry: TestProjectRegistry; + pythonExecFactory: IPythonExecutionFactory; + debugLauncher: ITestDebugLauncher; +} + +/** Executes tests for multiple projects, grouping by project and using each project's Python environment. */ +export async function executeTestsForProjects( + projects: ProjectAdapter[], + testItems: TestItem[], + runInstance: TestRun, + request: TestRunRequest, + token: CancellationToken, + deps: ProjectExecutionDependencies, +): Promise { + if (projects.length === 0) { + traceError(`[test-by-project] No projects provided for execution`); + return; + } + + // Early exit if already cancelled + if (token.isCancellationRequested) { + traceInfo(`[test-by-project] Execution cancelled before starting`); + return; + } + + // Group test items by project + const testsByProject = await groupTestItemsByProject(testItems, projects); + + const isDebugMode = request.profile?.kind === TestRunProfileKind.Debug; + traceInfo(`[test-by-project] Executing tests across ${testsByProject.size} project(s), debug=${isDebugMode}`); + + // Setup coverage once for all projects (single callback that routes by file path) + if (request.profile?.kind === TestRunProfileKind.Coverage) { + setupCoverageForProjects(request, projects); + } + + // Execute tests for each project in parallel + // For debug mode, multiple debug sessions will be launched in parallel + // Each execution respects cancellation via runInstance.token + const executions = Array.from(testsByProject.entries()).map(async ([_projectId, { project, items }]) => { + // Check for cancellation before starting each project + if (token.isCancellationRequested) { + traceInfo(`[test-by-project] Skipping ${project.projectName} - cancellation requested`); + return; + } + + if (items.length === 0) return; + + traceInfo(`[test-by-project] Executing ${items.length} test item(s) for project: ${project.projectName}`); + + sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { + tool: project.testProvider, + debugging: isDebugMode, + }); + + try { + await executeTestsForProject(project, items, runInstance, request, deps); + } catch (error) { + // Don't log cancellation as an error + if (!token.isCancellationRequested) { + traceError(`[test-by-project] Execution failed for project ${project.projectName}:`, error); + } + } + }); + + await Promise.all(executions); + + if (token.isCancellationRequested) { + traceInfo(`[test-by-project] Project executions cancelled`); + } else { + traceInfo(`[test-by-project] All project executions completed`); + } +} + +/** Lookup context for caching project lookups within a single test run. */ +interface ProjectLookupContext { + uriToAdapter: Map; + projectPathToAdapter: Map; +} + +/** Groups test items by owning project using env API or path-based matching as fallback. */ +export async function groupTestItemsByProject( + testItems: TestItem[], + projects: ProjectAdapter[], +): Promise> { + const result = new Map(); + + // Initialize entries for all projects + for (const project of projects) { + result.set(getProjectId(project.projectUri), { project, items: [] }); + } + + // Build lookup context for this run - O(p) one-time setup, enables O(1) lookups per item. + // When tests are from a single project, most lookups hit the cache after the first item. + const lookupContext: ProjectLookupContext = { + uriToAdapter: new Map(), + projectPathToAdapter: new Map(projects.map((p) => [p.projectUri.fsPath, p])), + }; + + // Assign each test item to its project + for (const item of testItems) { + const project = await findProjectForTestItem(item, projects, lookupContext); + if (project) { + const entry = result.get(getProjectId(project.projectUri)); + if (entry) { + entry.items.push(item); + } + } else { + // If no project matches, log it + traceWarn(`[test-by-project] Could not match test item ${item.id} to a project`); + } + } + + // Remove projects with no test items + for (const [projectId, entry] of result.entries()) { + if (entry.items.length === 0) { + result.delete(projectId); + } + } + + return result; +} + +/** Finds the project that owns a test item. */ +export async function findProjectForTestItem( + item: TestItem, + projects: ProjectAdapter[], + lookupContext?: ProjectLookupContext, +): Promise { + if (!item.uri) return undefined; + + const uriPath = item.uri.fsPath; + + // Check lookup context first - O(1) + if (lookupContext?.uriToAdapter.has(uriPath)) { + return lookupContext.uriToAdapter.get(uriPath); + } + + let result: ProjectAdapter | undefined; + + // Try using the Python Environment extension API first. + // Legacy path: when useEnvExtension() is false, this block is skipped and we go + // directly to findProjectByPath() below (path-based matching). + if (useEnvExtension()) { + try { + const envExtApi = await getEnvExtApi(); + const pythonProject = envExtApi.getPythonProject(item.uri); + if (pythonProject) { + // Use lookup context for O(1) adapter lookup instead of O(p) linear search + result = lookupContext?.projectPathToAdapter.get(pythonProject.uri.fsPath); + if (!result) { + // Fallback to linear search if lookup context not available + result = projects.find((p) => p.projectUri.fsPath === pythonProject.uri.fsPath); + } + } + } catch (error) { + traceVerbose(`[test-by-project] Failed to use env extension API, falling back to path matching: ${error}`); + } + } + + // Fallback: path-based matching when env API unavailable or didn't find a match. + // O(p) time complexity where p = number of projects. + if (!result) { + result = findProjectByPath(item, projects); + } + + // Store result for future lookups of same file within this run - O(1) + if (lookupContext) { + lookupContext.uriToAdapter.set(uriPath, result); + } + + return result; +} + +/** Fallback: finds project using path-based matching. */ +function findProjectByPath(item: TestItem, projects: ProjectAdapter[]): ProjectAdapter | undefined { + if (!item.uri) return undefined; + + const itemPath = item.uri.fsPath; + let bestMatch: ProjectAdapter | undefined; + let bestMatchLength = 0; + + for (const project of projects) { + const projectPath = project.projectUri.fsPath; + // Use isParentPath for safe path-boundary matching (handles separators and case normalization) + if (isParentPath(itemPath, projectPath) && projectPath.length > bestMatchLength) { + bestMatch = project; + bestMatchLength = projectPath.length; + } + } + + return bestMatch; +} + +/** Executes tests for a single project using the project's Python environment. */ +export async function executeTestsForProject( + project: ProjectAdapter, + testItems: TestItem[], + runInstance: TestRun, + request: TestRunRequest, + deps: ProjectExecutionDependencies, +): Promise { + const processedTestItemIds = new Set(); + const uniqueTestCaseIds = new Set(); + + // Mark items as started and collect test IDs (deduplicated to handle overlapping selections) + for (const item of testItems) { + const testCaseNodes = getTestCaseNodesRecursive(item); + for (const node of testCaseNodes) { + if (processedTestItemIds.has(node.id)) { + continue; + } + processedTestItemIds.add(node.id); + runInstance.started(node); + const runId = project.resultResolver.vsIdToRunId.get(node.id); + if (runId) { + uniqueTestCaseIds.add(runId); + } + } + } + + const testCaseIds = Array.from(uniqueTestCaseIds); + + if (testCaseIds.length === 0) { + traceVerbose(`[test-by-project] No test IDs found for project ${project.projectName}`); + return; + } + + traceInfo(`[test-by-project] Running ${testCaseIds.length} test(s) for project: ${project.projectName}`); + + // Execute tests using the project's execution adapter + await project.executionAdapter.runTests( + project.projectUri, + testCaseIds, + request.profile?.kind, + runInstance, + deps.pythonExecFactory, + deps.debugLauncher, + undefined, // interpreter not needed, project has its own environment + project, + ); +} + +/** Recursively gets all leaf test case nodes from a test item tree. */ +export function getTestCaseNodesRecursive(item: TestItem): TestItem[] { + const results: TestItem[] = []; + if (item.children.size === 0) { + // This is a leaf node (test case) + results.push(item); + } else { + // Recursively get children + item.children.forEach((child) => { + results.push(...getTestCaseNodesRecursive(child)); + }); + } + return results; +} + +/** Sets up detailed coverage loading that routes to the correct project by file path. */ +export function setupCoverageForProjects(request: TestRunRequest, projects: ProjectAdapter[]): void { + if (request.profile?.kind === TestRunProfileKind.Coverage) { + // Create a single callback that routes to the correct project's coverage map by file path + request.profile.loadDetailedCoverage = ( + _testRun: TestRun, + fileCoverage, + _token, + ): Thenable => { + const filePath = fileCoverage.uri.fsPath; + // Find the project that has coverage data for this file + for (const project of projects) { + const details = project.resultResolver.detailedCoverageMap.get(filePath); + if (details) { + return Promise.resolve(details); + } + } + return Promise.resolve([]); + }; + } +} diff --git a/src/client/testing/testController/common/projectUtils.ts b/src/client/testing/testController/common/projectUtils.ts new file mode 100644 index 000000000000..b104b7f6842d --- /dev/null +++ b/src/client/testing/testController/common/projectUtils.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { IConfigurationService } from '../../../common/types'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { UNITTEST_PROVIDER } from '../../common/constants'; +import { TestProvider } from '../../types'; +import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './types'; +import { UnittestTestDiscoveryAdapter } from '../unittest/testDiscoveryAdapter'; +import { UnittestTestExecutionAdapter } from '../unittest/testExecutionAdapter'; +import { PytestTestDiscoveryAdapter } from '../pytest/pytestDiscoveryAdapter'; +import { PytestTestExecutionAdapter } from '../pytest/pytestExecutionAdapter'; + +/** + * Separator used to scope test IDs to a specific project. + * Format: {projectId}{SEPARATOR}{testPath} + * Example: "file:///workspace/project@@PROJECT@@test_file.py::test_name" + */ +export const PROJECT_ID_SEPARATOR = '@@vsc@@'; + +/** + * Gets the project ID from a project URI. + * The project ID is simply the string representation of the URI, matching how + * the Python Environments extension stores projects in Map. + * + * @param projectUri The project URI + * @returns The project ID (URI as string) + */ +export function getProjectId(projectUri: Uri): string { + return projectUri.toString(); +} + +/** + * Parses a project-scoped vsId back into its components. + * + * @param vsId The VS Code test item ID to parse + * @returns A tuple of [projectId, runId]. If the ID is not project-scoped, + * returns [undefined, vsId] (legacy format) + */ +export function parseVsId(vsId: string): [string | undefined, string] { + const separatorIndex = vsId.indexOf(PROJECT_ID_SEPARATOR); + if (separatorIndex === -1) { + return [undefined, vsId]; // Legacy ID without project scope + } + return [vsId.substring(0, separatorIndex), vsId.substring(separatorIndex + PROJECT_ID_SEPARATOR.length)]; +} + +/** + * Creates a display name for a project including Python version. + * Format: "{projectName} (Python {version})" + * + * @param projectName The name of the project + * @param pythonVersion The Python version string (e.g., "3.11.2") + * @returns Formatted display name + */ +export function createProjectDisplayName(projectName: string, pythonVersion: string): string { + // Extract major.minor version if full version provided + const versionMatch = pythonVersion.match(/^(\d+\.\d+)/); + const shortVersion = versionMatch ? versionMatch[1] : pythonVersion; + + return `${projectName} (Python ${shortVersion})`; +} + +/** + * Creates test adapters (discovery and execution) for a given test provider. + * + * @param testProvider The test framework provider ('pytest' | 'unittest') + * @param resultResolver The result resolver to use for test results + * @param configSettings The configuration service + * @param envVarsService The environment variables provider + * @returns An object containing the discovery and execution adapters + */ +export function createTestAdapters( + testProvider: TestProvider, + resultResolver: ITestResultResolver, + configSettings: IConfigurationService, + envVarsService: IEnvironmentVariablesProvider, +): { discoveryAdapter: ITestDiscoveryAdapter; executionAdapter: ITestExecutionAdapter } { + if (testProvider === UNITTEST_PROVIDER) { + return { + discoveryAdapter: new UnittestTestDiscoveryAdapter(configSettings, resultResolver, envVarsService), + executionAdapter: new UnittestTestExecutionAdapter(configSettings, resultResolver, envVarsService), + }; + } + + return { + discoveryAdapter: new PytestTestDiscoveryAdapter(configSettings, resultResolver, envVarsService), + executionAdapter: new PytestTestExecutionAdapter(configSettings, resultResolver, envVarsService), + }; +} diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 959d08fee1a9..c126d233de1b 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -26,10 +26,31 @@ export class PythonResultResolver implements ITestResultResolver { public detailedCoverageMap = new Map(); - constructor(testController: TestController, testProvider: TestProvider, private workspaceUri: Uri) { + /** + * Optional project ID for scoping test IDs. + * When set, all test IDs are prefixed with `{projectId}@@vsc@@` for project-based testing. + * When undefined, uses legacy workspace-level IDs for backward compatibility. + */ + private projectId?: string; + + /** + * Optional project display name for labeling the test tree root. + * When set, the root node label will be "project: {projectName}" instead of the folder name. + */ + private projectName?: string; + + constructor( + testController: TestController, + testProvider: TestProvider, + private workspaceUri: Uri, + projectId?: string, + projectName?: string, + ) { this.testController = testController; this.testProvider = testProvider; - // Initialize a new TestItemIndex which will be used to track test items in this workspace + this.projectId = projectId; + this.projectName = projectName; + // Initialize a new TestItemIndex which will be used to track test items in this workspace/project this.testItemIndex = new TestItemIndex(); } @@ -46,6 +67,14 @@ export class PythonResultResolver implements ITestResultResolver { return this.testItemIndex.vsIdToRunIdMap; } + /** + * Gets the project ID for this resolver (if any). + * Used for project-scoped test ID generation. + */ + public getProjectId(): string | undefined { + return this.projectId; + } + public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void { PythonResultResolver.discoveryHandler.processDiscovery( payload, @@ -54,6 +83,8 @@ export class PythonResultResolver implements ITestResultResolver { this.workspaceUri, this.testProvider, token, + this.projectId, + this.projectName, ); sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, diff --git a/src/client/testing/testController/common/testDiscoveryHandler.ts b/src/client/testing/testController/common/testDiscoveryHandler.ts index 50f4fa71406a..3f70e6b68594 100644 --- a/src/client/testing/testController/common/testDiscoveryHandler.ts +++ b/src/client/testing/testController/common/testDiscoveryHandler.ts @@ -5,11 +5,12 @@ import { CancellationToken, TestController, Uri, MarkdownString } from 'vscode'; import * as util from 'util'; import { DiscoveredTestPayload } from './types'; import { TestProvider } from '../../types'; -import { traceError } from '../../../logging'; +import { traceError, traceWarn } from '../../../logging'; import { Testing } from '../../../common/utils/localize'; import { createErrorTestItem } from './testItemUtilities'; import { buildErrorNodeOptions, populateTestTree } from './utils'; import { TestItemIndex } from './testItemIndex'; +import { PROJECT_ID_SEPARATOR } from './projectUtils'; /** * Stateless handler for processing discovery payloads and building/updating the TestItem tree. @@ -27,6 +28,8 @@ export class TestDiscoveryHandler { workspaceUri: Uri, testProvider: TestProvider, token?: CancellationToken, + projectId?: string, + projectName?: string, ): void { if (!payload) { // No test data is available @@ -38,10 +41,13 @@ export class TestDiscoveryHandler { // Check if there were any errors in the discovery process. if (rawTestData.status === 'error') { - this.createErrorNode(testController, workspaceUri, rawTestData.error, testProvider); + this.createErrorNode(testController, workspaceUri, rawTestData.error, testProvider, projectId, projectName); } else { // remove error node only if no errors exist. - testController.items.delete(`DiscoveryError:${workspacePath}`); + const errorNodeId = projectId + ? `${projectId}${PROJECT_ID_SEPARATOR}DiscoveryError:${workspacePath}` + : `DiscoveryError:${workspacePath}`; + testController.items.delete(errorNodeId); } if (rawTestData.tests || rawTestData.tests === null) { @@ -62,8 +68,10 @@ export class TestDiscoveryHandler { runIdToTestItem: testItemIndex.runIdToTestItemMap, runIdToVSid: testItemIndex.runIdToVSidMap, vsIdToRunId: testItemIndex.vsIdToRunIdMap, - } as any, + }, token, + projectId, + projectName, ); } } @@ -76,6 +84,8 @@ export class TestDiscoveryHandler { workspaceUri: Uri, error: string[] | undefined, testProvider: TestProvider, + projectId?: string, + projectName?: string, ): void { const workspacePath = workspaceUri.fsPath; const testingErrorConst = @@ -83,14 +93,41 @@ export class TestDiscoveryHandler { traceError(testingErrorConst, 'for workspace: ', workspacePath, '\r\n', error?.join('\r\n\r\n') ?? ''); - let errorNode = testController.items.get(`DiscoveryError:${workspacePath}`); + // For unittest in project-based mode, check if the error might be caused by nested project imports + // This helps users understand that import errors from nested projects can be safely ignored + // if those tests are covered by a different project with the correct environment. + if (testProvider === 'unittest' && projectId) { + const errorText = error?.join(' ') ?? ''; + const isImportError = + errorText.includes('ModuleNotFoundError') || + errorText.includes('ImportError') || + errorText.includes('No module named'); + + if (isImportError) { + const warningMessage = + '--- ' + + `[test-by-project] Import error during unittest discovery for project at ${workspacePath}. ` + + 'This may be caused by test files in nested project directories that require different dependencies. ' + + 'If these tests are discovered successfully by their own project (with the correct Python environment), ' + + 'this error can be safely ignored. To avoid this, consider excluding nested project paths from parent project discovery. ' + + '---'; + traceWarn(warningMessage); + } + } + + const errorNodeId = projectId + ? `${projectId}${PROJECT_ID_SEPARATOR}DiscoveryError:${workspacePath}` + : `DiscoveryError:${workspacePath}`; + let errorNode = testController.items.get(errorNodeId); const message = util.format( `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, error?.join('\r\n\r\n') ?? '', ); if (errorNode === undefined) { - const options = buildErrorNodeOptions(workspaceUri, message, testProvider); + const options = buildErrorNodeOptions(workspaceUri, message, testProvider, projectName); + // Update the error node ID to include project scope if applicable + options.id = errorNodeId; errorNode = createErrorTestItem(testController, options); testController.items.add(errorNode); } diff --git a/src/client/testing/testController/common/testProjectRegistry.ts b/src/client/testing/testController/common/testProjectRegistry.ts new file mode 100644 index 000000000000..4f0702ad584c --- /dev/null +++ b/src/client/testing/testController/common/testProjectRegistry.ts @@ -0,0 +1,330 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { TestController, Uri } from 'vscode'; +import { isParentPath } from '../../../common/platform/fs-paths'; +import { IConfigurationService } from '../../../common/types'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { traceError, traceInfo } from '../../../logging'; +import { UNITTEST_PROVIDER } from '../../common/constants'; +import { TestProvider } from '../../types'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { PythonProject, PythonEnvironment } from '../../../envExt/types'; +import { getEnvExtApi, useEnvExtension } from '../../../envExt/api.internal'; +import { ProjectAdapter } from './projectAdapter'; +import { getProjectId, createProjectDisplayName, createTestAdapters } from './projectUtils'; +import { PythonResultResolver } from './resultResolver'; + +/** + * Registry for Python test projects within workspaces. + * + * Manages the lifecycle of test projects including: + * - Discovering Python projects via Python Environments API + * - Creating and storing ProjectAdapter instances per workspace + * - Computing nested project relationships for ignore lists + * - Fallback to default "legacy" project when API unavailable + * + * **Key concepts:** + * - **Workspace:** A VS Code workspace folder (may contain multiple projects) + * - **Project:** A Python project within a workspace (identified by pyproject.toml, setup.py, etc.) + * - **ProjectUri:** The unique identifier for a project (the URI of the project root directory) + * - Each project gets its own test tree root, Python environment, and test adapters + * + * **Project identification:** + * Projects are identified and tracked by their URI (projectUri.toString()). This matches + * how the Python Environments extension stores projects in its Map. + */ +export class TestProjectRegistry { + /** + * Map of workspace URI -> Map of project URI string -> ProjectAdapter + * + * Projects are keyed by their URI string (projectUri.toString()) which matches how + * the Python Environments extension identifies projects. This enables O(1) lookups + * when given a project URI. + */ + private readonly workspaceProjects: Map> = new Map(); + + constructor( + private readonly testController: TestController, + private readonly configSettings: IConfigurationService, + private readonly interpreterService: IInterpreterService, + private readonly envVarsService: IEnvironmentVariablesProvider, + ) {} + + /** + * Gets the projects map for a workspace, if it exists. + */ + public getWorkspaceProjects(workspaceUri: Uri): Map | undefined { + return this.workspaceProjects.get(workspaceUri); + } + + /** + * Checks if a workspace has been initialized with projects. + */ + public hasProjects(workspaceUri: Uri): boolean { + return this.workspaceProjects.has(workspaceUri); + } + + /** + * Gets all projects for a workspace as an array. + */ + public getProjectsArray(workspaceUri: Uri): ProjectAdapter[] { + const projectsMap = this.workspaceProjects.get(workspaceUri); + return projectsMap ? Array.from(projectsMap.values()) : []; + } + + /** + * Discovers and registers all Python projects for a workspace. + * Returns the discovered projects for the caller to use. + */ + public async discoverAndRegisterProjects(workspaceUri: Uri): Promise { + traceInfo(`[test-by-project] Discovering projects for workspace: ${workspaceUri.fsPath}`); + + const projects = await this.discoverProjects(workspaceUri); + + // Create map for this workspace, keyed by project URI + const projectsMap = new Map(); + projects.forEach((project) => { + projectsMap.set(getProjectId(project.projectUri), project); + }); + + this.workspaceProjects.set(workspaceUri, projectsMap); + traceInfo(`[test-by-project] Registered ${projects.length} project(s) for ${workspaceUri.fsPath}`); + + return projects; + } + + /** + * Computes and populates nested project ignore lists for all projects in a workspace. + * Must be called before discovery to ensure parent projects ignore nested children. + */ + public configureNestedProjectIgnores(workspaceUri: Uri): void { + const projectIgnores = this.computeNestedProjectIgnores(workspaceUri); + const projects = this.getProjectsArray(workspaceUri); + + for (const project of projects) { + const ignorePaths = projectIgnores.get(getProjectId(project.projectUri)); + if (ignorePaths && ignorePaths.length > 0) { + project.nestedProjectPathsToIgnore = ignorePaths; + traceInfo(`[test-by-project] ${project.projectName} will ignore nested: ${ignorePaths.join(', ')}`); + } + } + } + + /** + * Clears all projects for a workspace. + */ + public clearWorkspace(workspaceUri: Uri): void { + this.workspaceProjects.delete(workspaceUri); + } + + // ====== Private Methods ====== + + /** + * Discovers Python projects in a workspace using the Python Environment API. + * Falls back to creating a single default project if API is unavailable. + */ + private async discoverProjects(workspaceUri: Uri): Promise { + try { + if (!useEnvExtension()) { + traceInfo('[test-by-project] Python Environments API not available, using default project'); + return [await this.createDefaultProject(workspaceUri)]; + } + + const envExtApi = await getEnvExtApi(); + const allProjects = envExtApi.getPythonProjects(); + traceInfo(`[test-by-project] Found ${allProjects.length} total Python projects from API`); + + // Filter to projects within this workspace + const workspaceProjects = allProjects.filter((project) => + isParentPath(project.uri.fsPath, workspaceUri.fsPath), + ); + traceInfo(`[test-by-project] Filtered to ${workspaceProjects.length} projects in workspace`); + + if (workspaceProjects.length === 0) { + traceInfo('[test-by-project] No projects found, creating default project'); + return [await this.createDefaultProject(workspaceUri)]; + } + + // Create ProjectAdapter for each discovered project + const adapters: ProjectAdapter[] = []; + for (const pythonProject of workspaceProjects) { + try { + const adapter = await this.createProjectAdapter(pythonProject, workspaceUri); + adapters.push(adapter); + } catch (error) { + traceError(`[test-by-project] Failed to create adapter for ${pythonProject.uri.fsPath}:`, error); + } + } + + if (adapters.length === 0) { + traceInfo('[test-by-project] All adapters failed, falling back to default project'); + return [await this.createDefaultProject(workspaceUri)]; + } + + return adapters; + } catch (error) { + traceError('[test-by-project] Discovery failed, using default project:', error); + return [await this.createDefaultProject(workspaceUri)]; + } + } + + /** + * Creates a ProjectAdapter from a PythonProject. + * + * Each project gets its own isolated test infrastructure: + * - **ResultResolver:** Handles mapping test IDs and processing results for this project + * - **DiscoveryAdapter:** Discovers tests scoped to this project's root directory + * - **ExecutionAdapter:** Runs tests for this project using its Python environment + * + */ + private async createProjectAdapter(pythonProject: PythonProject, workspaceUri: Uri): Promise { + const projectId = getProjectId(pythonProject.uri); + traceInfo(`[test-by-project] Creating adapter for: ${pythonProject.name} at ${projectId}`); + + // Resolve Python environment + const envExtApi = await getEnvExtApi(); + const pythonEnvironment = await envExtApi.getEnvironment(pythonProject.uri); + if (!pythonEnvironment) { + throw new Error(`No Python environment found for project ${projectId}`); + } + + // Create test infrastructure + const testProvider = this.getTestProvider(workspaceUri); + const projectDisplayName = createProjectDisplayName(pythonProject.name, pythonEnvironment.version); + const resultResolver = new PythonResultResolver( + this.testController, + testProvider, + workspaceUri, + projectId, + pythonProject.name, // Use simple project name for test tree label (without version) + ); + const { discoveryAdapter, executionAdapter } = createTestAdapters( + testProvider, + resultResolver, + this.configSettings, + this.envVarsService, + ); + + return { + projectName: projectDisplayName, + projectUri: pythonProject.uri, + workspaceUri, + pythonProject, + pythonEnvironment, + testProvider, + discoveryAdapter, + executionAdapter, + resultResolver, + isDiscovering: false, + isExecuting: false, + }; + } + + /** + * Creates a default project for legacy/fallback mode. + */ + private async createDefaultProject(workspaceUri: Uri): Promise { + traceInfo(`[test-by-project] Creating default project for: ${workspaceUri.fsPath}`); + + const testProvider = this.getTestProvider(workspaceUri); + const resultResolver = new PythonResultResolver(this.testController, testProvider, workspaceUri); + const { discoveryAdapter, executionAdapter } = createTestAdapters( + testProvider, + resultResolver, + this.configSettings, + this.envVarsService, + ); + + const interpreter = await this.interpreterService.getActiveInterpreter(workspaceUri); + + const pythonEnvironment: PythonEnvironment = { + name: 'default', + displayName: interpreter?.displayName || 'Python', + shortDisplayName: interpreter?.displayName || 'Python', + displayPath: interpreter?.path || 'python', + version: interpreter?.version?.raw || '3.x', + environmentPath: Uri.file(interpreter?.path || 'python'), + sysPrefix: interpreter?.sysPrefix || '', + execInfo: { run: { executable: interpreter?.path || 'python' } }, + envId: { id: 'default', managerId: 'default' }, + }; + + const pythonProject: PythonProject = { + name: path.basename(workspaceUri.fsPath) || 'workspace', + uri: workspaceUri, + }; + + return { + projectName: pythonProject.name, + projectUri: workspaceUri, + workspaceUri, + pythonProject, + pythonEnvironment, + testProvider, + discoveryAdapter, + executionAdapter, + resultResolver, + isDiscovering: false, + isExecuting: false, + }; + } + + /** + * Identifies nested projects and returns ignore paths for parent projects. + * + * **Time complexity:** O(n²) where n is the number of projects in the workspace. + * For each project, checks all other projects to find nested relationships. + * + * Note: Uses path.normalize() to handle Windows path separator inconsistencies + * (e.g., paths from URI.fsPath may have mixed separators). + */ + private computeNestedProjectIgnores(workspaceUri: Uri): Map { + const ignoreMap = new Map(); + const projects = this.getProjectsArray(workspaceUri); + + if (projects.length === 0) return ignoreMap; + + for (const parent of projects) { + const nestedPaths: string[] = []; + + for (const child of projects) { + // Skip self-comparison using URI + if (parent.projectUri.toString() === child.projectUri.toString()) continue; + + // Normalize paths to handle Windows path separator inconsistencies + const parentNormalized = path.normalize(parent.projectUri.fsPath); + const childNormalized = path.normalize(child.projectUri.fsPath); + + // Add trailing separator to ensure we match directory boundaries + const parentWithSep = parentNormalized.endsWith(path.sep) + ? parentNormalized + : parentNormalized + path.sep; + const childWithSep = childNormalized.endsWith(path.sep) ? childNormalized : childNormalized + path.sep; + + // Check if child is inside parent (case-insensitive for Windows) + const childIsInsideParent = childWithSep.toLowerCase().startsWith(parentWithSep.toLowerCase()); + + if (childIsInsideParent) { + nestedPaths.push(child.projectUri.fsPath); + traceInfo(`[test-by-project] Nested: ${child.projectName} is inside ${parent.projectName}`); + } + } + + if (nestedPaths.length > 0) { + ignoreMap.set(getProjectId(parent.projectUri), nestedPaths); + } + } + + return ignoreMap; + } + + /** + * Determines the test provider based on workspace settings. + */ + private getTestProvider(workspaceUri: Uri): TestProvider { + const settings = this.configSettings.getSettings(workspaceUri); + return settings.testing.unittestEnabled ? UNITTEST_PROVIDER : 'pytest'; + } +} diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 6121b3e24442..017c41cf3d97 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -16,6 +16,7 @@ import { import { ITestDebugLauncher } from '../../common/types'; import { IPythonExecutionFactory } from '../../../common/process/types'; import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { ProjectAdapter } from './projectAdapter'; export enum TestDataKinds { Workspace, @@ -142,10 +143,18 @@ export type TestCommandOptions = { // triggerRunDataReceivedEvent(data: DataReceivedEvent): void; // triggerDiscoveryDataReceivedEvent(data: DataReceivedEvent): void; // } -export interface ITestResultResolver { + +/** + * Test item mapping interface used by populateTestTree. + * Contains only the maps needed for building the test tree. + */ +export interface ITestItemMappings { runIdToVSid: Map; runIdToTestItem: Map; vsIdToRunId: Map; +} + +export interface ITestResultResolver extends ITestItemMappings { detailedCoverageMap: Map; resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void; @@ -160,6 +169,7 @@ export interface ITestDiscoveryAdapter { executionFactory: IPythonExecutionFactory, token?: CancellationToken, interpreter?: PythonEnvironment, + project?: ProjectAdapter, ): Promise; } @@ -173,6 +183,7 @@ export interface ITestExecutionAdapter { executionFactory: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, interpreter?: PythonEnvironment, + project?: ProjectAdapter, ): Promise; } diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 7c2a3da42696..9782487d940b 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -13,11 +13,12 @@ import { DiscoveredTestNode, DiscoveredTestPayload, ExecutionTestPayload, - ITestResultResolver, + ITestItemMappings, } from './types'; import { Deferred, createDeferred } from '../../../common/utils/async'; import { createReaderPipe, generateRandomPipeName } from '../../../common/pipes/namedPipes'; import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { PROJECT_ID_SEPARATOR } from './projectUtils'; export function fixLogLinesNoTrailing(content: string): string { const lines = content.split(/\r?\n/g); @@ -196,7 +197,12 @@ function extractMissingModuleName(message: string): string | undefined { return undefined; } -export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { +export function buildErrorNodeOptions( + uri: Uri, + message: string, + testType: string, + projectName?: string, +): ErrorTestItemOptions { let labelText = testType === 'pytest' ? 'pytest Discovery Error' : 'Unittest Discovery Error'; let errorMessage = message; @@ -207,9 +213,12 @@ export function buildErrorNodeOptions(uri: Uri, message: string, testType: strin errorMessage = `The module '${missingModule}' is not installed in the selected Python environment. Please install it to enable test discovery.`; } + // Use project name for label if available (project-based testing), otherwise use folder name + const displayName = projectName ?? path.basename(uri.fsPath); + return { id: `DiscoveryError:${uri.fsPath}`, - label: `${labelText} [${path.basename(uri.fsPath)}]`, + label: `${labelText} [${displayName}]`, error: errorMessage, }; } @@ -218,12 +227,18 @@ export function populateTestTree( testController: TestController, testTreeData: DiscoveredTestNode, testRoot: TestItem | undefined, - resultResolver: ITestResultResolver, + testItemMappings: ITestItemMappings, token?: CancellationToken, + projectId?: string, + projectName?: string, ): void { // If testRoot is undefined, use the info of the root item of testTreeData to create a test item, and append it to the test controller. if (!testRoot) { - testRoot = testController.createTestItem(testTreeData.path, testTreeData.name, Uri.file(testTreeData.path)); + // Create project-scoped ID if projectId is provided + const rootId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${testTreeData.path}` : testTreeData.path; + // Use "Project: {name}" label for project-based testing, otherwise use folder name + const rootLabel = projectName ? `Project: ${projectName}` : testTreeData.name; + testRoot = testController.createTestItem(rootId, rootLabel, Uri.file(testTreeData.path)); testRoot.canResolveChildren = true; testRoot.tags = [RunTestTag, DebugTestTag]; @@ -235,7 +250,9 @@ export function populateTestTree( testTreeData.children.forEach((child) => { if (!token?.isCancellationRequested) { if (isTestItem(child)) { - const testItem = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); + // Create project-scoped vsId + const vsId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${child.id_}` : child.id_; + const testItem = testController.createTestItem(vsId, child.name, Uri.file(child.path)); testItem.tags = [RunTestTag, DebugTestTag]; let range: Range | undefined; @@ -254,15 +271,17 @@ export function populateTestTree( testItem.tags = [RunTestTag, DebugTestTag]; testRoot!.children.add(testItem); - // add to our map - resultResolver.runIdToTestItem.set(child.runID, testItem); - resultResolver.runIdToVSid.set(child.runID, child.id_); - resultResolver.vsIdToRunId.set(child.id_, child.runID); + // add to our map - use runID as key, vsId as value + testItemMappings.runIdToTestItem.set(child.runID, testItem); + testItemMappings.runIdToVSid.set(child.runID, vsId); + testItemMappings.vsIdToRunId.set(vsId, child.runID); } else { - let node = testController.items.get(child.path); + // Use project-scoped ID for non-test nodes and look up within the current root + const nodeId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${child.id_}` : child.id_; + let node = testRoot!.children.get(nodeId); if (!node) { - node = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); + node = testController.createTestItem(nodeId, child.name, Uri.file(child.path)); node.canResolveChildren = true; node.tags = [RunTestTag, DebugTestTag]; @@ -283,7 +302,7 @@ export function populateTestTree( testRoot!.children.add(node); } - populateTestTree(testController, child, node, resultResolver, token); + populateTestTree(testController, child, node, testItemMappings, token, projectId, projectName); } } }); diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 8c8ce422e3c1..04de209c171d 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -29,29 +29,25 @@ import { IConfigurationService, IDisposableRegistry, Resource } from '../../comm import { DelayedTrigger, IDelayedTrigger } from '../../common/utils/delayTrigger'; import { noop } from '../../common/utils/misc'; import { IInterpreterService } from '../../interpreter/contracts'; -import { traceError, traceVerbose } from '../../logging'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; import { IEventNamePropertyMapping, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../common/constants'; import { TestProvider } from '../types'; import { createErrorTestItem, DebugTestTag, getNodeByUri, RunTestTag } from './common/testItemUtilities'; import { buildErrorNodeOptions } from './common/utils'; -import { - ITestController, - ITestDiscoveryAdapter, - ITestFrameworkController, - TestRefreshOptions, - ITestExecutionAdapter, -} from './common/types'; -import { UnittestTestDiscoveryAdapter } from './unittest/testDiscoveryAdapter'; -import { UnittestTestExecutionAdapter } from './unittest/testExecutionAdapter'; -import { PytestTestDiscoveryAdapter } from './pytest/pytestDiscoveryAdapter'; -import { PytestTestExecutionAdapter } from './pytest/pytestExecutionAdapter'; +import { ITestController, ITestFrameworkController, TestRefreshOptions } from './common/types'; import { WorkspaceTestAdapter } from './workspaceTestAdapter'; import { ITestDebugLauncher } from '../common/types'; import { PythonResultResolver } from './common/resultResolver'; import { onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis'; import { IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { ProjectAdapter } from './common/projectAdapter'; +import { TestProjectRegistry } from './common/testProjectRegistry'; +import { createTestAdapters, getProjectId } from './common/projectUtils'; +import { executeTestsForProjects } from './common/projectTestExecution'; +import { useEnvExtension, getEnvExtApi } from '../../envExt/api.internal'; +import { DidChangePythonProjectsEventArgs, PythonProject } from '../../envExt/types'; // Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER]; @@ -62,8 +58,12 @@ type TriggerType = EventPropertyType[TriggerKeyType]; export class PythonTestController implements ITestController, IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + // Legacy: Single workspace test adapter per workspace (backward compatibility) private readonly testAdapters: Map = new Map(); + // Registry for multi-project testing (one registry instance manages all projects across workspaces) + private readonly projectRegistry: TestProjectRegistry; + private readonly triggerTypes: TriggerType[] = []; private readonly testController: TestController; @@ -105,6 +105,14 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.testController = tests.createTestController('python-tests', 'Python Tests'); this.disposables.push(this.testController); + // Initialize project registry for multi-project testing support + this.projectRegistry = new TestProjectRegistry( + this.testController, + this.configSettings, + this.interpreterService, + this.envVarsService, + ); + const delayTrigger = new DelayedTrigger( (uri: Uri, invalidate: boolean) => { this.refreshTestDataInternal(uri); @@ -160,60 +168,260 @@ export class PythonTestController implements ITestController, IExtensionSingleAc }; } + /** + * Determines the test provider (pytest or unittest) based on workspace settings. + */ + private getTestProvider(workspaceUri: Uri): TestProvider { + const settings = this.configSettings.getSettings(workspaceUri); + return settings.testing.unittestEnabled ? UNITTEST_PROVIDER : PYTEST_PROVIDER; + } + + /** + * Sets up file watchers for test discovery triggers. + */ + private setupFileWatchers(workspace: WorkspaceFolder): void { + const settings = this.configSettings.getSettings(workspace.uri); + if (settings.testing.autoTestDiscoverOnSaveEnabled) { + traceVerbose(`Testing: Setting up watcher for ${workspace.uri.fsPath}`); + this.watchForSettingsChanges(workspace); + this.watchForTestContentChangeOnSave(); + } + } + + /** + * Activates the test controller for all workspaces. + * + * Two activation modes: + * 1. **Project-based mode** (when Python Environments API available): + * 2. **Legacy mode** (fallback): + * + * Uses `Promise.allSettled` for resilient multi-workspace activation: + */ public async activate(): Promise { const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; + + // PROJECT-BASED MODE: Uses Python Environments API to discover projects + // Each project becomes its own test tree root with its own Python environment + if (useEnvExtension()) { + traceInfo('[test-by-project] Activating project-based testing mode'); + + // Discover projects in parallel across all workspaces + // Promise.allSettled ensures one workspace failure doesn't block others + const results = await Promise.allSettled( + Array.from(workspaces).map(async (workspace) => { + // Queries Python Environments API and creates ProjectAdapter instances + const projects = await this.projectRegistry.discoverAndRegisterProjects(workspace.uri); + return { workspace, projectCount: projects.length }; + }), + ); + + // Process results: successful workspaces get file watchers, failed ones fall back to legacy + results.forEach((result, index) => { + const workspace = workspaces[index]; + if (result.status === 'fulfilled') { + traceInfo( + `[test-by-project] Activated ${result.value.projectCount} project(s) for ${workspace.uri.fsPath}`, + ); + this.setupFileWatchers(workspace); + } else { + // Graceful degradation: if project discovery fails, use legacy single-adapter mode + traceError(`[test-by-project] Failed for ${workspace.uri.fsPath}:`, result.reason); + this.activateLegacyWorkspace(workspace); + } + }); + // Subscribe to project changes to update test tree when projects are added/removed + await this.subscribeToProjectChanges(); + return; + } + + // LEGACY MODE: Single WorkspaceTestAdapter per workspace (backward compatibility) workspaces.forEach((workspace) => { - const settings = this.configSettings.getSettings(workspace.uri); + this.activateLegacyWorkspace(workspace); + }); + } + + /** + * Subscribes to Python project changes from the Python Environments API. + * When projects are added or removed, updates the test tree accordingly. + */ + private async subscribeToProjectChanges(): Promise { + try { + const envExtApi = await getEnvExtApi(); + this.disposables.push( + envExtApi.onDidChangePythonProjects((event: DidChangePythonProjectsEventArgs) => { + this.handleProjectChanges(event).catch((error) => { + traceError('[test-by-project] Error handling project changes:', error); + }); + }), + ); + traceInfo('[test-by-project] Subscribed to Python project changes'); + } catch (error) { + traceError('[test-by-project] Failed to subscribe to project changes:', error); + } + } - let discoveryAdapter: ITestDiscoveryAdapter; - let executionAdapter: ITestExecutionAdapter; - let testProvider: TestProvider; - let resultResolver: PythonResultResolver; + /** + * Handles changes to Python projects (added or removed). + * Cleans up stale test items and re-discovers projects and tests for affected workspaces. + */ + private async handleProjectChanges(event: DidChangePythonProjectsEventArgs): Promise { + const { added, removed } = event; - if (settings.testing.unittestEnabled) { - testProvider = UNITTEST_PROVIDER; - resultResolver = new PythonResultResolver(this.testController, testProvider, workspace.uri); - discoveryAdapter = new UnittestTestDiscoveryAdapter( - this.configSettings, - resultResolver, - this.envVarsService, - ); - executionAdapter = new UnittestTestExecutionAdapter( - this.configSettings, - resultResolver, - this.envVarsService, - ); - } else { - testProvider = PYTEST_PROVIDER; - resultResolver = new PythonResultResolver(this.testController, testProvider, workspace.uri); - discoveryAdapter = new PytestTestDiscoveryAdapter( - this.configSettings, - resultResolver, - this.envVarsService, - ); - executionAdapter = new PytestTestExecutionAdapter( - this.configSettings, - resultResolver, - this.envVarsService, - ); + if (added.length === 0 && removed.length === 0) { + return; + } + + traceInfo(`[test-by-project] Project changes detected: ${added.length} added, ${removed.length} removed`); + + // Find all affected workspaces + const affectedWorkspaces = new Set(); + + const findWorkspace = (project: PythonProject): WorkspaceFolder | undefined => { + return this.workspaceService.getWorkspaceFolder(project.uri); + }; + + for (const project of [...added, ...removed]) { + const workspace = findWorkspace(project); + if (workspace) { + affectedWorkspaces.add(workspace); } + } - const workspaceTestAdapter = new WorkspaceTestAdapter( - testProvider, - discoveryAdapter, - executionAdapter, - workspace.uri, - resultResolver, - ); + // For each affected workspace, clean up and re-discover + for (const workspace of affectedWorkspaces) { + traceInfo(`[test-by-project] Re-discovering projects for workspace: ${workspace.uri.fsPath}`); - this.testAdapters.set(workspace.uri, workspaceTestAdapter); + // Get the current projects before clearing to know what to clean up + const existingProjects = this.projectRegistry.getProjectsArray(workspace.uri); - if (settings.testing.autoTestDiscoverOnSaveEnabled) { - traceVerbose(`Testing: Setting up watcher for ${workspace.uri.fsPath}`); - this.watchForSettingsChanges(workspace); - this.watchForTestContentChangeOnSave(); + // Remove ALL test items for the affected workspace's projects + // This ensures no stale items remain from deleted/changed projects + this.removeWorkspaceProjectTestItems(workspace.uri, existingProjects); + + // Also explicitly remove test items for removed projects (in case they weren't tracked) + for (const project of removed) { + const projectWorkspace = findWorkspace(project); + if (projectWorkspace?.uri.toString() === workspace.uri.toString()) { + this.removeProjectTestItems(project); + } + } + + // Re-discover all projects and tests for the workspace in a single pass. + // discoverAllProjectsInWorkspace is responsible for clearing/re-registering + // projects and performing test discovery for the workspace. + await this.discoverAllProjectsInWorkspace(workspace.uri); + } + } + + /** + * Removes all test items associated with projects in a workspace. + * Used to clean up stale items before re-discovery. + */ + private removeWorkspaceProjectTestItems(workspaceUri: Uri, projects: ProjectAdapter[]): void { + const idsToRemove: string[] = []; + + // Collect IDs of test items belonging to any project in this workspace + for (const project of projects) { + const projectIdPrefix = getProjectId(project.projectUri); + const projectFsPath = project.projectUri.fsPath; + + this.testController.items.forEach((item) => { + // Match by project ID prefix (e.g., "file:///path@@vsc@@...") + if (item.id.startsWith(projectIdPrefix)) { + idsToRemove.push(item.id); + } + // Match by fsPath in ID (legacy items might use path directly) + else if (item.id.includes(projectFsPath)) { + idsToRemove.push(item.id); + } + // Match by item URI being within project directory + else if (item.uri && item.uri.fsPath.startsWith(projectFsPath)) { + idsToRemove.push(item.id); + } + }); + } + + // Also remove any items whose URI is within the workspace (catch-all for edge cases) + this.testController.items.forEach((item) => { + if ( + item.uri && + this.workspaceService.getWorkspaceFolder(item.uri)?.uri.toString() === workspaceUri.toString() + ) { + if (!idsToRemove.includes(item.id)) { + idsToRemove.push(item.id); + } } }); + + // Remove all collected items + for (const id of idsToRemove) { + this.testController.items.delete(id); + } + + traceInfo( + `[test-by-project] Cleaned up ${idsToRemove.length} test items for workspace: ${workspaceUri.fsPath}`, + ); + } + + /** + * Removes test items associated with a specific project from the test controller. + * Matches items by project ID prefix, fsPath, or URI. + */ + private removeProjectTestItems(project: PythonProject): void { + const projectId = getProjectId(project.uri); + const projectFsPath = project.uri.fsPath; + const idsToRemove: string[] = []; + + // Find all root items that belong to this project + this.testController.items.forEach((item) => { + // Match by project ID prefix (e.g., "file:///path@@vsc@@...") + if (item.id.startsWith(projectId)) { + idsToRemove.push(item.id); + } + // Match by fsPath in ID (items might use path directly without URI prefix) + else if (item.id.startsWith(projectFsPath) || item.id.includes(projectFsPath)) { + idsToRemove.push(item.id); + } + // Match by item URI being within project directory + else if (item.uri && item.uri.fsPath.startsWith(projectFsPath)) { + idsToRemove.push(item.id); + } + }); + + for (const id of idsToRemove) { + this.testController.items.delete(id); + traceVerbose(`[test-by-project] Removed test item: ${id}`); + } + + if (idsToRemove.length > 0) { + traceInfo(`[test-by-project] Removed ${idsToRemove.length} test items for project: ${project.name}`); + } + } + + /** + * Activates testing for a workspace using the legacy single-adapter approach. + * Used for backward compatibility when project-based testing is disabled or unavailable. + */ + private activateLegacyWorkspace(workspace: WorkspaceFolder): void { + const testProvider = this.getTestProvider(workspace.uri); + const resultResolver = new PythonResultResolver(this.testController, testProvider, workspace.uri); + const { discoveryAdapter, executionAdapter } = createTestAdapters( + testProvider, + resultResolver, + this.configSettings, + this.envVarsService, + ); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + testProvider, + discoveryAdapter, + executionAdapter, + workspace.uri, + resultResolver, + ); + + this.testAdapters.set(workspace.uri, workspaceTestAdapter); + this.setupFileWatchers(workspace); } public refreshTestData(uri?: Resource, options?: TestRefreshOptions): Promise { @@ -255,9 +463,9 @@ export class PythonTestController implements ITestController, IExtensionSingleAc this.refreshingStartedEvent.fire(); try { if (uri) { - await this.refreshSingleWorkspace(uri); + await this.discoverTestsInWorkspace(uri); } else { - await this.refreshAllWorkspaces(); + await this.discoverTestsInAllWorkspaces(); } } finally { this.refreshingCompletedEvent.fire(); @@ -266,8 +474,18 @@ export class PythonTestController implements ITestController, IExtensionSingleAc /** * Discovers tests for a single workspace. + * + * **Discovery flow:** + * 1. If the workspace has registered projects (via Python Environments API), + * uses project-based discovery: each project is discovered independently + * with its own Python environment and test adapters. + * 2. Otherwise, falls back to legacy mode: a single WorkspaceTestAdapter + * discovers all tests in the workspace using the active interpreter. + * + * In project-based mode, the test tree will have separate roots for each project. + * In legacy mode, the workspace folder is the single test tree root. */ - private async refreshSingleWorkspace(uri: Uri): Promise { + private async discoverTestsInWorkspace(uri: Uri): Promise { const workspace = this.workspaceService.getWorkspaceFolder(uri); if (!workspace?.uri) { traceError('Unable to find workspace for given file'); @@ -280,40 +498,137 @@ export class PythonTestController implements ITestController, IExtensionSingleAc // Ensure we send test telemetry if it gets disabled again this.sendTestDisabledTelemetry = true; + // Check if any test framework is enabled BEFORE project-based discovery + // This ensures the config screen stays visible when testing is disabled + if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { + await this.handleNoTestProviderEnabled(workspace); + return; + } + + // Use project-based discovery if applicable (only reached if testing is enabled) + if (this.projectRegistry.hasProjects(workspace.uri)) { + await this.discoverAllProjectsInWorkspace(workspace.uri); + return; + } + + // Legacy mode: Single workspace adapter if (settings.testing.pytestEnabled) { - await this.discoverTestsForProvider(workspace.uri, 'pytest'); + await this.discoverWorkspaceTestsLegacy(workspace.uri, 'pytest'); } else if (settings.testing.unittestEnabled) { - await this.discoverTestsForProvider(workspace.uri, 'unittest'); - } else { - await this.handleNoTestProviderEnabled(workspace); + await this.discoverWorkspaceTestsLegacy(workspace.uri, 'unittest'); } } /** - * Discovers tests for all workspaces in the workspace folders. + * Discovers tests for all projects within a workspace (project-based mode). + * Re-discovers projects from the Python Environments API before running test discovery. + * This ensures the test tree stays in sync with project changes. */ - private async refreshAllWorkspaces(): Promise { + private async discoverAllProjectsInWorkspace(workspaceUri: Uri): Promise { + // Defensive check: ensure testing is enabled (should be checked by caller, but be safe) + const settings = this.configSettings.getSettings(workspaceUri); + if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { + traceVerbose('[test-by-project] Skipping discovery - no test framework enabled'); + return; + } + + // Get existing projects before re-discovery for cleanup + const existingProjects = this.projectRegistry.getProjectsArray(workspaceUri); + + // Clean up all existing test items for this workspace + // This ensures stale items from deleted/changed projects are removed + this.removeWorkspaceProjectTestItems(workspaceUri, existingProjects); + + // Re-discover projects from Python Environments API + // This picks up any added/removed projects since last discovery + this.projectRegistry.clearWorkspace(workspaceUri); + const projects = await this.projectRegistry.discoverAndRegisterProjects(workspaceUri); + + if (projects.length === 0) { + traceError(`[test-by-project] No projects found for workspace: ${workspaceUri.fsPath}`); + return; + } + + traceInfo(`[test-by-project] Starting discovery for ${projects.length} project(s) in workspace`); + + try { + // Configure nested project exclusions before discovery + this.projectRegistry.configureNestedProjectIgnores(workspaceUri); + + // Track completion for progress logging + const projectsCompleted = new Set(); + + // Run discovery for all projects in parallel + await Promise.all(projects.map((project) => this.discoverTestsForProject(project, projectsCompleted))); + + traceInfo( + `[test-by-project] Discovery complete: ${projectsCompleted.size}/${projects.length} projects completed`, + ); + } catch (error) { + traceError(`[test-by-project] Discovery failed for workspace ${workspaceUri.fsPath}:`, error); + } + } + + /** + * Discovers tests for a single project (project-based mode). + * Creates test tree items rooted at the project's directory. + */ + private async discoverTestsForProject(project: ProjectAdapter, projectsCompleted: Set): Promise { + try { + traceInfo(`[test-by-project] Discovering tests for project: ${project.projectName}`); + project.isDiscovering = true; + + // In project-based mode, the discovery adapter uses the Python Environments API + // to get the environment directly, so we don't need to pass the interpreter + await project.discoveryAdapter.discoverTests( + project.projectUri, + this.pythonExecFactory, + this.refreshCancellation.token, + undefined, // Interpreter not needed; adapter uses Python Environments API + project, + ); + + // Mark project as completed (use URI string as unique key) + projectsCompleted.add(project.projectUri.toString()); + traceInfo(`[test-by-project] Project ${project.projectName} discovery completed`); + } catch (error) { + traceError(`[test-by-project] Discovery failed for project ${project.projectName}:`, error); + // Individual project failures don't block others + projectsCompleted.add(project.projectUri.toString()); // Still mark as completed + } finally { + project.isDiscovering = false; + } + } + + /** + * Discovers tests across all workspace folders. + * Iterates each workspace and triggers discovery. + */ + private async discoverTestsInAllWorkspaces(): Promise { traceVerbose('Testing: Refreshing all test data'); const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; await Promise.all( workspaces.map(async (workspace) => { - if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { - this.commandManager - .executeCommand(constants.Commands.TriggerEnvironmentSelection, workspace.uri) - .then(noop, noop); - return; + // In project-based mode, each project has its own environment, + // so we don't require a global active interpreter + if (!useEnvExtension()) { + if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { + this.commandManager + .executeCommand(constants.Commands.TriggerEnvironmentSelection, workspace.uri) + .then(noop, noop); + return; + } } - await this.refreshSingleWorkspace(workspace.uri); + await this.discoverTestsInWorkspace(workspace.uri); }), ); } /** - * Discovers tests for a specific test provider (pytest or unittest). - * Validates that the adapter's provider matches the expected provider. + * Discovers tests for a workspace using legacy single-adapter mode. */ - private async discoverTestsForProvider(workspaceUri: Uri, expectedProvider: TestProvider): Promise { + private async discoverWorkspaceTestsLegacy(workspaceUri: Uri, expectedProvider: TestProvider): Promise { const testAdapter = this.testAdapters.get(workspaceUri); if (!testAdapter) { @@ -386,9 +701,13 @@ export class PythonTestController implements ITestController, IExtensionSingleAc const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; await Promise.all( workspaces.map(async (workspace) => { - if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { - traceError('Cannot trigger test discovery as a valid interpreter is not selected'); - return; + // In project-based mode, each project has its own environment, + // so we don't require a global active interpreter + if (!useEnvExtension()) { + if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { + traceError('Cannot trigger test discovery as a valid interpreter is not selected'); + return; + } } await this.refreshTestDataInternal(workspace.uri); }), @@ -472,8 +791,30 @@ export class PythonTestController implements ITestController, IExtensionSingleAc return; } - const testAdapter = - this.testAdapters.get(workspace.uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); + // Check if we're in project-based mode and should use project-specific execution + if (this.projectRegistry.hasProjects(workspace.uri)) { + const projects = this.projectRegistry.getProjectsArray(workspace.uri); + await executeTestsForProjects(projects, testItems, runInstance, request, token, { + projectRegistry: this.projectRegistry, + pythonExecFactory: this.pythonExecFactory, + debugLauncher: this.debugLauncher, + }); + return; + } + + // For unittest (or pytest when not in project mode), use the legacy WorkspaceTestAdapter. + // In project mode, legacy adapters may not be initialized, so create one on demand. + let testAdapter = this.testAdapters.get(workspace.uri); + if (!testAdapter) { + // Initialize legacy adapter on demand (needed for unittest in project mode) + this.activateLegacyWorkspace(workspace); + testAdapter = this.testAdapters.get(workspace.uri); + } + + if (!testAdapter) { + traceError(`[test] No test adapter available for workspace: ${workspace.uri.fsPath}`); + return; + } this.setupCoverageIfNeeded(request, testAdapter); diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 7ad69c71fa0e..16e27635e66c 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -19,6 +19,7 @@ import { PythonEnvironment } from '../../../pythonEnvironments/info'; import { useEnvExtension, getEnvironment, runInBackground } from '../../../envExt/api.internal'; import { buildPytestEnv as configureSubprocessEnv, handleSymlinkAndRootDir } from './pytestHelpers'; import { cleanupOnCancellation, createProcessHandlers, setupDiscoveryPipe } from '../common/discoveryHelpers'; +import { ProjectAdapter } from '../common/projectAdapter'; /** * Configures the subprocess environment for pytest discovery. @@ -53,6 +54,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { executionFactory: IPythonExecutionFactory, token?: CancellationToken, interpreter?: PythonEnvironment, + project?: ProjectAdapter, ): Promise { // Setup discovery pipe and cancellation const { @@ -76,6 +78,18 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { let { pytestArgs } = settings.testing; const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; pytestArgs = await handleSymlinkAndRootDir(cwd, pytestArgs); + + // Add --ignore flags for nested projects to prevent duplicate discovery + if (project?.nestedProjectPathsToIgnore?.length) { + const ignoreArgs = project.nestedProjectPathsToIgnore.map((nestedPath) => `--ignore=${nestedPath}`); + pytestArgs = [...pytestArgs, ...ignoreArgs]; + traceInfo( + `[test-by-project] Project ${project.projectName} ignoring nested project(s): ${ignoreArgs.join( + ' ', + )}`, + ); + } + const commandArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); traceVerbose( `Running pytest discovery with command: ${commandArgs.join(' ')} for workspace ${uri.fsPath}.`, @@ -84,13 +98,18 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // Configure subprocess environment const mutableEnv = await configureDiscoveryEnv(this.envVarsService, uri, discoveryPipeName); + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + } + // Setup process handlers (shared by both execution paths) const handlers = createProcessHandlers('pytest', uri, cwd, this.resultResolver, deferredTillExecClose, [5]); // Execute using environment extension if available if (useEnvExtension()) { traceInfo(`Using environment extension for pytest discovery in workspace ${uri.fsPath}`); - const pythonEnv = await getEnvironment(uri); + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); if (!pythonEnv) { traceError( `Python environment not found for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 3b2f9f7de33a..102841c2e2dd 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -21,6 +21,7 @@ import * as utils from '../common/utils'; import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; import { PythonEnvironment } from '../../../pythonEnvironments/info'; import { getEnvironment, runInBackground, useEnvExtension } from '../../../envExt/api.internal'; +import { ProjectAdapter } from '../common/projectAdapter'; export class PytestTestExecutionAdapter implements ITestExecutionAdapter { constructor( @@ -37,6 +38,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { executionFactory: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, interpreter?: PythonEnvironment, + project?: ProjectAdapter, ): Promise { const deferredTillServerClose: Deferred = utils.createTestingDeferred(); @@ -71,6 +73,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { executionFactory, debugLauncher, interpreter, + project, ); } finally { await deferredTillServerClose.promise; @@ -87,6 +90,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { executionFactory: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, interpreter?: PythonEnvironment, + project?: ProjectAdapter, ): Promise { const relativePathToPytest = 'python_files'; const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); @@ -102,9 +106,17 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); mutableEnv.PYTHONPATH = pythonPathCommand; mutableEnv.TEST_RUN_PIPE = resultNamedPipeName; + + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + traceInfo(`[test-by-project] Setting PROJECT_ROOT_PATH=${project.projectUri.fsPath} for pytest execution`); + } + if (profileKind && profileKind === TestRunProfileKind.Coverage) { mutableEnv.COVERAGE_ENABLED = 'True'; } + const debugBool = profileKind && profileKind === TestRunProfileKind.Debug; // Create the Python environment in which to execute the command. @@ -155,6 +167,8 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { testProvider: PYTEST_PROVIDER, runTestIdsPort: testIdsFileName, pytestPort: resultNamedPipeName, + // Pass project for project-based debugging (Python path and session name derived from this) + project: project?.pythonProject, }; const sessionOptions: DebugSessionOptions = { testRun: runInstance, @@ -168,7 +182,9 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { sessionOptions, ); } else if (useEnvExtension()) { - const pythonEnv = await getEnvironment(uri); + // For project-based execution, use the project's Python environment + // Otherwise, fall back to getting the environment from the URI + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); if (pythonEnv) { const deferredTillExecClose: Deferred = utils.createTestingDeferred(); diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index 7c986e95a449..558e01f3514d 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -18,6 +18,7 @@ import { PythonEnvironment } from '../../../pythonEnvironments/info'; import { createTestingDeferred } from '../common/utils'; import { buildDiscoveryCommand, buildUnittestEnv as configureSubprocessEnv } from './unittestHelpers'; import { cleanupOnCancellation, createProcessHandlers, setupDiscoveryPipe } from '../common/discoveryHelpers'; +import { ProjectAdapter } from '../common/projectAdapter'; /** * Configures the subprocess environment for unittest discovery. @@ -51,6 +52,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { executionFactory: IPythonExecutionFactory, token?: CancellationToken, interpreter?: PythonEnvironment, + project?: ProjectAdapter, ): Promise { // Setup discovery pipe and cancellation const { @@ -78,13 +80,21 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { // Configure subprocess environment const mutableEnv = await configureDiscoveryEnv(this.envVarsService, uri, discoveryPipeName); + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + traceInfo( + `[test-by-project] Setting PROJECT_ROOT_PATH=${project.projectUri.fsPath} for unittest discovery`, + ); + } + // Setup process handlers (shared by both execution paths) const handlers = createProcessHandlers('unittest', uri, cwd, this.resultResolver, deferredTillExecClose); // Execute using environment extension if available if (useEnvExtension()) { traceInfo(`Using environment extension for unittest discovery in workspace ${uri.fsPath}`); - const pythonEnv = await getEnvironment(uri); + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); if (!pythonEnv) { traceError( `Python environment not found for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index cbc1d2985f84..c7d21b768c5b 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -27,6 +27,8 @@ import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; import { UNITTEST_PROVIDER } from '../../common/constants'; import * as utils from '../common/utils'; import { getEnvironment, runInBackground, useEnvExtension } from '../../../envExt/api.internal'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { ProjectAdapter } from '../common/projectAdapter'; /** * Wrapper Class for unittest test execution. This is where we call `runTestCommand`? @@ -46,6 +48,8 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { runInstance: TestRun, executionFactory: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, + _interpreter?: PythonEnvironment, + project?: ProjectAdapter, ): Promise { // deferredTillServerClose awaits named pipe server close const deferredTillServerClose: Deferred = utils.createTestingDeferred(); @@ -80,6 +84,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { profileKind, executionFactory, debugLauncher, + project, ); } catch (error) { traceError(`Error in running unittest tests: ${error}`); @@ -97,6 +102,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { profileKind: boolean | TestRunProfileKind | undefined, executionFactory: IPythonExecutionFactory, debugLauncher?: ITestDebugLauncher, + project?: ProjectAdapter, ): Promise { const settings = this.configSettings.getSettings(uri); const { unittestArgs } = settings.testing; @@ -111,6 +117,15 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { const pythonPathCommand = [cwd, ...pythonPathParts].join(path.delimiter); mutableEnv.PYTHONPATH = pythonPathCommand; mutableEnv.TEST_RUN_PIPE = resultNamedPipeName; + + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + traceInfo( + `[test-by-project] Setting PROJECT_ROOT_PATH=${project.projectUri.fsPath} for unittest execution`, + ); + } + if (profileKind && profileKind === TestRunProfileKind.Coverage) { mutableEnv.COVERAGE_ENABLED = cwd; } @@ -165,6 +180,8 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { testProvider: UNITTEST_PROVIDER, runTestIdsPort: testIdsFileName, pytestPort: resultNamedPipeName, // change this from pytest + // Pass project for project-based debugging (Python path and session name derived from this) + project: project?.pythonProject, }; const sessionOptions: DebugSessionOptions = { testRun: runInstance, @@ -183,7 +200,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { sessionOptions, ); } else if (useEnvExtension()) { - const pythonEnv = await getEnvironment(uri); + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); if (pythonEnv) { traceInfo(`Running unittest with arguments: ${args.join(' ')} for workspace ${uri.fsPath} \r\n`); const deferredTillExecClose = createDeferred(); diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index 75b9489f708e..f17687732f57 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -15,6 +15,7 @@ import { IPythonExecutionFactory } from '../../common/process/types'; import { ITestDebugLauncher } from '../common/types'; import { buildErrorNodeOptions } from './common/utils'; import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { ProjectAdapter } from './common/projectAdapter'; /** * This class exposes a test-provider-agnostic way of discovering tests. @@ -47,6 +48,7 @@ export class WorkspaceTestAdapter { profileKind?: boolean | TestRunProfileKind, debugLauncher?: ITestDebugLauncher, interpreter?: PythonEnvironment, + project?: ProjectAdapter, ): Promise { if (this.executing) { traceError('Test execution already in progress, not starting a new one.'); @@ -84,6 +86,7 @@ export class WorkspaceTestAdapter { executionFactory, debugLauncher, interpreter, + project, ); deferred.resolve(); } catch (ex) { diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index 397ae03eafc2..86e862103bf6 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -30,6 +30,7 @@ import { TestProvider } from '../../../client/testing/types'; import { isOs, OSType } from '../../common'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { createDeferred } from '../../../client/common/utils/async'; +import * as envExtApi from '../../../client/envExt/api.internal'; use(chaiAsPromised.default); @@ -106,7 +107,7 @@ suite('Unit Tests - Debug Launcher', () => { ); } function setupDebugManager( - workspaceFolder: WorkspaceFolder, + _workspaceFolder: WorkspaceFolder, expected: DebugConfiguration, testProvider: TestProvider, ) { @@ -123,35 +124,48 @@ suite('Unit Tests - Debug Launcher', () => { .returns(() => Promise.resolve(expected.env)); const deferred = createDeferred(); + let capturedConfig: DebugConfiguration | undefined; + // Use TypeMoq.It.isAny() because the implementation adds a session marker to the config debugService - .setup((d) => - d.startDebugging(TypeMoq.It.isValue(workspaceFolder), TypeMoq.It.isValue(expected), undefined), - ) - .returns((_wspc: WorkspaceFolder, _expectedParam: DebugConfiguration) => { + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_wspc: WorkspaceFolder, config: DebugConfiguration) => { + capturedConfig = config; deferred.resolve(); - return Promise.resolve(undefined as any); - }); - - // create a fake debug session that the debug service will return on terminate - const fakeDebugSession = TypeMoq.Mock.ofType(); - fakeDebugSession.setup((ds) => ds.id).returns(() => 'id-val'); - const debugSessionInstance = fakeDebugSession.object; + }) + .returns(() => Promise.resolve(true)); + // Setup onDidStartDebugSession - the new implementation uses this to capture the session debugService - .setup((d) => d.activeDebugSession) - .returns(() => debugSessionInstance) - .verifiable(TypeMoq.Times.once()); + .setup((d) => d.onDidStartDebugSession(TypeMoq.It.isAny())) + .returns((callback) => { + deferred.promise.then(() => { + if (capturedConfig) { + callback(({ + id: 'test-session-id', + configuration: capturedConfig, + } as unknown) as DebugSession); + } + }); + return { dispose: () => {} }; + }); + // Setup onDidTerminateDebugSession - fires after the session starts debugService .setup((d) => d.onDidTerminateDebugSession(TypeMoq.It.isAny())) .returns((callback) => { deferred.promise.then(() => { - callback(debugSessionInstance); + setTimeout(() => { + if (capturedConfig) { + callback(({ + id: 'test-session-id', + configuration: capturedConfig, + } as unknown) as DebugSession); + } + }, 10); }); - return undefined as any; - }) - .verifiable(TypeMoq.Times.once()); + return { dispose: () => {} }; + }); } function createWorkspaceFolder(folderPath: string): WorkspaceFolder { return { @@ -692,4 +706,228 @@ suite('Unit Tests - Debug Launcher', () => { expect(configs).to.be.deep.equal([]); }); + + // ===== PROJECT-BASED DEBUG SESSION TESTS ===== + + suite('Project-based debug sessions', () => { + function setupForProjectTests(options: LaunchOptions) { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'python' } as unknown) as PythonEnvironment)); + settings.setup((p) => p.envFile).returns(() => __filename); + + debugEnvHelper + .setup((x) => x.getEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({})); + + const workspaceFolders = [{ index: 0, name: 'test', uri: Uri.file(options.cwd) }]; + getWorkspaceFoldersStub.returns(workspaceFolders); + getWorkspaceFolderStub.returns(workspaceFolders[0]); + pathExistsStub.resolves(false); + + // Stub useEnvExtension to avoid null reference errors in tests + sinon.stub(envExtApi, 'useEnvExtension').returns(false); + } + + /** + * Helper to setup debug service mocks with proper session lifecycle simulation. + * The implementation uses onDidStartDebugSession to capture the session via marker, + * then onDidTerminateDebugSession to resolve when that session ends. + */ + function setupDebugServiceWithSessionLifecycle(): { + capturedConfigs: DebugConfiguration[]; + } { + const capturedConfigs: DebugConfiguration[] = []; + let startCallback: ((session: DebugSession) => void) | undefined; + let terminateCallback: ((session: DebugSession) => void) | undefined; + + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_, config) => { + capturedConfigs.push(config); + // Simulate the full session lifecycle after startDebugging resolves + setTimeout(() => { + const session = ({ + id: `session-${capturedConfigs.length}`, + configuration: config, + } as unknown) as DebugSession; + // Fire start first (so ourSession is captured) + startCallback?.(session); + // Then fire terminate (so the promise resolves) + setTimeout(() => terminateCallback?.(session), 5); + }, 5); + }) + .returns(() => Promise.resolve(true)); + + debugService + .setup((d) => d.onDidStartDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + startCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + debugService + .setup((d) => d.onDidTerminateDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + terminateCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + return { capturedConfigs }; + } + + test('should use project name in config name when provided', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + project: { name: 'myproject (Python 3.11)', uri: Uri.file('one/two/three') }, + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(1); + expect(capturedConfigs[0].name).to.equal('Debug Tests: myproject (Python 3.11)'); + }); + + test('should use default python when no project provided', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(1); + // Should use the default 'python' from interpreterService mock + expect(capturedConfigs[0].python).to.equal('python'); + }); + + test('should add unique session marker to launch config', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(1); + // Should have a session marker of format 'test-{timestamp}-{random}' + const marker = (capturedConfigs[0] as any).__vscodeTestSessionMarker; + expect(marker).to.be.a('string'); + expect(marker).to.match(/^test-\d+-[a-z0-9]+$/); + }); + + test('should generate unique markers for each launch', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + // Launch twice + await debugLauncher.launchDebugger(options); + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(2); + const marker1 = (capturedConfigs[0] as any).__vscodeTestSessionMarker; + const marker2 = (capturedConfigs[1] as any).__vscodeTestSessionMarker; + expect(marker1).to.not.equal(marker2); + }); + + test('should only resolve when matching session terminates', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + + let capturedConfig: DebugConfiguration | undefined; + let terminateCallback: ((session: DebugSession) => void) | undefined; + let startCallback: ((session: DebugSession) => void) | undefined; + + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_, config) => { + capturedConfig = config; + }) + .returns(() => Promise.resolve(true)); + + debugService + .setup((d) => d.onDidStartDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + startCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + debugService + .setup((d) => d.onDidTerminateDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + terminateCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + const launchPromise = debugLauncher.launchDebugger(options); + + // Wait for config to be captured + await new Promise((r) => setTimeout(r, 10)); + + // Simulate our session starting + const ourSession = ({ + id: 'our-session-id', + configuration: capturedConfig!, + } as unknown) as DebugSession; + startCallback?.(ourSession); + + // Create a different session (like another project's debug) + const otherSession = ({ + id: 'other-session-id', + configuration: { __vscodeTestSessionMarker: 'different-marker' }, + } as unknown) as DebugSession; + + // Terminate the OTHER session first - should NOT resolve our promise + terminateCallback?.(otherSession); + + // Wait a bit to ensure it didn't resolve + let resolved = false; + const checkPromise = launchPromise.then(() => { + resolved = true; + }); + + await new Promise((r) => setTimeout(r, 20)); + expect(resolved).to.be.false; + + // Now terminate OUR session - should resolve + terminateCallback?.(ourSession); + + await checkPromise; + expect(resolved).to.be.true; + }); + }); }); diff --git a/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts index e2133f5c767b..643ea17903e6 100644 --- a/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts +++ b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts @@ -82,4 +82,30 @@ suite('buildErrorNodeOptions - missing module detection', () => { expect(result.label).to.equal('Unittest Discovery Error [workspace]'); expect(result.error).to.equal('Some other error occurred'); }); + + test('Should use project name in label when projectName is provided', () => { + const errorMessage = 'Some error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest', 'my-project'); + + expect(result.label).to.equal('Unittest Discovery Error [my-project]'); + expect(result.error).to.equal('Some error occurred'); + }); + + test('Should use project name in label for pytest when projectName is provided', () => { + const errorMessage = 'Some error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest', 'ada'); + + expect(result.label).to.equal('pytest Discovery Error [ada]'); + expect(result.error).to.equal('Some error occurred'); + }); + + test('Should use folder name when projectName is undefined', () => { + const errorMessage = 'Some error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest', undefined); + + expect(result.label).to.equal('Unittest Discovery Error [workspace]'); + }); }); diff --git a/src/test/testing/testController/common/projectTestExecution.unit.test.ts b/src/test/testing/testController/common/projectTestExecution.unit.test.ts new file mode 100644 index 000000000000..1cce2d1a8ce0 --- /dev/null +++ b/src/test/testing/testController/common/projectTestExecution.unit.test.ts @@ -0,0 +1,740 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { + CancellationToken, + CancellationTokenSource, + TestRun, + TestRunProfile, + TestRunProfileKind, + TestRunRequest, + Uri, +} from 'vscode'; +import { + createMockDependencies, + createMockProjectAdapter, + createMockTestItem, + createMockTestItemWithoutUri, + createMockTestRun, +} from '../testMocks'; +import { + executeTestsForProject, + executeTestsForProjects, + findProjectForTestItem, + getTestCaseNodesRecursive, + groupTestItemsByProject, + setupCoverageForProjects, +} from '../../../../client/testing/testController/common/projectTestExecution'; +import * as telemetry from '../../../../client/telemetry'; +import * as envExtApi from '../../../../client/envExt/api.internal'; + +suite('Project Test Execution', () => { + let sandbox: sinon.SinonSandbox; + let useEnvExtensionStub: sinon.SinonStub; + + setup(() => { + sandbox = sinon.createSandbox(); + // Default to disabled env extension for path-based fallback tests + useEnvExtensionStub = sandbox.stub(envExtApi, 'useEnvExtension').returns(false); + }); + + teardown(() => { + sandbox.restore(); + }); + + // ===== findProjectForTestItem Tests ===== + + suite('findProjectForTestItem', () => { + test('should return undefined when test item has no URI', async () => { + // Mock + const item = createMockTestItemWithoutUri('test1'); + const projects = [createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' })]; + + // Run + const result = await findProjectForTestItem(item, projects); + + // Assert + expect(result).to.be.undefined; + }); + + test('should return matching project when item path is within project directory', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj/tests/test_file.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.equal(project); + }); + + test('should return undefined when item path is outside all project directories', async () => { + // Mock + const item = createMockTestItem('test1', '/other/path/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.be.undefined; + }); + + test('should return most specific (deepest) project when nested projects exist', async () => { + // Mock - parent and child project with overlapping paths + const item = createMockTestItem('test1', '/workspace/parent/child/tests/test.py'); + const parentProject = createMockProjectAdapter({ projectPath: '/workspace/parent', projectName: 'parent' }); + const childProject = createMockProjectAdapter({ + projectPath: '/workspace/parent/child', + projectName: 'child', + }); + + // Run + const result = await findProjectForTestItem(item, [parentProject, childProject]); + + // Assert - should match child (longer path) not parent + expect(result).to.equal(childProject); + }); + + test('should return most specific project regardless of input order', async () => { + // Mock - same as above but different order + const item = createMockTestItem('test1', '/workspace/parent/child/tests/test.py'); + const parentProject = createMockProjectAdapter({ projectPath: '/workspace/parent', projectName: 'parent' }); + const childProject = createMockProjectAdapter({ + projectPath: '/workspace/parent/child', + projectName: 'child', + }); + + // Run - pass child first, then parent + const result = await findProjectForTestItem(item, [childProject, parentProject]); + + // Assert - order shouldn't affect result + expect(result).to.equal(childProject); + }); + + test('should match item at project root level', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.equal(project); + }); + + test('should use env extension API when available', async () => { + // Enable env extension + useEnvExtensionStub.returns(true); + + // Mock the env extension API + const item = createMockTestItem('test1', '/workspace/proj/tests/test_file.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + const mockEnvApi = { + getPythonProject: sandbox.stub().returns({ uri: project.projectUri }), + }; + sandbox.stub(envExtApi, 'getEnvExtApi').resolves(mockEnvApi as any); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.equal(project); + expect(mockEnvApi.getPythonProject.calledOnceWith(item.uri)).to.be.true; + }); + + test('should fall back to path matching when env extension API is unavailable', async () => { + // Env extension enabled but throws + useEnvExtensionStub.returns(true); + sandbox.stub(envExtApi, 'getEnvExtApi').rejects(new Error('API unavailable')); + + // Mock + const item = createMockTestItem('test1', '/workspace/proj/tests/test_file.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert - should still work via fallback + expect(result).to.equal(project); + }); + }); + + // ===== groupTestItemsByProject Tests ===== + + suite('groupTestItemsByProject', () => { + test('should group single test item to its matching project', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([item], [project]); + + // Assert + expect(result.size).to.equal(1); + const entry = Array.from(result.values())[0]; + expect(entry.project).to.equal(project); + expect(entry.items).to.deep.equal([item]); + }); + + test('should aggregate multiple items belonging to same project', async () => { + // Mock + const item1 = createMockTestItem('test1', '/workspace/proj/tests/test1.py'); + const item2 = createMockTestItem('test2', '/workspace/proj/tests/test2.py'); + const item3 = createMockTestItem('test3', '/workspace/proj/test3.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([item1, item2, item3], [project]); + + // Assert - use Set for order-agnostic comparison + expect(result.size).to.equal(1); + const entry = Array.from(result.values())[0]; + expect(entry.items).to.have.length(3); + expect(new Set(entry.items)).to.deep.equal(new Set([item1, item2, item3])); + }); + + test('should separate items into groups by their owning project', async () => { + // Mock + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const item3 = createMockTestItem('test3', '/workspace/proj1/other_test.py'); + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + + // Run + const result = await groupTestItemsByProject([item1, item2, item3], [proj1, proj2]); + + // Assert - use Set for order-agnostic comparison + expect(result.size).to.equal(2); + const proj1Entry = result.get(proj1.projectUri.toString()); + const proj2Entry = result.get(proj2.projectUri.toString()); + expect(proj1Entry?.items).to.have.length(2); + expect(new Set(proj1Entry?.items)).to.deep.equal(new Set([item1, item3])); + expect(proj2Entry?.items).to.deep.equal([item2]); + }); + + test('should return empty map when no test items provided', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([], [project]); + + // Assert + expect(result.size).to.equal(0); + }); + + test('should exclude items that do not match any project path', async () => { + // Mock + const item = createMockTestItem('test1', '/other/path/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([item], [project]); + + // Assert + expect(result.size).to.equal(0); + }); + + test('should assign item to most specific (deepest) project for nested paths', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/parent/child/test.py'); + const parentProject = createMockProjectAdapter({ projectPath: '/workspace/parent', projectName: 'parent' }); + const childProject = createMockProjectAdapter({ + projectPath: '/workspace/parent/child', + projectName: 'child', + }); + + // Run + const result = await groupTestItemsByProject([item], [parentProject, childProject]); + + // Assert + expect(result.size).to.equal(1); + const entry = result.get(childProject.projectUri.toString()); + expect(entry?.project).to.equal(childProject); + expect(entry?.items).to.deep.equal([item]); + }); + + test('should omit projects that have no matching test items', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj1/test.py'); + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + + // Run + const result = await groupTestItemsByProject([item], [proj1, proj2]); + + // Assert + expect(result.size).to.equal(1); + expect(result.has(proj1.projectUri.toString())).to.be.true; + expect(result.has(proj2.projectUri.toString())).to.be.false; + }); + }); + + // ===== getTestCaseNodesRecursive Tests ===== + + suite('getTestCaseNodesRecursive', () => { + test('should return single item when it is a leaf node with no children', () => { + // Mock + const item = createMockTestItem('test_func', '/test.py'); + + // Run + const result = getTestCaseNodesRecursive(item); + + // Assert + expect(result).to.deep.equal([item]); + }); + + test('should return all leaf nodes from single-level nested structure', () => { + // Mock + const leaf1 = createMockTestItem('test_method1', '/test.py'); + const leaf2 = createMockTestItem('test_method2', '/test.py'); + const classItem = createMockTestItem('TestClass', '/test.py', [leaf1, leaf2]); + + // Run + const result = getTestCaseNodesRecursive(classItem); + + // Assert - use Set for order-agnostic comparison + expect(result).to.have.length(2); + expect(new Set(result)).to.deep.equal(new Set([leaf1, leaf2])); + }); + + test('should traverse deeply nested structure to find all leaf nodes', () => { + // Mock - 3 levels deep: file → class → inner class → test + const leaf1 = createMockTestItem('test1', '/test.py'); + const leaf2 = createMockTestItem('test2', '/test.py'); + const innerClass = createMockTestItem('InnerClass', '/test.py', [leaf2]); + const outerClass = createMockTestItem('OuterClass', '/test.py', [leaf1, innerClass]); + const fileItem = createMockTestItem('test_file.py', '/test.py', [outerClass]); + + // Run + const result = getTestCaseNodesRecursive(fileItem); + + // Assert - use Set for order-agnostic comparison + expect(result).to.have.length(2); + expect(new Set(result)).to.deep.equal(new Set([leaf1, leaf2])); + }); + + test('should collect leaves from multiple sibling branches', () => { + // Mock - multiple test classes at same level + const leaf1 = createMockTestItem('test1', '/test.py'); + const leaf2 = createMockTestItem('test2', '/test.py'); + const leaf3 = createMockTestItem('test3', '/test.py'); + const class1 = createMockTestItem('Class1', '/test.py', [leaf1]); + const class2 = createMockTestItem('Class2', '/test.py', [leaf2, leaf3]); + const fileItem = createMockTestItem('test_file.py', '/test.py', [class1, class2]); + + // Run + const result = getTestCaseNodesRecursive(fileItem); + + // Assert - use Set for order-agnostic comparison + expect(result).to.have.length(3); + expect(new Set(result)).to.deep.equal(new Set([leaf1, leaf2, leaf3])); + }); + }); + + // ===== executeTestsForProject Tests ===== + + suite('executeTestsForProject', () => { + test('should call executionAdapter.runTests with project URI and mapped test IDs', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'test_file.py::test1'); + const testItem = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [testItem], runMock.object, request, deps); + + // Assert + expect(project.executionAdapterStub.calledOnce).to.be.true; + const callArgs = project.executionAdapterStub.firstCall.args; + expect(callArgs[0].fsPath).to.equal(project.projectUri.fsPath); // uri + expect(callArgs[1]).to.deep.equal(['test_file.py::test1']); // testCaseIds + expect(callArgs[7]).to.equal(project); // project + }); + + test('should mark all leaf test items as started in the test run', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + project.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [item1, item2], runMock.object, request, deps); + + // Assert - both items marked as started + runMock.verify((r) => r.started(item1), typemoq.Times.once()); + runMock.verify((r) => r.started(item2), typemoq.Times.once()); + }); + + test('should resolve test IDs via resultResolver.vsIdToRunId mapping', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'path/to/test1'); + project.resultResolver.vsIdToRunId.set('test2', 'path/to/test2'); + const item1 = createMockTestItem('test1', '/workspace/proj/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [item1, item2], runMock.object, request, deps); + + // Assert - use Set for order-agnostic comparison + const passedTestIds = project.executionAdapterStub.firstCall.args[1] as string[]; + expect(new Set(passedTestIds)).to.deep.equal(new Set(['path/to/test1', 'path/to/test2'])); + }); + + test('should skip execution when no items have vsIdToRunId mappings', async () => { + // Mock - no mappings set, so lookups return undefined + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const item = createMockTestItem('unmapped_test', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [item], runMock.object, request, deps); + + // Assert - execution adapter never called + expect(project.executionAdapterStub.called).to.be.false; + }); + + test('should recursively expand nested test items to find leaf nodes', async () => { + // Mock - class containing two test methods + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const leaf1 = createMockTestItem('test1', '/workspace/proj/test.py'); + const leaf2 = createMockTestItem('test2', '/workspace/proj/test.py'); + const classItem = createMockTestItem('TestClass', '/workspace/proj/test.py', [leaf1, leaf2]); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + project.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [classItem], runMock.object, request, deps); + + // Assert - leaf nodes marked as started, not the parent class + runMock.verify((r) => r.started(leaf1), typemoq.Times.once()); + runMock.verify((r) => r.started(leaf2), typemoq.Times.once()); + const passedTestIds = project.executionAdapterStub.firstCall.args[1] as string[]; + expect(passedTestIds).to.have.length(2); + }); + }); + + // ===== executeTestsForProjects Tests ===== + + suite('executeTestsForProjects', () => { + let telemetryStub: sinon.SinonStub; + + setup(() => { + telemetryStub = sandbox.stub(telemetry, 'sendTelemetryEvent'); + }); + + test('should return immediately when empty projects array provided', async () => { + // Mock + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([], [], runMock.object, request, token, deps); + + // Assert - no telemetry sent since no projects executed + expect(telemetryStub.called).to.be.false; + }); + + test('should skip execution when cancellation requested before start', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const tokenSource = new CancellationTokenSource(); + tokenSource.cancel(); // Pre-cancel + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([project], [item], runMock.object, request, tokenSource.token, deps); + + // Assert - execution adapter never called + expect(project.executionAdapterStub.called).to.be.false; + }); + + test('should execute tests for each project when multiple projects provided', async () => { + // Mock + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([proj1, proj2], [item1, item2], runMock.object, request, token, deps); + + // Assert - both projects had their execution adapters called + expect(proj1.executionAdapterStub.calledOnce).to.be.true; + expect(proj2.executionAdapterStub.calledOnce).to.be.true; + }); + + test('should emit telemetry event for each project execution', async () => { + // Mock + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([proj1, proj2], [item1, item2], runMock.object, request, token, deps); + + // Assert - telemetry sent twice (once per project) + expect(telemetryStub.callCount).to.equal(2); + }); + + test('should stop processing remaining projects when cancellation requested mid-execution', async () => { + // Mock + const tokenSource = new CancellationTokenSource(); + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + // First project triggers cancellation during its execution + proj1.executionAdapterStub.callsFake(async () => { + tokenSource.cancel(); + }); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects( + [proj1, proj2], + [item1, item2], + runMock.object, + request, + tokenSource.token, + deps, + ); + + // Assert - first project executed, second may be skipped due to cancellation check + expect(proj1.executionAdapterStub.calledOnce).to.be.true; + }); + + test('should continue executing remaining projects when one project fails', async () => { + // Mock + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + proj1.executionAdapterStub.rejects(new Error('Execution failed')); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run - should not throw + await executeTestsForProjects([proj1, proj2], [item1, item2], runMock.object, request, token, deps); + + // Assert - second project still executed despite first failing + expect(proj2.executionAdapterStub.calledOnce).to.be.true; + }); + + test('should configure loadDetailedCoverage callback when run profile is Coverage', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([project], [item], runMock.object, request, token, deps); + + // Assert - loadDetailedCoverage callback was configured + expect(profileMock.loadDetailedCoverage).to.not.be.undefined; + }); + + test('should include debugging=true in telemetry when run profile is Debug', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Debug } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([project], [item], runMock.object, request, token, deps); + + // Assert - telemetry contains debugging=true + expect(telemetryStub.calledOnce).to.be.true; + const telemetryProps = telemetryStub.firstCall.args[2]; + expect(telemetryProps.debugging).to.be.true; + }); + }); + + // ===== setupCoverageForProjects Tests ===== + + suite('setupCoverageForProjects', () => { + test('should configure loadDetailedCoverage callback when profile kind is Coverage', () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run + setupCoverageForProjects(request, [project]); + + // Assert + expect(profileMock.loadDetailedCoverage).to.be.a('function'); + }); + + test('should leave loadDetailedCoverage undefined when profile kind is Run', () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const profileMock = ({ + kind: TestRunProfileKind.Run, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run + setupCoverageForProjects(request, [project]); + + // Assert + expect(profileMock.loadDetailedCoverage).to.be.undefined; + }); + + test('should return coverage data from detailedCoverageMap when loadDetailedCoverage is called', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const mockCoverageDetails = [{ line: 1, executed: true }]; + // Use Uri.fsPath as the key to match the implementation's lookup + const fileUri = Uri.file('/workspace/proj/file.py'); + project.resultResolver.detailedCoverageMap.set(fileUri.fsPath, mockCoverageDetails as any); + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run - configure coverage + setupCoverageForProjects(request, [project]); + + // Run - call the configured callback + const fileCoverage = { uri: fileUri }; + const result = await profileMock.loadDetailedCoverage!( + {} as TestRun, + fileCoverage as any, + {} as CancellationToken, + ); + + // Assert + expect(result).to.deep.equal(mockCoverageDetails); + }); + + test('should return empty array when file has no coverage data in map', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run - configure coverage + setupCoverageForProjects(request, [project]); + + // Run - call callback for file not in map + const fileCoverage = { uri: Uri.file('/workspace/proj/uncovered_file.py') }; + const result = await profileMock.loadDetailedCoverage!( + {} as TestRun, + fileCoverage as any, + {} as CancellationToken, + ); + + // Assert + expect(result).to.deep.equal([]); + }); + + test('should route to correct project when multiple projects have coverage data', async () => { + // Mock - two projects with different coverage data + const project1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const project2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + const coverage1 = [{ line: 1, executed: true }]; + const coverage2 = [{ line: 2, executed: false }]; + const file1Uri = Uri.file('/workspace/proj1/file1.py'); + const file2Uri = Uri.file('/workspace/proj2/file2.py'); + project1.resultResolver.detailedCoverageMap.set(file1Uri.fsPath, coverage1 as any); + project2.resultResolver.detailedCoverageMap.set(file2Uri.fsPath, coverage2 as any); + + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run - configure coverage with both projects + setupCoverageForProjects(request, [project1, project2]); + + // Assert - can get coverage from both projects through single callback + const result1 = await profileMock.loadDetailedCoverage!( + {} as TestRun, + { uri: file1Uri } as any, + {} as CancellationToken, + ); + const result2 = await profileMock.loadDetailedCoverage!( + {} as TestRun, + { uri: file2Uri } as any, + {} as CancellationToken, + ); + + expect(result1).to.deep.equal(coverage1); + expect(result2).to.deep.equal(coverage2); + }); + }); +}); diff --git a/src/test/testing/testController/common/projectUtils.unit.test.ts b/src/test/testing/testController/common/projectUtils.unit.test.ts new file mode 100644 index 000000000000..75f399e89fc0 --- /dev/null +++ b/src/test/testing/testController/common/projectUtils.unit.test.ts @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { Uri } from 'vscode'; +import { + getProjectId, + createProjectDisplayName, + parseVsId, + PROJECT_ID_SEPARATOR, +} from '../../../../client/testing/testController/common/projectUtils'; + +suite('Project Utils Tests', () => { + suite('getProjectId', () => { + test('should return URI string representation', () => { + const uri = Uri.file('/workspace/project'); + + const id = getProjectId(uri); + + expect(id).to.equal(uri.toString()); + }); + + test('should be consistent for same URI', () => { + const uri = Uri.file('/workspace/project'); + + const id1 = getProjectId(uri); + const id2 = getProjectId(uri); + + expect(id1).to.equal(id2); + }); + + test('should be different for different URIs', () => { + const uri1 = Uri.file('/workspace/project1'); + const uri2 = Uri.file('/workspace/project2'); + + const id1 = getProjectId(uri1); + const id2 = getProjectId(uri2); + + expect(id1).to.not.equal(id2); + }); + + test('should handle Windows paths', () => { + const uri = Uri.file('C:\\workspace\\project'); + + const id = getProjectId(uri); + + expect(id).to.be.a('string'); + expect(id).to.have.length.greaterThan(0); + }); + + test('should handle nested project paths', () => { + const parentUri = Uri.file('/workspace/parent'); + const childUri = Uri.file('/workspace/parent/child'); + + const parentId = getProjectId(parentUri); + const childId = getProjectId(childUri); + + expect(parentId).to.not.equal(childId); + }); + + test('should match Python Environments extension format', () => { + const uri = Uri.file('/workspace/project'); + + const id = getProjectId(uri); + + // Should match how Python Environments extension keys projects + expect(id).to.equal(uri.toString()); + expect(typeof id).to.equal('string'); + }); + }); + + suite('createProjectDisplayName', () => { + test('should format name with major.minor version', () => { + const result = createProjectDisplayName('MyProject', '3.11.2'); + + expect(result).to.equal('MyProject (Python 3.11)'); + }); + + test('should handle version with patch and pre-release', () => { + const result = createProjectDisplayName('MyProject', '3.12.0rc1'); + + expect(result).to.equal('MyProject (Python 3.12)'); + }); + + test('should handle version with only major.minor', () => { + const result = createProjectDisplayName('MyProject', '3.10'); + + expect(result).to.equal('MyProject (Python 3.10)'); + }); + + test('should handle invalid version format gracefully', () => { + const result = createProjectDisplayName('MyProject', 'invalid-version'); + + expect(result).to.equal('MyProject (Python invalid-version)'); + }); + + test('should handle empty version string', () => { + const result = createProjectDisplayName('MyProject', ''); + + expect(result).to.equal('MyProject (Python )'); + }); + + test('should handle version with single digit', () => { + const result = createProjectDisplayName('MyProject', '3'); + + expect(result).to.equal('MyProject (Python 3)'); + }); + + test('should handle project name with special characters', () => { + const result = createProjectDisplayName('My-Project_123', '3.11.5'); + + expect(result).to.equal('My-Project_123 (Python 3.11)'); + }); + + test('should handle empty project name', () => { + const result = createProjectDisplayName('', '3.11.2'); + + expect(result).to.equal(' (Python 3.11)'); + }); + }); + + suite('parseVsId', () => { + test('should parse project-scoped ID correctly', () => { + const projectUri = Uri.file('/workspace/project'); + const projectId = getProjectId(projectUri); + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}test_file.py::test_name`; + + const [parsedProjectId, runId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(runId).to.equal('test_file.py::test_name'); + }); + + test('should handle legacy ID without project scope', () => { + const vsId = 'test_file.py'; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.be.undefined; + expect(runId).to.equal('test_file.py'); + }); + + test('should handle runId containing separator', () => { + const projectUri = Uri.file('/workspace/project'); + const projectId = getProjectId(projectUri); + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}test_file.py::test_class::test_method`; + + const [parsedProjectId, runId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(runId).to.equal('test_file.py::test_class::test_method'); + }); + + test('should handle empty project ID', () => { + const vsId = `${PROJECT_ID_SEPARATOR}test_file.py::test_name`; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.equal(''); + expect(runId).to.equal('test_file.py::test_name'); + }); + + test('should handle empty runId', () => { + const vsId = `project-abc123def456${PROJECT_ID_SEPARATOR}`; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.equal('project-abc123def456'); + expect(runId).to.equal(''); + }); + + test('should handle ID with file path', () => { + const vsId = `project-abc123def456${PROJECT_ID_SEPARATOR}/workspace/tests/test_file.py`; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.equal('project-abc123def456'); + expect(runId).to.equal('/workspace/tests/test_file.py'); + }); + + test('should handle Windows file paths', () => { + const projectUri = Uri.file('/workspace/project'); + const projectId = getProjectId(projectUri); + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}C:\\workspace\\tests\\test_file.py`; + + const [parsedProjectId, runId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(runId).to.equal('C:\\workspace\\tests\\test_file.py'); + }); + }); + + suite('Integration Tests', () => { + test('should generate unique IDs for different URIs', () => { + const uris = [ + Uri.file('/workspace/a'), + Uri.file('/workspace/b'), + Uri.file('/workspace/c'), + Uri.file('/workspace/d'), + Uri.file('/workspace/e'), + ]; + + const ids = uris.map((uri) => getProjectId(uri)); + const uniqueIds = new Set(ids); + + expect(uniqueIds.size).to.equal(uris.length, 'All IDs should be unique'); + }); + + test('should handle nested project paths', () => { + const parentUri = Uri.file('/workspace/parent'); + const childUri = Uri.file('/workspace/parent/child'); + + const parentId = getProjectId(parentUri); + const childId = getProjectId(childUri); + + expect(parentId).to.not.equal(childId); + }); + + test('should create complete vsId and parse it back', () => { + const projectUri = Uri.file('/workspace/myproject'); + const projectId = getProjectId(projectUri); + const runId = 'tests/test_module.py::TestClass::test_method'; + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}${runId}`; + + const [parsedProjectId, parsedRunId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(parsedRunId).to.equal(runId); + }); + + test('should match Python Environments extension URI format', () => { + const uri = Uri.file('/workspace/project'); + + const projectId = getProjectId(uri); + + // Should be string representation of URI + expect(projectId).to.equal(uri.toString()); + expect(typeof projectId).to.equal('string'); + }); + }); +}); diff --git a/src/test/testing/testController/common/testProjectRegistry.unit.test.ts b/src/test/testing/testController/common/testProjectRegistry.unit.test.ts new file mode 100644 index 000000000000..5d04930d0e88 --- /dev/null +++ b/src/test/testing/testController/common/testProjectRegistry.unit.test.ts @@ -0,0 +1,440 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { TestController, Uri } from 'vscode'; +import { IConfigurationService } from '../../../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../../../client/common/variables/types'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { TestProjectRegistry } from '../../../../client/testing/testController/common/testProjectRegistry'; +import * as envExtApiInternal from '../../../../client/envExt/api.internal'; +import { PythonProject, PythonEnvironment } from '../../../../client/envExt/types'; + +suite('TestProjectRegistry', () => { + let sandbox: sinon.SinonSandbox; + let testController: TestController; + let configSettings: IConfigurationService; + let interpreterService: IInterpreterService; + let envVarsService: IEnvironmentVariablesProvider; + let registry: TestProjectRegistry; + + setup(() => { + sandbox = sinon.createSandbox(); + + // Create mock test controller + testController = ({ + items: { + get: sandbox.stub(), + add: sandbox.stub(), + delete: sandbox.stub(), + forEach: sandbox.stub(), + }, + createTestItem: sandbox.stub(), + dispose: sandbox.stub(), + } as unknown) as TestController; + + // Create mock config settings + configSettings = ({ + getSettings: sandbox.stub().returns({ + testing: { + pytestEnabled: true, + unittestEnabled: false, + }, + }), + } as unknown) as IConfigurationService; + + // Create mock interpreter service + interpreterService = ({ + getActiveInterpreter: sandbox.stub().resolves({ + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + }), + } as unknown) as IInterpreterService; + + // Create mock env vars service + envVarsService = ({ + getEnvironmentVariables: sandbox.stub().resolves({}), + } as unknown) as IEnvironmentVariablesProvider; + + registry = new TestProjectRegistry(testController, configSettings, interpreterService, envVarsService); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('hasProjects', () => { + test('should return false for uninitialized workspace', () => { + const workspaceUri = Uri.file('/workspace'); + + const result = registry.hasProjects(workspaceUri); + + expect(result).to.be.false; + }); + + test('should return true after projects are registered', async () => { + const workspaceUri = Uri.file('/workspace'); + + // Mock useEnvExtension to return false to use default project path + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + + const result = registry.hasProjects(workspaceUri); + + expect(result).to.be.true; + }); + }); + + suite('getProjectsArray', () => { + test('should return empty array for uninitialized workspace', () => { + const workspaceUri = Uri.file('/workspace'); + + const result = registry.getProjectsArray(workspaceUri); + + expect(result).to.be.an('array').that.is.empty; + }); + + test('should return projects after registration', async () => { + const workspaceUri = Uri.file('/workspace'); + + // Mock useEnvExtension to return false to use default project path + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + + const result = registry.getProjectsArray(workspaceUri); + + expect(result).to.be.an('array').with.length(1); + expect(result[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + }); + }); + + suite('discoverAndRegisterProjects', () => { + test('should create default project when env extension not available', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + expect(projects[0].testProvider).to.equal('pytest'); + }); + + test('should use unittest when configured', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + (configSettings.getSettings as sinon.SinonStub).returns({ + testing: { + pytestEnabled: false, + unittestEnabled: true, + }, + }); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].testProvider).to.equal('unittest'); + }); + + test('should discover projects from Python Environments API', async () => { + const workspaceUri = Uri.file('/workspace'); + const projectUri = Uri.file('/workspace/project1'); + + const mockPythonProject: PythonProject = { + name: 'project1', + uri: projectUri, + }; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [mockPythonProject], + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectName).to.include('project1'); + expect(projects[0].pythonEnvironment).to.deep.equal(mockPythonEnv); + }); + + test('should filter projects to current workspace', async () => { + const workspaceUri = Uri.file('/workspace1'); + const projectInWorkspace = Uri.file('/workspace1/project1'); + const projectOutsideWorkspace = Uri.file('/workspace2/project2'); + + const mockProjects: PythonProject[] = [ + { name: 'project1', uri: projectInWorkspace }, + { name: 'project2', uri: projectOutsideWorkspace }, + ]; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => mockProjects, + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(projectInWorkspace.fsPath); + }); + + test('should fallback to default project when no projects found', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [], + } as any); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + }); + + test('should fallback to default project on API error', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').rejects(new Error('API error')); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + }); + }); + + suite('configureNestedProjectIgnores', () => { + test('should not set ignores when no nested projects', async () => { + const workspaceUri = Uri.file('/workspace'); + const projectUri = Uri.file('/workspace/project1'); + + const mockPythonProject: PythonProject = { + name: 'project1', + uri: projectUri, + }; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [mockPythonProject], + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + await registry.discoverAndRegisterProjects(workspaceUri); + registry.configureNestedProjectIgnores(workspaceUri); + + const projects = registry.getProjectsArray(workspaceUri); + expect(projects[0].nestedProjectPathsToIgnore).to.be.undefined; + }); + + test('should configure ignore paths for nested projects', async () => { + const workspaceUri = Uri.file('/workspace'); + const parentProjectUri = Uri.file('/workspace/parent'); + const childProjectUri = Uri.file(path.join('/workspace/parent', 'child')); + + const mockProjects: PythonProject[] = [ + { name: 'parent', uri: parentProjectUri }, + { name: 'child', uri: childProjectUri }, + ]; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => mockProjects, + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + await registry.discoverAndRegisterProjects(workspaceUri); + registry.configureNestedProjectIgnores(workspaceUri); + + const projects = registry.getProjectsArray(workspaceUri); + const parentProject = projects.find((p) => p.projectUri.fsPath === parentProjectUri.fsPath); + + expect(parentProject?.nestedProjectPathsToIgnore).to.include(childProjectUri.fsPath); + }); + + test('should not set child project as ignored for sibling projects', async () => { + const workspaceUri = Uri.file('/workspace'); + const project1Uri = Uri.file('/workspace/project1'); + const project2Uri = Uri.file('/workspace/project2'); + + const mockProjects: PythonProject[] = [ + { name: 'project1', uri: project1Uri }, + { name: 'project2', uri: project2Uri }, + ]; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => mockProjects, + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + await registry.discoverAndRegisterProjects(workspaceUri); + registry.configureNestedProjectIgnores(workspaceUri); + + const projects = registry.getProjectsArray(workspaceUri); + projects.forEach((project) => { + expect(project.nestedProjectPathsToIgnore).to.be.undefined; + }); + }); + }); + + suite('clearWorkspace', () => { + test('should remove all projects for a workspace', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + expect(registry.hasProjects(workspaceUri)).to.be.true; + + registry.clearWorkspace(workspaceUri); + + expect(registry.hasProjects(workspaceUri)).to.be.false; + expect(registry.getProjectsArray(workspaceUri)).to.be.empty; + }); + + test('should not affect other workspaces', async () => { + const workspace1Uri = Uri.file('/workspace1'); + const workspace2Uri = Uri.file('/workspace2'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspace1Uri); + await registry.discoverAndRegisterProjects(workspace2Uri); + + registry.clearWorkspace(workspace1Uri); + + expect(registry.hasProjects(workspace1Uri)).to.be.false; + expect(registry.hasProjects(workspace2Uri)).to.be.true; + }); + }); + + suite('getWorkspaceProjects', () => { + test('should return undefined for uninitialized workspace', () => { + const workspaceUri = Uri.file('/workspace'); + + const result = registry.getWorkspaceProjects(workspaceUri); + + expect(result).to.be.undefined; + }); + + test('should return map after registration', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + + const result = registry.getWorkspaceProjects(workspaceUri); + + expect(result).to.be.instanceOf(Map); + expect(result?.size).to.equal(1); + }); + }); + + suite('ProjectAdapter properties', () => { + test('should create adapter with correct test infrastructure', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + const project = projects[0]; + + expect(project.projectName).to.be.a('string'); + expect(project.projectUri.fsPath).to.equal(workspaceUri.fsPath); + expect(project.workspaceUri.fsPath).to.equal(workspaceUri.fsPath); + expect(project.testProvider).to.equal('pytest'); + expect(project.discoveryAdapter).to.exist; + expect(project.executionAdapter).to.exist; + expect(project.resultResolver).to.exist; + expect(project.isDiscovering).to.be.false; + expect(project.isExecuting).to.be.false; + }); + + test('should include python environment details', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + const project = projects[0]; + + expect(project.pythonEnvironment).to.exist; + expect(project.pythonProject).to.exist; + expect(project.pythonProject.name).to.equal('myproject'); + }); + }); +}); diff --git a/src/test/testing/testController/controller.unit.test.ts b/src/test/testing/testController/controller.unit.test.ts new file mode 100644 index 000000000000..feb5f36fc797 --- /dev/null +++ b/src/test/testing/testController/controller.unit.test.ts @@ -0,0 +1,344 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import { TestController, Uri } from 'vscode'; + +import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; +import * as envExtApiInternal from '../../../client/envExt/api.internal'; +import * as projectUtils from '../../../client/testing/testController/common/projectUtils'; +import { PythonTestController } from '../../../client/testing/testController/controller'; +import { TestProjectRegistry } from '../../../client/testing/testController/common/testProjectRegistry'; + +function createStubTestController(): TestController { + const disposable = { dispose: () => undefined }; + + const controller = ({ + items: { + forEach: sinon.stub(), + get: sinon.stub(), + add: sinon.stub(), + replace: sinon.stub(), + delete: sinon.stub(), + size: 0, + [Symbol.iterator]: sinon.stub(), + }, + createRunProfile: sinon.stub().returns(disposable), + createTestItem: sinon.stub(), + dispose: sinon.stub(), + resolveHandler: undefined, + refreshHandler: undefined, + } as unknown) as TestController; + + return controller; +} + +suite('PythonTestController', () => { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + function createController(options?: { unittestEnabled?: boolean; interpreter?: any }): any { + const unittestEnabled = options?.unittestEnabled ?? false; + const interpreter = + options?.interpreter ?? + ({ + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + } as any); + + const workspaceService = ({ workspaceFolders: [] } as unknown) as any; + const configSettings = ({ + getSettings: sandbox.stub().returns({ + testing: { + unittestEnabled, + autoTestDiscoverOnSaveEnabled: false, + }, + }), + } as unknown) as any; + + const pytest = ({} as unknown) as any; + const unittest = ({} as unknown) as any; + const disposables: any[] = []; + const interpreterService = ({ + getActiveInterpreter: sandbox.stub().resolves(interpreter), + } as unknown) as any; + + const commandManager = ({ + registerCommand: sandbox.stub().returns({ dispose: () => undefined }), + } as unknown) as any; + const pythonExecFactory = ({} as unknown) as any; + const debugLauncher = ({} as unknown) as any; + const envVarsService = ({} as unknown) as any; + + return new PythonTestController( + workspaceService, + configSettings, + pytest, + unittest, + disposables, + interpreterService, + commandManager, + pythonExecFactory, + debugLauncher, + envVarsService, + ); + } + + suite('getTestProvider', () => { + test('returns unittest when enabled', () => { + const controller = createController({ unittestEnabled: true }); + const workspaceUri: Uri = vscode.Uri.file('/workspace'); + + const provider = (controller as any).getTestProvider(workspaceUri); + + assert.strictEqual(provider, UNITTEST_PROVIDER); + }); + + test('returns pytest when unittest not enabled', () => { + const controller = createController({ unittestEnabled: false }); + const workspaceUri: Uri = vscode.Uri.file('/workspace'); + + const provider = (controller as any).getTestProvider(workspaceUri); + + assert.strictEqual(provider, PYTEST_PROVIDER); + }); + }); + + suite('createDefaultProject (via TestProjectRegistry)', () => { + test('creates a single default project using active interpreter', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/myws'); + const interpreter = { + displayName: 'My Python', + path: '/opt/py/bin/python', + version: { raw: '3.12.1' }, + sysPrefix: '/opt/py', + }; + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + // Stub useEnvExtension to return false so createDefaultProject is called + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves(interpreter), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + const project = projects[0]; + + assert.strictEqual(projects.length, 1); + assert.strictEqual(project.workspaceUri.toString(), workspaceUri.toString()); + assert.strictEqual(project.projectUri.toString(), workspaceUri.toString()); + assert.strictEqual(project.projectName, 'myws'); + + assert.strictEqual(project.testProvider, PYTEST_PROVIDER); + assert.strictEqual(project.discoveryAdapter, fakeDiscoveryAdapter); + assert.strictEqual(project.executionAdapter, fakeExecutionAdapter); + + assert.strictEqual(project.pythonProject.uri.toString(), workspaceUri.toString()); + assert.strictEqual(project.pythonProject.name, 'myws'); + + assert.strictEqual(project.pythonEnvironment.displayName, 'My Python'); + assert.strictEqual(project.pythonEnvironment.version, '3.12.1'); + assert.strictEqual(project.pythonEnvironment.execInfo.run.executable, '/opt/py/bin/python'); + }); + }); + + suite('discoverWorkspaceProjects (via TestProjectRegistry)', () => { + test('respects useEnvExtension() == false and falls back to single default project', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/a'); + + const useEnvExtensionStub = sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + const getEnvExtApiStub = sandbox.stub(envExtApiInternal, 'getEnvExtApi'); + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves({ + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + }), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + assert.strictEqual(useEnvExtensionStub.called, true); + assert.strictEqual(getEnvExtApiStub.notCalled, true); + assert.strictEqual(projects.length, 1); + assert.strictEqual(projects[0].projectUri.toString(), workspaceUri.toString()); + }); + + test('filters Python projects to workspace and creates adapters for each', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/root'); + + const pythonProjects = [ + { name: 'p1', uri: vscode.Uri.file('/workspace/root/p1') }, + { name: 'p2', uri: vscode.Uri.file('/workspace/root/nested/p2') }, + { name: 'other', uri: vscode.Uri.file('/other/root/p3') }, + ]; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => pythonProjects, + getEnvironment: sandbox.stub().resolves({ + name: 'env', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: vscode.Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'test', managerId: 'test' }, + }), + } as any); + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves(null), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + // Should only create adapters for the 2 projects in the workspace (not 'other') + assert.strictEqual(projects.length, 2); + const projectUris = projects.map((p: { projectUri: { fsPath: string } }) => p.projectUri.fsPath); + const expectedInWorkspace = [ + vscode.Uri.file('/workspace/root/p1').fsPath, + vscode.Uri.file('/workspace/root/nested/p2').fsPath, + ]; + const expectedOutOfWorkspace = vscode.Uri.file('/other/root/p3').fsPath; + + expectedInWorkspace.forEach((expectedPath) => { + assert.ok(projectUris.includes(expectedPath)); + }); + assert.ok(!projectUris.includes(expectedOutOfWorkspace)); + }); + + test('falls back to default project when no projects are in the workspace', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/root'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [{ name: 'other', uri: vscode.Uri.file('/other/root/p3') }], + } as any); + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + const interpreter = { + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + }; + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves(interpreter), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + // Should fall back to default project since no projects are in the workspace + assert.strictEqual(projects.length, 1); + assert.strictEqual(projects[0].projectUri.toString(), workspaceUri.toString()); + }); + }); +}); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts index e0401edc7b41..40c701b22641 100644 --- a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -22,6 +22,7 @@ import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; import { MockChildProcess } from '../../../mocks/mockChildProcess'; import { traceInfo } from '../../../../client/logging'; import * as extapi from '../../../../client/envExt/api.internal'; +import { createMockProjectAdapter } from '../testMocks'; suite('pytest test execution adapter', () => { let useEnvExtensionStub: sinon.SinonStub; @@ -325,4 +326,210 @@ suite('pytest test execution adapter', () => { typeMoq.Times.once(), ); }); + + // ===== PROJECT-BASED EXECUTION TESTS ===== + + suite('project-based execution', () => { + test('should set PROJECT_ROOT_PATH env var when project provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject', + pythonPath: '/custom/python/path', + }); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Run, + testRun.object, + execFactory.object, + undefined, + undefined, + mockProject, + ); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.isAny(), + typeMoq.It.is((options) => { + assert.equal(options.env?.PROJECT_ROOT_PATH, projectPath); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('should pass debugSessionName in LaunchOptions for debug mode with project', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + traceInfo('stubs launch debugger'); + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject (Python 3.11)', + pythonPath: '/custom/python/path', + }); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Debug, + testRun.object, + execFactory.object, + debugLauncher.object, + undefined, + mockProject, + ); + + await deferred3.promise; + + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is((launchOptions) => { + // Project should be passed for project-based debugging + assert.ok(launchOptions.project, 'project should be defined'); + assert.equal(launchOptions.project?.name, 'myproject (Python 3.11)'); + assert.equal(launchOptions.project?.uri.fsPath, projectPath); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + typeMoq.Times.once(), + ); + }); + + test('should not set PROJECT_ROOT_PATH when no project provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + // Call without project parameter + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.isAny(), + typeMoq.It.is((options) => { + assert.equal(options.env?.PROJECT_ROOT_PATH, undefined); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('should not set project in LaunchOptions when no project provided', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + // Call without project parameter + adapter.runTests( + uri, + [], + TestRunProfileKind.Debug, + testRun.object, + execFactory.object, + debugLauncher.object, + ); + + await deferred3.promise; + + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is((launchOptions) => { + assert.equal(launchOptions.project, undefined); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + typeMoq.Times.once(), + ); + }); + }); }); diff --git a/src/test/testing/testController/testMocks.ts b/src/test/testing/testController/testMocks.ts new file mode 100644 index 000000000000..eb37d492f1d9 --- /dev/null +++ b/src/test/testing/testController/testMocks.ts @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Centralized mock utilities for testing testController components. + * Re-use these helpers across multiple test files for consistency. + */ + +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { TestItem, TestItemCollection, TestRun, Uri } from 'vscode'; +import { IPythonExecutionFactory } from '../../../client/common/process/types'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { ProjectAdapter } from '../../../client/testing/testController/common/projectAdapter'; +import { ProjectExecutionDependencies } from '../../../client/testing/testController/common/projectTestExecution'; +import { TestProjectRegistry } from '../../../client/testing/testController/common/testProjectRegistry'; +import { ITestExecutionAdapter, ITestResultResolver } from '../../../client/testing/testController/common/types'; + +/** + * Creates a mock TestItem with configurable properties. + * @param id - The unique ID of the test item + * @param uriPath - The file path for the test item's URI + * @param children - Optional array of child test items + */ +export function createMockTestItem(id: string, uriPath: string, children?: TestItem[]): TestItem { + const childMap = new Map(); + children?.forEach((c) => childMap.set(c.id, c)); + + const mockChildren: TestItemCollection = { + size: childMap.size, + forEach: (callback: (item: TestItem, collection: TestItemCollection) => void) => { + childMap.forEach((item) => callback(item, mockChildren)); + }, + get: (itemId: string) => childMap.get(itemId), + add: () => {}, + delete: () => {}, + replace: () => {}, + [Symbol.iterator]: function* () { + for (const [key, value] of childMap) { + yield [key, value] as [string, TestItem]; + } + }, + } as TestItemCollection; + + return ({ + id, + uri: Uri.file(uriPath), + children: mockChildren, + label: id, + canResolveChildren: false, + busy: false, + tags: [], + range: undefined, + error: undefined, + parent: undefined, + } as unknown) as TestItem; +} + +/** + * Creates a mock TestItem without a URI. + * Useful for testing edge cases where test items have no associated file. + * @param id - The unique ID of the test item + */ +export function createMockTestItemWithoutUri(id: string): TestItem { + return ({ + id, + uri: undefined, + children: ({ size: 0, forEach: () => {} } as unknown) as TestItemCollection, + label: id, + } as unknown) as TestItem; +} + +export interface MockProjectAdapterConfig { + projectPath: string; + projectName: string; + pythonPath?: string; + testProvider?: 'pytest' | 'unittest'; +} + +export type MockProjectAdapter = ProjectAdapter & { executionAdapterStub: sinon.SinonStub }; + +/** + * Creates a mock ProjectAdapter for testing project-based test execution. + * @param config - Configuration object with project details + * @returns A mock ProjectAdapter with an exposed executionAdapterStub for verification + */ +export function createMockProjectAdapter(config: MockProjectAdapterConfig): MockProjectAdapter { + const runTestsStub = sinon.stub().resolves(); + const executionAdapter: ITestExecutionAdapter = ({ + runTests: runTestsStub, + } as unknown) as ITestExecutionAdapter; + + const resultResolverMock: ITestResultResolver = ({ + vsIdToRunId: new Map(), + runIdToVSid: new Map(), + runIdToTestItem: new Map(), + detailedCoverageMap: new Map(), + resolveDiscovery: () => Promise.resolve(), + resolveExecution: () => {}, + } as unknown) as ITestResultResolver; + + const adapter = ({ + projectUri: Uri.file(config.projectPath), + projectName: config.projectName, + workspaceUri: Uri.file(config.projectPath), + testProvider: config.testProvider ?? 'pytest', + pythonEnvironment: config.pythonPath + ? { + execInfo: { run: { executable: config.pythonPath } }, + } + : undefined, + pythonProject: { + name: config.projectName, + uri: Uri.file(config.projectPath), + }, + executionAdapter, + discoveryAdapter: {} as any, + resultResolver: resultResolverMock, + isDiscovering: false, + isExecuting: false, + // Expose the stub for testing + executionAdapterStub: runTestsStub, + } as unknown) as MockProjectAdapter; + + return adapter; +} + +/** + * Creates mock dependencies for project test execution. + * @returns An object containing mocked ProjectExecutionDependencies + */ +export function createMockDependencies(): ProjectExecutionDependencies { + return { + projectRegistry: typemoq.Mock.ofType().object, + pythonExecFactory: typemoq.Mock.ofType().object, + debugLauncher: typemoq.Mock.ofType().object, + }; +} + +/** + * Creates a mock TestRun with common setup methods. + * @returns A TypeMoq mock of TestRun + */ +export function createMockTestRun(): typemoq.IMock { + const runMock = typemoq.Mock.ofType(); + runMock.setup((r) => r.started(typemoq.It.isAny())); + runMock.setup((r) => r.passed(typemoq.It.isAny(), typemoq.It.isAny())); + runMock.setup((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())); + runMock.setup((r) => r.skipped(typemoq.It.isAny())); + runMock.setup((r) => r.end()); + return runMock; +} diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index 4dae070bccbe..031f30afba8a 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -22,6 +22,7 @@ import { SpawnOptions, } from '../../../../client/common/process/types'; import * as extapi from '../../../../client/envExt/api.internal'; +import { ProjectAdapter } from '../../../../client/testing/testController/common/projectAdapter'; suite('Unittest test discovery adapter', () => { let configService: IConfigurationService; @@ -244,4 +245,98 @@ suite('Unittest test discovery adapter', () => { await discoveryPromise; assert.ok(true, 'Test resolves correctly when triggering a cancellation token in exec observable.'); }); + + test('DiscoverTests should set PROJECT_ROOT_PATH when project is provided', async () => { + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = ({ + projectId: 'file:///workspace/myproject', + projectUri: Uri.file(projectPath), + projectName: 'myproject', + workspaceUri: Uri.file('/workspace'), + } as unknown) as ProjectAdapter; + + const adapter = new UnittestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object, undefined, undefined, mockProject); + const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); + const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; + + // must await until the execObservable is called in order to verify it + await deferred.promise; + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.is>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is((options) => { + try { + // Verify PROJECT_ROOT_PATH is set when project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + projectPath, + 'PROJECT_ROOT_PATH should be set to project URI path', + ); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); + + test('DiscoverTests should NOT set PROJECT_ROOT_PATH when no project is provided', async () => { + const adapter = new UnittestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object); + const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); + const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; + + // must await until the execObservable is called in order to verify it + await deferred.promise; + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.is>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is((options) => { + try { + // Verify PROJECT_ROOT_PATH is NOT set when no project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + undefined, + 'PROJECT_ROOT_PATH should NOT be set when no project is provided', + ); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); }); diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index ab492736f0ad..8a86e9228567 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -22,6 +22,8 @@ import { MockChildProcess } from '../../../mocks/mockChildProcess'; import { traceInfo } from '../../../../client/logging'; import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; import * as extapi from '../../../../client/envExt/api.internal'; +import { ProjectAdapter } from '../../../../client/testing/testController/common/projectAdapter'; +import { createMockProjectAdapter } from '../testMocks'; suite('Unittest test execution adapter', () => { let configService: IConfigurationService; @@ -321,4 +323,259 @@ suite('Unittest test execution adapter', () => { typeMoq.Times.once(), ); }); + + test('RunTests should set PROJECT_ROOT_PATH when project is provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = ({ + projectId: 'file:///workspace/myproject', + projectUri: Uri.file(projectPath), + projectName: 'myproject', + workspaceUri: Uri.file('/workspace'), + } as unknown) as ProjectAdapter; + + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Run, + testRun.object, + execFactory.object, + undefined, // debugLauncher + undefined, // interpreter + mockProject, + ); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is((options) => { + // Verify PROJECT_ROOT_PATH is set when project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + projectPath, + 'PROJECT_ROOT_PATH should be set to project URI path', + ); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('RunTests should NOT set PROJECT_ROOT_PATH when no project is provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is((options) => { + // Verify PROJECT_ROOT_PATH is NOT set when no project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + undefined, + 'PROJECT_ROOT_PATH should NOT be set when no project is provided', + ); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('Debug mode with project should pass project.pythonProject to debug launcher', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + traceInfo('stubs launch debugger'); + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject (Python 3.11)', + pythonPath: '/custom/python/path', + testProvider: 'unittest', + }); + + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Debug, + testRun.object, + execFactory.object, + debugLauncher.object, + undefined, + mockProject, + ); + + await deferred3.promise; + + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is((launchOptions) => { + // Project should be passed for project-based debugging + assert.ok(launchOptions.project, 'project should be defined'); + assert.equal(launchOptions.project?.name, 'myproject (Python 3.11)'); + assert.equal(launchOptions.project?.uri.fsPath, projectPath); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + typeMoq.Times.once(), + ); + }); + + test('useEnvExtension mode with project should use project pythonEnvironment', async () => { + // Enable the useEnvExtension path + useEnvExtensionStub.returns(true); + + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + // Store the deferredTillServerClose so we can resolve it + let serverCloseDeferred: Deferred | undefined; + utilsStartRunResultNamedPipeStub.callsFake((_callback: unknown, deferred: Deferred, _token: unknown) => { + serverCloseDeferred = deferred; + return Promise.resolve('runResultPipe-mockName'); + }); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject (Python 3.11)', + pythonPath: '/custom/python/path', + testProvider: 'unittest', + }); + + // Stub runInBackground to capture which environment was used + const runInBackgroundStub = sinon.stub(extapi, 'runInBackground'); + const exitCallbacks: ((code: number, signal: string | null) => void)[] = []; + // Promise that resolves when the production code registers its onExit handler + const onExitRegistered = createDeferred(); + const mockProc2 = { + stdout: { on: sinon.stub() }, + stderr: { on: sinon.stub() }, + onExit: (cb: (code: number, signal: string | null) => void) => { + exitCallbacks.push(cb); + onExitRegistered.resolve(); + }, + kill: sinon.stub(), + }; + runInBackgroundStub.callsFake(() => Promise.resolve(mockProc2 as any)); + + const testRun = typeMoq.Mock.ofType(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + const runPromise = adapter.runTests( + uri, + [], + TestRunProfileKind.Run, + testRun.object, + execFactory.object, + debugLauncher.object, + undefined, + mockProject, + ); + + // Wait for production code to register its onExit handler + await onExitRegistered.promise; + + // Simulate process exit to complete the test + exitCallbacks.forEach((cb) => cb(0, null)); + + // Resolve the server close deferred to allow the runTests to complete + serverCloseDeferred?.resolve(); + + await runPromise; + + // Verify runInBackground was called with the project's Python environment + sinon.assert.calledOnce(runInBackgroundStub); + const envArg = runInBackgroundStub.firstCall.args[0]; + // The environment should be the project's pythonEnvironment + assert.ok(envArg, 'runInBackground should be called with an environment'); + assert.equal(envArg.execInfo?.run?.executable, '/custom/python/path'); + }); }); diff --git a/src/test/testing/testController/utils.unit.test.ts b/src/test/testing/testController/utils.unit.test.ts index c6d9a70831a9..3cba6fb697a5 100644 --- a/src/test/testing/testController/utils.unit.test.ts +++ b/src/test/testing/testController/utils.unit.test.ts @@ -393,16 +393,18 @@ suite('populateTestTree tests', () => { }; const rootChildrenAddStub = sandbox.stub(); + const rootChildrenGetStub = sandbox.stub().returns(undefined); const mockRootItem: TestItem = { - children: { add: rootChildrenAddStub }, + children: { add: rootChildrenAddStub, get: rootChildrenGetStub }, } as any; const nestedChildrenAddStub = sandbox.stub(); + const nestedChildrenGetStub = sandbox.stub().returns(undefined); const mockNestedNode: TestItem = { id: 'nested-id', canResolveChildren: true, tags: [], - children: { add: nestedChildrenAddStub }, + children: { add: nestedChildrenAddStub, get: nestedChildrenGetStub }, } as any; const mockNestedTestItem: TestItem = { @@ -460,14 +462,15 @@ suite('populateTestTree tests', () => { }; const rootChildrenAddStub = sandbox.stub(); - const mockRootItem: TestItem = { - children: { add: rootChildrenAddStub }, - } as any; - const existingChildrenAddStub = sandbox.stub(); + const existingChildrenGetStub = sandbox.stub().returns(undefined); const existingNode: TestItem = { id: 'existing-id', - children: { add: existingChildrenAddStub }, + children: { add: existingChildrenAddStub, get: existingChildrenGetStub }, + } as any; + const rootChildrenGetStub = sandbox.stub().withArgs('existing-id').returns(existingNode); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub, get: rootChildrenGetStub }, } as any; const mockTestItem: TestItem = { @@ -597,14 +600,14 @@ suite('populateTestTree tests', () => { id: 'root-id', tags: [], canResolveChildren: true, - children: { add: sandbox.stub() }, + children: { add: sandbox.stub(), get: sandbox.stub().returns(undefined) }, } as any; const mockNestedNode: TestItem = { id: 'nested-id', tags: [], canResolveChildren: true, - children: { add: sandbox.stub() }, + children: { add: sandbox.stub(), get: sandbox.stub().returns(undefined) }, } as any; const mockTestItem: TestItem = { diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index 3e2816afbbde..b7ea2bc549a0 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import * as vscodeMocks from './mocks/vsc'; import { vscMockTelemetryReporter } from './mocks/vsc/telemetryReporter'; import { anything, instance, mock, when } from 'ts-mockito'; +import { TestItem } from 'vscode'; const Module = require('module'); type VSCode = typeof vscode; @@ -148,3 +149,29 @@ mockedVSCode.TestRunProfileKind = vscodeMocks.TestRunProfileKind; (mockedVSCode as any).StatementCoverage = class StatementCoverage { constructor(public executed: number | boolean, public location: any, public branches?: any) {} }; + +// Mock TestController for vscode.tests namespace +function createMockTestController(): vscode.TestController { + const disposable = { dispose: () => undefined }; + return ({ + items: { + forEach: () => undefined, + get: () => undefined, + add: () => undefined, + replace: () => undefined, + delete: () => undefined, + size: 0, + [Symbol.iterator]: function* () {}, + }, + createRunProfile: () => disposable, + createTestItem: () => ({} as TestItem), + dispose: () => undefined, + resolveHandler: undefined, + refreshHandler: undefined, + } as unknown) as vscode.TestController; +} + +// Add tests namespace with createTestController +(mockedVSCode as any).tests = { + createTestController: (_id: string, _label: string) => createMockTestController(), +}; From 3c0301d5f8af9826b710f989f2ed6e569f6e8a45 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:13:01 -0800 Subject: [PATCH 1102/1136] Bump version to 2026.2.0 in package.json (#25801) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 863c2a720678..96e5097bb9c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2026.1.0-dev", + "version": "2026.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2026.1.0-dev", + "version": "2026.2.0", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/package.json b/package.json index bc9131276c59..d2d28bee1c4c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2026.1.0-dev", + "version": "2026.2.0", "featureFlags": { "usingNewInterpreterStorage": true }, From 85898d5624adb7ab3940bff9fec37ae4ecce5b1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:47:09 -0800 Subject: [PATCH 1103/1136] Bump actions/upload-artifact from 6 to 7 (#25817) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
Release notes

Sourced from actions/upload-artifact's releases.

v7.0.0

v7 What's new

Direct Uploads

Adds support for uploading single files directly (unzipped). Callers can set the new archive parameter to false to skip zipping the file during upload. Right now, we only support single files. The action will fail if the glob passed resolves to multiple files. The name parameter is also ignored with this setting. Instead, the name of the artifact will be the name of the uploaded file.

ESM

To support new versions of the @actions/* packages, we've upgraded the package to ESM.

What's Changed

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v6...v7.0.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=6&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 95024788d915..82b42c841fce 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -682,7 +682,7 @@ jobs: run: npm run test:cover:report - name: Upload HTML report - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ${{ runner.os }}-coverage-report-html path: ./coverage From a56c733786cab910783a28ab11acd0add24982a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:47:12 -0800 Subject: [PATCH 1104/1136] Bump actions/upload-artifact from 6 to 7 in /.github/actions/build-vsix (#25818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
Release notes

Sourced from actions/upload-artifact's releases.

v7.0.0

v7 What's new

Direct Uploads

Adds support for uploading single files directly (unzipped). Callers can set the new archive parameter to false to skip zipping the file during upload. Right now, we only support single files. The action will fail if the glob passed resolves to multiple files. The name parameter is also ignored with this setting. Instead, the name of the artifact will be the name of the uploaded file.

ESM

To support new versions of the @actions/* packages, we've upgraded the package to ESM.

What's Changed

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v6...v7.0.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=6&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-vsix/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index 95fec979b08e..40d8d73cb4c6 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -93,7 +93,7 @@ runs: VSIX_NAME: ${{ inputs.vsix_name }} - name: Upload VSIX - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ${{ inputs.artifact_name }} path: ${{ inputs.vsix_name }} From 8d92b829e5ae76053e4803e9b213163c6e63ca29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:47:41 -0800 Subject: [PATCH 1105/1136] Bump jakebailey/pyright-action from 2.3.3 to 3.0.2 (#25799) Bumps [jakebailey/pyright-action](https://github.com/jakebailey/pyright-action) from 2.3.3 to 3.0.2.
Release notes

Sourced from jakebailey/pyright-action's releases.

v3.0.2

  • Update mentioned checkout in readme (03fd2c0)
  • Update mentioned setup-python in readme (9fb0169)
  • Switch to tiny-jsonc (19c6c23)
  • fix lint (1296485)
  • output metafile in build (20b106d)

v3.0.1

  • Make docs reference v3 (c781035)

v3.0.0

  • Update github actions (#208) (5ceb87e)
  • Update actions/cache action to v5 (#210) (49e6fb4)
  • Disable type lint of build script (d991920)
  • Fix tests (1edc551)
  • Update action related deps (ce79cd6)
  • Update non-action deps (88a1ce8)
  • More v24 updates (c45be15)
  • Bump to v3, node24 (7dc11cf)
  • Update deps (f8c6100)
  • Update github actions (#191) (e20b42a)
  • Update github actions to v6 (#195) (f5686a6)
  • Update deps (e058033)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=jakebailey/pyright-action&package-manager=github_actions&previous-version=2.3.3&new-version=3.0.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/pr-check.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 74f5d5a58a3a..42447b873b3b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -155,7 +155,7 @@ jobs: python -m pip install --upgrade -r build/test-requirements.txt - name: Run Pyright - uses: jakebailey/pyright-action@6cabc0f01c4994be48fd45cd9dbacdd6e1ee6e5e # v2.3.3 + uses: jakebailey/pyright-action@8ec14b5cfe41f26e5f41686a31eb6012758217ef # v3.0.2 with: version: 1.1.308 working-directory: 'python_files' diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 82b42c841fce..2a0bbb598cb0 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -138,7 +138,7 @@ jobs: python -m pip install --upgrade -r build/test-requirements.txt - name: Run Pyright - uses: jakebailey/pyright-action@6cabc0f01c4994be48fd45cd9dbacdd6e1ee6e5e # v2.3.3 + uses: jakebailey/pyright-action@8ec14b5cfe41f26e5f41686a31eb6012758217ef # v3.0.2 with: version: 1.1.308 working-directory: 'python_files' From 23555316ba396af06148aa5989d807b2aa8ce42a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:48:38 +0000 Subject: [PATCH 1106/1136] Bump webpack from 5.94.0 to 5.105.0 (#25766) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [webpack](https://github.com/webpack/webpack) from 5.94.0 to 5.105.0.
Release notes

Sourced from webpack's releases.

v5.105.0

Minor Changes

  • Allow resolving worker module by export condition name when using new Worker() (by @​hai-x in #20353)

  • Detect conditional imports to avoid compile-time linking errors for non-existent exports. (by @​hai-x in #20320)

  • Added the tsconfig option for the resolver options (replacement for tsconfig-paths-webpack-plugin). Can be false (disabled), true (use the default tsconfig.json file to search for it), a string path to tsconfig.json, or an object with configFile and references options. (by @​alexander-akait in #20400)

  • Support import.defer() for context modules. (by @​ahabhgk in #20399)

  • Added support for array values ​​to the devtool option. (by @​hai-x in #20191)

  • Improve rendering node built-in modules for ECMA module output. (by @​hai-x in #20255)

  • Unknown import.meta properties are now determined at runtime instead of being statically analyzed at compile time. (by @​xiaoxiaojx in #20312)

Patch Changes

  • Fixed ESM default export handling for .mjs files in Module Federation (by @​y-okt in #20189)

  • Optimized import.meta.env handling in destructuring assignments by using cached stringified environment definitions. (by @​xiaoxiaojx in #20313)

  • Respect the stats.errorStack option in stats output. (by @​samarthsinh2660 in #20258)

  • Fixed a bug where declaring a module variable in module scope would conflict with the default moduleArgument. (by @​xiaoxiaojx in #20265)

  • Fix VirtualUrlPlugin to set resourceData.context for proper module resolution. Previously, when context was not set, it would fallback to the virtual scheme path (e.g., virtual:routes), which is not a valid filesystem path, causing subsequent resolve operations to fail. (by @​xiaoxiaojx in #20390)

  • Fixed Worker self-import handling to support various URL patterns (e.g., import.meta.url, new URL(import.meta.url), new URL(import.meta.url, import.meta.url), new URL("./index.js", import.meta.url)). Workers that resolve to the same module are now properly deduplicated, regardless of the URL syntax used. (by @​xiaoxiaojx in #20381)

  • Reuse the same async entrypoint for the same Worker URL within a module to avoid circular dependency warnings when multiple Workers reference the same resource. (by @​xiaoxiaojx in #20345)

  • Fixed a bug where a self-referencing dependency would have an unused export name when imported inside a web worker. (by @​samarthsinh2660 in #20251)

  • Fix missing export generation when concatenated modules in different chunks share the same runtime in module library bundles. (by @​hai-x in #20346)

  • Fixed import.meta.env.xxx behavior: when accessing a non-existent property, it now returns empty object instead of full object at runtime. (by @​xiaoxiaojx in #20289)

  • Improved parsing error reporting by adding a link to the loader documentation. (by @​gaurav10gg in #20244)

  • Fix typescript types. (by @​alexander-akait in #20305)

  • Add declaration for unused harmony import specifier. (by @​hai-x in #20286)

  • Fix compressibility of modules while retaining portability. (by @​dmichon-msft in #20287)

  • Optimize source map generation: only include ignoreList property when it has content, avoiding empty arrays in source maps. (by @​xiaoxiaojx in #20319)

  • Preserve star exports for dependencies in ECMA module output. (by @​hai-x in #20293)

... (truncated)

Changelog

Sourced from webpack's changelog.

5.105.0

Minor Changes

  • Allow resolving worker module by export condition name when using new Worker() (by @​hai-x in #20353)

  • Detect conditional imports to avoid compile-time linking errors for non-existent exports. (by @​hai-x in #20320)

  • Added the tsconfig option for the resolver options (replacement for tsconfig-paths-webpack-plugin). Can be false (disabled), true (use the default tsconfig.json file to search for it), a string path to tsconfig.json, or an object with configFile and references options. (by @​alexander-akait in #20400)

  • Support import.defer() for context modules. (by @​ahabhgk in #20399)

  • Added support for array values ​​to the devtool option. (by @​hai-x in #20191)

  • Improve rendering node built-in modules for ECMA module output. (by @​hai-x in #20255)

  • Unknown import.meta properties are now determined at runtime instead of being statically analyzed at compile time. (by @​xiaoxiaojx in #20312)

Patch Changes

  • Fixed ESM default export handling for .mjs files in Module Federation (by @​y-okt in #20189)

  • Optimized import.meta.env handling in destructuring assignments by using cached stringified environment definitions. (by @​xiaoxiaojx in #20313)

  • Respect the stats.errorStack option in stats output. (by @​samarthsinh2660 in #20258)

  • Fixed a bug where declaring a module variable in module scope would conflict with the default moduleArgument. (by @​xiaoxiaojx in #20265)

  • Fix VirtualUrlPlugin to set resourceData.context for proper module resolution. Previously, when context was not set, it would fallback to the virtual scheme path (e.g., virtual:routes), which is not a valid filesystem path, causing subsequent resolve operations to fail. (by @​xiaoxiaojx in #20390)

  • Fixed Worker self-import handling to support various URL patterns (e.g., import.meta.url, new URL(import.meta.url), new URL(import.meta.url, import.meta.url), new URL("./index.js", import.meta.url)). Workers that resolve to the same module are now properly deduplicated, regardless of the URL syntax used. (by @​xiaoxiaojx in #20381)

  • Reuse the same async entrypoint for the same Worker URL within a module to avoid circular dependency warnings when multiple Workers reference the same resource. (by @​xiaoxiaojx in #20345)

  • Fixed a bug where a self-referencing dependency would have an unused export name when imported inside a web worker. (by @​samarthsinh2660 in #20251)

  • Fix missing export generation when concatenated modules in different chunks share the same runtime in module library bundles. (by @​hai-x in #20346)

  • Fixed import.meta.env.xxx behavior: when accessing a non-existent property, it now returns empty object instead of full object at runtime. (by @​xiaoxiaojx in #20289)

  • Improved parsing error reporting by adding a link to the loader documentation. (by @​gaurav10gg in #20244)

  • Fix typescript types. (by @​alexander-akait in #20305)

  • Add declaration for unused harmony import specifier. (by @​hai-x in #20286)

  • Fix compressibility of modules while retaining portability. (by @​dmichon-msft in #20287)

  • Optimize source map generation: only include ignoreList property when it has content, avoiding empty arrays in source maps. (by @​xiaoxiaojx in #20319)

... (truncated)

Commits
  • 1486f9a chore(release): new release
  • 1a517f6 feat: added the tsconfig option for the resolver options (#20400)
  • 7b3b0f7 feat: support import.defer() for context modules
  • c4a6a92 refactor: more types and increase types coverage
  • 5ecc58d feat: consider asset module as side-effect-free (#20352)
  • cce0f69 test: avoid comma operator in BinaryMiddleware test (#20398)
  • cd4793d feat: support import specifier guard (#20320)
  • fe48655 docs: update examples (#20397)
  • de107f8 fix(VirtualUrlPlugin): set resourceData.context to avoid invalid fallback (#2...
  • a656ab1 test: add self-import test case for dynamic import (#20389)
  • Additional commits viewable in compare view
Maintainer changes

This version was pushed to npm by [GitHub Actions](https://www.npmjs.com/~GitHub Actions), a new releaser for webpack since your current version.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=webpack&package-manager=npm_and_yarn&previous-version=5.94.0&new-version=5.105.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 1014 ++++++++++++++++++++++++++++++--------------- package.json | 2 +- 2 files changed, 690 insertions(+), 326 deletions(-) diff --git a/package-lock.json b/package-lock.json index 96e5097bb9c7..3b5a9c8991f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -104,7 +104,7 @@ "typemoq": "^2.1.0", "typescript": "~5.2", "uuid": "^8.3.2", - "webpack": "^5.76.0", + "webpack": "^5.105.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", "webpack-fix-default-import-plugin": "^1.0.3", @@ -1265,9 +1265,9 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1780,10 +1780,30 @@ "@types/node": "*" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, "node_modules/@types/fs-extra": { @@ -2516,148 +2536,148 @@ } }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -2710,10 +2730,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "license": "MIT", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "bin": { "acorn": "bin/acorn" }, @@ -2729,13 +2748,16 @@ "acorn": "^8" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, + "engines": { + "node": ">=10.13.0" + }, "peerDependencies": { - "acorn": "^8" + "acorn": "^8.14.0" } }, "node_modules/acorn-jsx": { @@ -2813,6 +2835,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -3420,6 +3481,15 @@ } ] }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/bent": { "version": "7.3.12", "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", @@ -3641,9 +3711,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -3660,10 +3730,11 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -3880,9 +3951,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001655", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz", - "integrity": "sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==", + "version": "1.0.30001768", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz", + "integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==", "dev": true, "funding": [ { @@ -5386,9 +5457,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.13", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", - "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "dev": true }, "node_modules/elliptic": { @@ -5446,13 +5517,13 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -5557,9 +5628,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true }, "node_modules/es-object-atoms": { @@ -6662,6 +6733,22 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, "node_modules/fastest-levenshtein": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", @@ -9483,12 +9570,16 @@ } }, "node_modules/loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loader-utils": { @@ -10759,9 +10850,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true }, "node_modules/node-stream-zip": { @@ -12087,6 +12178,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-in-the-middle": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", @@ -13185,12 +13285,16 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar-fs": { @@ -13295,13 +13399,13 @@ } }, "node_modules/terser": { - "version": "5.31.6", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", - "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -13313,16 +13417,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -13346,6 +13450,59 @@ } } }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -14116,9 +14273,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -14135,8 +14292,8 @@ } ], "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -14496,9 +14653,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -14509,34 +14666,36 @@ } }, "node_modules/webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" @@ -14767,14 +14926,67 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, "engines": { "node": ">=10.13.0" } }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -16125,9 +16337,9 @@ "dev": true }, "@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "requires": { "@jridgewell/gen-mapping": "^0.3.5", @@ -16573,10 +16785,30 @@ "@types/node": "*" } }, + "@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, "@types/fs-extra": { @@ -17097,148 +17329,148 @@ "optional": true }, "@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "requires": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true }, "@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true }, "@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true }, "@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true }, "@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "requires": { "@xtuc/ieee754": "^1.2.0" } }, "@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "requires": { "@xtuc/long": "4.2.2" } }, "@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true }, "@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -17278,9 +17510,9 @@ "dev": true }, "acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==" + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==" }, "acorn-import-assertions": { "version": "1.9.0", @@ -17288,10 +17520,10 @@ "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "requires": {} }, - "acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "requires": {} }, @@ -17348,6 +17580,35 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -17805,6 +18066,12 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, + "baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true + }, "bent": { "version": "7.3.12", "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", @@ -17995,15 +18262,16 @@ } }, "browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" } }, "buffer": { @@ -18159,9 +18427,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001655", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz", - "integrity": "sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==", + "version": "1.0.30001768", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz", + "integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==", "dev": true }, "caseless": { @@ -19313,9 +19581,9 @@ } }, "electron-to-chromium": { - "version": "1.5.13", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", - "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "dev": true }, "elliptic": { @@ -19371,13 +19639,13 @@ } }, "enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "dev": true, "requires": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" } }, "entities": { @@ -19457,9 +19725,9 @@ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, "es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true }, "es-object-atoms": { @@ -20266,6 +20534,12 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true + }, "fastest-levenshtein": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", @@ -22367,9 +22641,9 @@ } }, "loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true }, "loader-utils": { @@ -23340,9 +23614,9 @@ } }, "node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true }, "node-stream-zip": { @@ -24343,6 +24617,12 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-in-the-middle": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.2.0.tgz", @@ -25135,9 +25415,9 @@ } }, "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true }, "tar-fs": { @@ -25234,28 +25514,69 @@ } }, "terser": { - "version": "5.31.6", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", - "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "requires": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" } }, "terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "dev": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + } } }, "test-exclude": { @@ -25839,13 +26160,13 @@ } }, "update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "requires": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" } }, "uri-js": { @@ -26137,9 +26458,9 @@ } }, "watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "requires": { "glob-to-regexp": "^0.4.1", @@ -26147,34 +26468,77 @@ } }, "webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", - "dev": true, - "requires": { - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + } } }, "webpack-bundle-analyzer": { @@ -26324,9 +26688,9 @@ "requires": {} }, "webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true }, "which": { diff --git a/package.json b/package.json index d2d28bee1c4c..043e14d9f341 100644 --- a/package.json +++ b/package.json @@ -1800,7 +1800,7 @@ "typemoq": "^2.1.0", "typescript": "~5.2", "uuid": "^8.3.2", - "webpack": "^5.76.0", + "webpack": "^5.105.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", "webpack-fix-default-import-plugin": "^1.0.3", From d06be726c794611d19069d3416b2979b3015649d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:49:50 -0800 Subject: [PATCH 1107/1136] Bump minimatch from 5.1.0 to 5.1.8 (#25823) Bumps [minimatch](https://github.com/isaacs/minimatch) from 5.1.0 to 5.1.8.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=minimatch&package-manager=npm_and_yarn&previous-version=5.1.0&new-version=5.1.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 36 +++++++----------------------------- package.json | 2 +- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3b5a9c8991f1..2f55d050f4d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "inversify": "^6.0.2", "jsonc-parser": "^3.0.0", "lodash": "^4.17.23", - "minimatch": "^5.0.1", + "minimatch": "^5.1.8", "named-js-regexp": "^1.3.3", "node-stream-zip": "^1.6.0", "reflect-metadata": "^0.2.2", @@ -10002,9 +10002,9 @@ "dev": true }, "node_modules/minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.8.tgz", + "integrity": "sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -10343,19 +10343,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mocha/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -22982,9 +22969,9 @@ "dev": true }, "minimatch": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", - "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.8.tgz", + "integrity": "sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ==", "requires": { "brace-expansion": "^2.0.1" }, @@ -23188,15 +23175,6 @@ "p-locate": "^5.0.0" } }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index 043e14d9f341..847926dccc64 100644 --- a/package.json +++ b/package.json @@ -1714,7 +1714,7 @@ "inversify": "^6.0.2", "jsonc-parser": "^3.0.0", "lodash": "^4.17.23", - "minimatch": "^5.0.1", + "minimatch": "^5.1.8", "named-js-regexp": "^1.3.3", "node-stream-zip": "^1.6.0", "reflect-metadata": "^0.2.2", From 8a61a8736d60d2de309736c0598ffa8c15516617 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:50:29 -0800 Subject: [PATCH 1108/1136] Bump tomli from 2.3.0 to 2.4.0 (#25726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tomli](https://github.com/hukkin/tomli) from 2.3.0 to 2.4.0.
Changelog

Sourced from tomli's changelog.

2.4.0

  • Added
    • TOML v1.1.0 compatibility
    • Binary wheels for Windows arm64
Commits
  • a678e6f Bump version: 2.3.0 → 2.4.0
  • b8a1358 Tests: remove now needless "TOML compliance"->"burntsushi" format conversion
  • 4979375 Update GitHub actions
  • f890dd1 Update pre-commit hooks
  • d9c65c3 Add 2.4.0 change log
  • 0efe49d Update README for v2.4.0
  • 9eb2125 TOML 1.1: Make seconds optional in Date-Time and Time (#203)
  • 12314bd TOML 1.1: Add \xHH Unicode escape code to basic strings (#202)
  • 2a2aa62 TOML 1.1: Allow newlines and trailing comma in inline tables (#200)
  • 38297f8 Xfail on tests for TOML 1.1 features not yet supported
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tomli&package-manager=pip&previous-version=2.3.0&new-version=2.4.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 91 +++++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/requirements.txt b/requirements.txt index ae747359d4e2..e9e99ec59363 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,49 +12,54 @@ packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f # via -r requirements.in -tomli==2.3.0 \ - --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ - --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ - --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ - --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ - --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ - --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ - --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ - --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ - --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ - --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ - --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ - --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ - --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ - --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ - --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ - --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ - --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ - --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ - --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ - --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ - --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ - --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ - --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ - --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ - --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ - --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ - --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ - --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ - --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ - --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ - --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ - --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ - --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ - --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ - --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ - --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ - --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ - --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ - --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ - --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ - --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ - --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 +tomli==2.4.0 \ + --hash=sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729 \ + --hash=sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b \ + --hash=sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d \ + --hash=sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df \ + --hash=sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576 \ + --hash=sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d \ + --hash=sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1 \ + --hash=sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a \ + --hash=sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e \ + --hash=sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc \ + --hash=sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702 \ + --hash=sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6 \ + --hash=sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd \ + --hash=sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4 \ + --hash=sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776 \ + --hash=sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a \ + --hash=sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66 \ + --hash=sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87 \ + --hash=sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2 \ + --hash=sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f \ + --hash=sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475 \ + --hash=sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f \ + --hash=sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95 \ + --hash=sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9 \ + --hash=sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3 \ + --hash=sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9 \ + --hash=sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76 \ + --hash=sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da \ + --hash=sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8 \ + --hash=sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51 \ + --hash=sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86 \ + --hash=sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8 \ + --hash=sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0 \ + --hash=sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b \ + --hash=sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1 \ + --hash=sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e \ + --hash=sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d \ + --hash=sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c \ + --hash=sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867 \ + --hash=sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a \ + --hash=sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c \ + --hash=sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0 \ + --hash=sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4 \ + --hash=sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614 \ + --hash=sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132 \ + --hash=sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa \ + --hash=sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087 # via -r requirements.in typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ From d7cf7fec53937b8a79898430c3c7c2707b3fbced Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:50:48 -0800 Subject: [PATCH 1109/1136] Bump packaging from 25.0 to 26.0 (#25747) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [packaging](https://github.com/pypa/packaging) from 25.0 to 26.0.
Release notes

Sourced from packaging's releases.

26.0

Read about the performance improvements here: https://iscinumpy.dev/post/packaging-faster.

What's Changed

Features:

Behavior adaptations:

Fixes:

Performance:

... (truncated)

Changelog

Sourced from packaging's changelog.

26.0 - 2026-01-20


Features:
  • PEP 751: support pylock (:pull:900)
  • PEP 794: import name metadata (:pull:948)
  • Support for writing metadata to a file (:pull:846)
  • Support __replace__ on Version (:pull:1003)
  • Support positional pattern matching for Version and SpecifierSet (:pull:1004)

Behavior adaptations:

  • PEP 440 handling of prereleases for Specifier.contains, SpecifierSet.contains, and SpecifierSet.filter (:pull:897)
  • Handle PEP 440 edge case in SpecifierSet.filter (:pull:942)
  • Adjust arbitrary equality intersection preservation in SpecifierSet (:pull:951)
  • Return False instead of raising for .contains with invalid version (:pull:932)
  • Support arbitrary equality on arbitrary strings for Specifier and SpecifierSet's filter and contains method. (:pull:954)
  • Only try to parse as Version on certain marker keys, return False on unequal ordered comparisons (:pull:939)

Fixes:

  • Update _hash when unpickling Tag() (:pull:860)
  • Correct comment and simplify implicit prerelease handling in Specifier.prereleases (:pull:896)
  • Use explicit _GLibCVersion NamedTuple in _manylinux (:pull:868)
  • Detect invalid license expressions containing () (:pull:879)
  • Correct regex for metadata 'name' format (:pull:925)
  • Improve the message around expecting a semicolon (:pull:833)
  • Support nested parens in license expressions (:pull:931)
  • Add space before at symbol in Requirements string (:pull:953)
  • A root logger use found, use a packaging logger instead (:pull:965)
  • Better support for subclassing Marker and Requirement (:pull:1022)
  • Normalize all extras, not just if it comes first (:pull:1024)
  • Don't produce a broken repr if Marker fails to construct (:pull:1033)

Performance:

  • Avoid recompiling regexes in the tokenizer for a 3x speedup (:pull:1019)
  • Improve performance in _manylinux.py (:pull:869)
  • Minor cleanups to Version (:pull:913)
  • Skip redundant creation of Version's in specifier comparison (:pull:986)
  • Cache the Specifier's Version (:pull:985)
  • Make Version a little faster (:pull:987)
  • Minor Version regex cleanup (:pull:990)
  • Faster regex on Python 3.11.5+ for Version (:pull:988, :pull:1055)
  • Lazily calculate _key in Version (:pull:989, :pull:1048)
  • Faster canonicalize_version (:pull:993)
  • Use re.fullmatch in a couple more places (:pull:992, :pull:1029)
  • Use map instead of generator (:pull:996)
  • Deprecate ._version (_Version, a NamedTuple) (:pull:995, :pull:1062)
    </tr></table>

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=packaging&package-manager=pip&previous-version=25.0&new-version=26.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index e9e99ec59363..68850210d58c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,9 @@ microvenv==2025.0 \ --hash=sha256:568155ec18af01c89f270d35d123ab803b09672b480c3702d15fd69e9cc5bd1e \ --hash=sha256:8a2568a8390a4ffb5af2f05e7642454e03b887e582d192b6316326974eab5d0f # via -r requirements.in -packaging==25.0 \ - --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ - --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f +packaging==26.0 \ + --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ + --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 # via -r requirements.in tomli==2.4.0 \ --hash=sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729 \ From 3b9c6e9bf87ee0cd00731012e75f3bf59fc19e62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:10:55 -0800 Subject: [PATCH 1110/1136] Bump qs from 6.14.1 to 6.14.2 (#25796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [qs](https://github.com/ljharb/qs) from 6.14.1 to 6.14.2.
Changelog

Sourced from qs's changelog.

6.14.2

  • [Fix] parse: mark overflow objects for indexed notation exceeding arrayLimit (#546)
  • [Fix] arrayLimit means max count, not max index, in combine/merge/parseArrayValue
  • [Fix] parse: throw on arrayLimit exceeded with indexed notation when throwOnLimitExceeded is true (#529)
  • [Fix] parse: enforce arrayLimit on comma-parsed values
  • [Fix] parse: fix error message to reflect arrayLimit as max index; remove extraneous comments (#545)
  • [Robustness] avoid .push, use void
  • [readme] document that addQueryPrefix does not add ? to empty output (#418)
  • [readme] clarify parseArrays and arrayLimit documentation (#543)
  • [readme] replace runkit CI badge with shields.io check-runs badge
  • [meta] fix changelog typo (arrayLengtharrayLimit)
  • [actions] fix rebase workflow permissions
Commits
  • bdcf0c7 v6.14.2
  • 294db90 [readme] document that addQueryPrefix does not add ? to empty output
  • 5c308e5 [readme] clarify parseArrays and arrayLimit documentation
  • 6addf8c [Fix] parse: mark overflow objects for indexed notation exceeding arrayLimit
  • cfc108f [Fix] arrayLimit means max count, not max index, in combine/merge/`pars...
  • febb644 [Fix] parse: throw on arrayLimit exceeded with indexed notation when `thr...
  • f6a7abf [Fix] parse: enforce arrayLimit on comma-parsed values
  • fbc5206 [Fix] parse: fix error message to reflect arrayLimit as max index; remove e...
  • 1b9a8b4 [actions] fix rebase workflow permissions
  • 2a35775 [meta] fix changelog typo (arrayLengtharrayLimit)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=qs&package-manager=npm_and_yarn&previous-version=6.14.1&new-version=6.14.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f55d050f4d2..bab844f91162 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11884,9 +11884,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dev": true, "dependencies": { "side-channel": "^1.1.0" @@ -24381,9 +24381,9 @@ "dev": true }, "qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dev": true, "requires": { "side-channel": "^1.1.0" From aab1c364b3a8a34f9f1a1547d760d8f3e6a5de22 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:05:39 -0800 Subject: [PATCH 1111/1136] Add environment discovery logging and messages when using Python Environments extension (#25830) should help users find that the python environments extension is installed and may be the item causing problems for those who don't know we are moving to the envs extension for handling discovery --- .../diagnostics/checks/pythonInterpreter.ts | 11 ++++++-- src/client/common/utils/localize.ts | 18 +++++++++++++ src/client/envExt/api.internal.ts | 21 ++++++++++++--- src/client/envExt/envExtApi.ts | 26 ++++++++++++++++--- 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/client/application/diagnostics/checks/pythonInterpreter.ts b/src/client/application/diagnostics/checks/pythonInterpreter.ts index 31da53e75357..9167e232a417 100644 --- a/src/client/application/diagnostics/checks/pythonInterpreter.ts +++ b/src/client/application/diagnostics/checks/pythonInterpreter.ts @@ -20,7 +20,7 @@ import { IDiagnosticHandlerService, IDiagnosticMessageOnCloseHandler, } from '../types'; -import { Common } from '../../../common/utils/localize'; +import { Common, Interpreters } from '../../../common/utils/localize'; import { Commands } from '../../../common/constants'; import { ICommandManager, IWorkspaceService } from '../../../common/application/types'; import { sendTelemetryEvent } from '../../../telemetry'; @@ -30,11 +30,12 @@ import { cache } from '../../../common/utils/decorators'; import { noop } from '../../../common/utils/misc'; import { getEnvironmentVariable, getOSType, OSType } from '../../../common/utils/platform'; import { IFileSystem } from '../../../common/platform/types'; -import { traceError } from '../../../logging'; +import { traceError, traceWarn } from '../../../logging'; import { getExecutable } from '../../../common/process/internal/python'; import { getSearchPathEnvVarNames } from '../../../common/utils/exec'; import { IProcessServiceFactory } from '../../../common/process/types'; import { normCasePath } from '../../../common/platform/fs-paths'; +import { useEnvExtension } from '../../../envExt/api.internal'; const messages = { [DiagnosticCodes.NoPythonInterpretersDiagnostic]: l10n.t( @@ -144,6 +145,9 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService const isInterpreterSetToDefault = interpreterPathService.get(resource) === 'python'; if (!hasInterpreters && isInterpreterSetToDefault) { + if (useEnvExtension()) { + traceWarn(Interpreters.envExtDiscoveryNoEnvironments); + } return [ new InvalidPythonInterpreterDiagnostic( DiagnosticCodes.NoPythonInterpretersDiagnostic, @@ -156,6 +160,9 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService const currentInterpreter = await interpreterService.getActiveInterpreter(resource); if (!currentInterpreter) { + if (useEnvExtension()) { + traceWarn(Interpreters.envExtNoActiveEnvironment); + } return [ new InvalidPythonInterpreterDiagnostic( DiagnosticCodes.InvalidPythonInterpreterDiagnostic, diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index d108dfddb54b..7b7560c74e05 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -191,6 +191,24 @@ export namespace Interpreters { export const installingPython = l10n.t('Installing Python into Environment...'); export const discovering = l10n.t('Discovering Python Interpreters'); export const refreshing = l10n.t('Refreshing Python Interpreters'); + export const envExtDiscoveryAttribution = l10n.t( + 'Environment discovery is managed by the Python Environments extension (ms-python.vscode-python-envs). Check the "Python Environments" output channel for environment-specific logs.', + ); + export const envExtDiscoveryFailed = l10n.t( + 'Environment discovery failed. Check the "Python Environments" output channel for details. The Python Environments extension (ms-python.vscode-python-envs) manages environment discovery.', + ); + export const envExtDiscoverySlow = l10n.t( + 'Environment discovery is taking longer than expected. Check the "Python Environments" output channel for progress. The Python Environments extension (ms-python.vscode-python-envs) manages environment discovery.', + ); + export const envExtActivationFailed = l10n.t( + 'Failed to activate the Python Environments extension (ms-python.vscode-python-envs), which is required for environment discovery. Please ensure it is installed and enabled.', + ); + export const envExtDiscoveryNoEnvironments = l10n.t( + 'Environment discovery completed but no Python environments were found. Check the "Python Environments" output channel for details.', + ); + export const envExtNoActiveEnvironment = l10n.t( + 'No Python environment is set for this resource. Check the "Python Environments" output channel for details, or select an interpreter.', + ); export const condaInheritEnvMessage = l10n.t( 'We noticed you\'re using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change "terminal.integrated.inheritEnv" to false in your user settings. [Learn more](https://aka.ms/AA66i8f).', ); diff --git a/src/client/envExt/api.internal.ts b/src/client/envExt/api.internal.ts index 07bc58ffc11e..5acdd5bba8e3 100644 --- a/src/client/envExt/api.internal.ts +++ b/src/client/envExt/api.internal.ts @@ -14,6 +14,8 @@ import { } from './types'; import { executeCommand } from '../common/vscodeApis/commandApis'; import { getConfiguration } from '../common/vscodeApis/workspaceApis'; +import { traceError, traceLog } from '../logging'; +import { Interpreters } from '../common/utils/localize'; export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; @@ -22,7 +24,8 @@ export function useEnvExtension(): boolean { if (_useExt !== undefined) { return _useExt; } - const inExpSetting = getConfiguration('python').get('useEnvironmentsExtension', false); + const config = getConfiguration('python'); + const inExpSetting = config?.get('useEnvironmentsExtension', false) ?? false; // If extension is installed and in experiment, then use it. _useExt = !!getExtension(ENVS_EXTENSION_ID) && inExpSetting; return _useExt; @@ -46,12 +49,20 @@ export async function getEnvExtApi(): Promise { } const extension = getExtension(ENVS_EXTENSION_ID); if (!extension) { + traceError(Interpreters.envExtActivationFailed); throw new Error('Python Environments extension not found.'); } if (!extension?.isActive) { - await extension.activate(); + try { + await extension.activate(); + } catch (ex) { + traceError(Interpreters.envExtActivationFailed, ex); + throw ex; + } } + traceLog(Interpreters.envExtDiscoveryAttribution); + _extApi = extension.exports as PythonEnvironmentApi; _extApi.onDidChangeEnvironment((e) => { onDidChangeEnvironmentEnvExtEmitter.fire(e); @@ -70,7 +81,11 @@ export async function runInBackground( export async function getEnvironment(scope: GetEnvironmentScope): Promise { const envExtApi = await getEnvExtApi(); - return envExtApi.getEnvironment(scope); + const env = await envExtApi.getEnvironment(scope); + if (!env) { + traceLog(Interpreters.envExtNoActiveEnvironment); + } + return env; } export async function resolveEnvironment(pythonPath: string): Promise { diff --git a/src/client/envExt/envExtApi.ts b/src/client/envExt/envExtApi.ts index 598899b7d248..34f42f0d6954 100644 --- a/src/client/envExt/envExtApi.ts +++ b/src/client/envExt/envExtApi.ts @@ -17,7 +17,7 @@ import { PythonEnvCollectionChangedEvent } from '../pythonEnvironments/base/watc import { getEnvExtApi } from './api.internal'; import { createDeferred, Deferred } from '../common/utils/async'; import { StopWatch } from '../common/utils/stopWatch'; -import { traceLog } from '../logging'; +import { traceError, traceLog, traceWarn } from '../logging'; import { DidChangeEnvironmentsEventArgs, EnvironmentChangeKind, @@ -27,6 +27,7 @@ import { import { FileChangeType } from '../common/platform/fileSystemWatcher'; import { Architecture, isWindows } from '../common/utils/platform'; import { parseVersion } from '../pythonEnvironments/base/info/pythonVersion'; +import { Interpreters } from '../common/utils/localize'; function getKind(pythonEnv: PythonEnvironment): PythonEnvKind { if (pythonEnv.envId.managerId.toLowerCase().endsWith('system')) { @@ -242,13 +243,23 @@ class EnvExtApis implements IDiscoveryAPI, Disposable { this._onProgress.fire({ stage: this.refreshState }); this._refreshPromise = createDeferred(); + const SLOW_DISCOVERY_THRESHOLD_MS = 25_000; + const slowDiscoveryTimer = setTimeout(() => { + traceWarn(Interpreters.envExtDiscoverySlow); + }, SLOW_DISCOVERY_THRESHOLD_MS); + setImmediate(async () => { try { await this.envExtApi.refreshEnvironments(undefined); + if (this._envs.length === 0) { + traceWarn(Interpreters.envExtDiscoveryNoEnvironments); + } this._refreshPromise?.resolve(); } catch (error) { + traceError(Interpreters.envExtDiscoveryFailed, error); this._refreshPromise?.reject(error); } finally { + clearTimeout(slowDiscoveryTimer); traceLog(`Native locator: Refresh finished in ${stopwatch.elapsedTime} ms`); this.refreshState = ProgressReportStage.discoveryFinished; this._refreshPromise = undefined; @@ -297,9 +308,16 @@ class EnvExtApis implements IDiscoveryAPI, Disposable { if (envPath === undefined) { return undefined; } - const pythonEnv = await this.envExtApi.resolveEnvironment(Uri.file(envPath)); - if (pythonEnv) { - return this.addEnv(pythonEnv); + try { + const pythonEnv = await this.envExtApi.resolveEnvironment(Uri.file(envPath)); + if (pythonEnv) { + return this.addEnv(pythonEnv); + } + } catch (error) { + traceError( + `Failed to resolve environment "${envPath}" via the Python Environments extension (ms-python.vscode-python-envs). Check the "Python Environments" output channel for details.`, + error, + ); } return undefined; } From bec2bbd6975e3d052b4a9a71e620c2242f64ddda Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:20:34 +0000 Subject: [PATCH 1112/1136] Run npm audit fix (non-breaking) (#25832) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applies `npm audit fix` (no `--force`) to resolve 5 of 26 reported vulnerabilities. Only `package-lock.json` is modified — semver ranges in `package.json` are unchanged. ## Packages updated | Package | Before | After | CVE/Advisory | |---|---|---|---| | `cipher-base` | 1.0.4 | 1.0.7 | [GHSA-cpq7-6gpm-g9rc](https://github.com/advisories/GHSA-cpq7-6gpm-g9rc) — **critical**, missing type checks | | `ajv` | 6.12.6 / 8.17.1 | 6.14.0 / 8.18.0 | [GHSA-2g4f-4pwh-qvx6](https://github.com/advisories/GHSA-2g4f-4pwh-qvx6) — ReDoS via `$data` | | `bn.js` | 4.11.8 / 5.2.1 | 4.12.3 / 5.2.3 | [GHSA-378v-28hj-76wf](https://github.com/advisories/GHSA-378v-28hj-76wf) — infinite loop | | `glob` | 10.4.5 | 10.5.0 | [GHSA-5j98-mcp5-4vw2](https://github.com/advisories/GHSA-5j98-mcp5-4vw2) — CLI command injection | | `minimatch` (3.x / 9.x) | 3.1.2 / 9.0.x | 3.1.5 / 9.0.9 | [GHSA-3ppc-4f35-3m26](https://github.com/advisories/GHSA-3ppc-4f35-3m26) — ReDoS | ## Remaining vulnerabilities (21) All require `--force` and involve breaking changes (e.g. mocha downgrade, `copy-webpack-plugin` major bump, `node-polyfill-webpack-plugin` major bump). Not addressed here per the constraint of no forced updates.
Original prompt > Run npm audit fix. Do not use force flag.
Created from [VS Code](https://code.visualstudio.com/docs/copilot/copilot-coding-agent). --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --- package-lock.json | 996 ++++++++++++++++++++++++++-------------------- 1 file changed, 554 insertions(+), 442 deletions(-) diff --git a/package-lock.json b/package-lock.json index bab844f91162..e015ad836067 100644 --- a/package-lock.json +++ b/package-lock.json @@ -945,9 +945,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1043,9 +1043,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2524,10 +2524,11 @@ } }, "node_modules/@vscode/vsce/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2820,10 +2821,11 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2853,10 +2855,11 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3225,15 +3228,15 @@ } }, "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", "dev": true, + "license": "MIT", "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" + "minimalistic-assert": "^1.0.0" } }, "node_modules/assert": { @@ -3543,10 +3546,11 @@ } }, "node_modules/bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true, + "license": "MIT" }, "node_modules/boolbase": { "version": "1.0.0", @@ -3626,60 +3630,75 @@ } }, "node_modules/browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", "dev": true, + "license": "MIT", "dependencies": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/browserify-rsa/node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserify-rsa/node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, "node_modules/browserify-sign": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", - "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", "dev": true, + "license": "ISC", "dependencies": { - "bn.js": "^5.2.1", - "browserify-rsa": "^4.1.0", + "bn.js": "^5.2.2", + "browserify-rsa": "^4.1.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.4", + "elliptic": "^6.6.1", "inherits": "^2.0.4", - "parse-asn1": "^5.1.6", - "readable-stream": "^3.6.2", + "parse-asn1": "^5.1.9", + "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1" }, "engines": { - "node": ">= 4" + "node": ">= 0.10" } }, "node_modules/browserify-sign/node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, - "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } + "license": "MIT" }, "node_modules/browserify-sign/node_modules/safe-buffer": { "version": "5.2.1", @@ -4330,15 +4349,41 @@ } }, "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", "dev": true, + "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" } }, + "node_modules/cipher-base/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/circular-json": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", @@ -4653,13 +4698,14 @@ "dev": true }, "node_modules/create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", "dev": true, + "license": "MIT", "dependencies": { "bn.js": "^4.1.0", - "elliptic": "^6.0.0" + "elliptic": "^6.5.3" } }, "node_modules/create-hash": { @@ -4806,25 +4852,30 @@ } }, "node_modules/crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", "dev": true, + "license": "MIT", "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" }, "engines": { - "node": "*" + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/damerau-levenshtein": { @@ -5279,10 +5330,11 @@ } }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -5478,12 +5530,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, "node_modules/emitter-listener": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", @@ -5876,10 +5922,11 @@ } }, "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5942,10 +5989,11 @@ "dev": true }, "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6013,10 +6061,11 @@ } }, "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6296,10 +6345,11 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -7414,9 +7464,10 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -10057,29 +10108,30 @@ "optional": true }, "node_modules/mocha": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", - "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", "dev": true, "license": "MIT", "dependencies": { - "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", + "chokidar": "^4.0.1", "debug": "^4.3.5", - "diff": "^5.2.0", + "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", + "minimatch": "^9.0.5", "ms": "^2.1.3", + "picocolors": "^1.1.1", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", - "workerpool": "^6.5.1", + "workerpool": "^9.2.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" @@ -10141,16 +10193,6 @@ } } }, - "node_modules/mocha/node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/mocha/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -10167,6 +10209,22 @@ "balanced-match": "^1.0.0" } }, + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/mocha/node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -10216,9 +10274,9 @@ } }, "node_modules/mocha/node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -10271,9 +10329,10 @@ } }, "node_modules/mocha/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -10291,22 +10350,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/mocha/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -10343,6 +10386,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mocha/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mocha/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10389,6 +10448,20 @@ "node": ">=8" } }, + "node_modules/mocha/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/mocha/node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11373,18 +11446,43 @@ } }, "node_modules/parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", "dev": true, + "license": "ISC", "dependencies": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.5", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, + "node_modules/parse-asn1/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/parse-filepath": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", @@ -11555,55 +11653,21 @@ } }, "node_modules/pbkdf2": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz", - "integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", "dev": true, "license": "MIT", "dependencies": { - "create-hash": "~1.1.3", + "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "ripemd160": "=2.0.1", + "ripemd160": "^2.0.3", "safe-buffer": "^5.2.1", - "sha.js": "^2.4.11", - "to-buffer": "^1.2.0" + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" }, "engines": { - "node": ">=0.12" - } - }, - "node_modules/pbkdf2/node_modules/create-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", - "integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "sha.js": "^2.4.0" - } - }, - "node_modules/pbkdf2/node_modules/hash-base": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", - "integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1" - } - }, - "node_modules/pbkdf2/node_modules/ripemd160": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", - "integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hash-base": "^2.0.0", - "inherits": "^2.0.1" + "node": ">= 0.10" } }, "node_modules/pbkdf2/node_modules/safe-buffer": { @@ -12022,10 +12086,11 @@ } }, "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -12316,15 +12381,56 @@ } }, "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", "dev": true, + "license": "MIT", "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" } }, + "node_modules/ripemd160/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12794,10 +12900,11 @@ } }, "node_modules/sinon/node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -13404,15 +13511,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -13438,10 +13545,11 @@ } }, "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -13505,10 +13613,11 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -13609,9 +13718,9 @@ "dev": true }, "node_modules/to-buffer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", - "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "dev": true, "license": "MIT", "dependencies": { @@ -14601,11 +14710,12 @@ } }, "node_modules/vscode-languageclient/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -14922,10 +15032,11 @@ } }, "node_modules/webpack/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -15073,9 +15184,9 @@ } }, "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", "dev": true, "license": "Apache-2.0" }, @@ -16111,9 +16222,9 @@ } }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -16175,9 +16286,9 @@ } }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -17225,9 +17336,9 @@ } }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -17556,9 +17667,9 @@ } }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -17577,9 +17688,9 @@ }, "dependencies": { "ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "requires": { "fast-deep-equal": "^3.1.3", @@ -17841,15 +17952,14 @@ } }, "asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", "dev": true, "requires": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" + "minimalistic-assert": "^1.0.0" } }, "assert": { @@ -18101,9 +18211,9 @@ } }, "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true }, "boolbase": { @@ -18180,57 +18290,53 @@ } }, "browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", "dev": true, "requires": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" }, "dependencies": { "bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "dev": true + }, + "safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true } } }, "browserify-sign": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", - "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", "dev": true, "requires": { - "bn.js": "^5.2.1", - "browserify-rsa": "^4.1.0", + "bn.js": "^5.2.2", + "browserify-rsa": "^4.1.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.4", + "elliptic": "^6.6.1", "inherits": "^2.0.4", - "parse-asn1": "^5.1.6", - "readable-stream": "^3.6.2", + "parse-asn1": "^5.1.9", + "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1" }, "dependencies": { "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "dev": true }, - "readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -18683,13 +18789,22 @@ } }, "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", "dev": true, "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "circular-json": { @@ -18944,13 +19059,13 @@ "dev": true }, "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", "dev": true, "requires": { "bn.js": "^4.1.0", - "elliptic": "^6.0.0" + "elliptic": "^6.5.3" } }, "create-hash": { @@ -19066,22 +19181,23 @@ "dev": true }, "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", "dev": true, "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" } }, "damerau-levenshtein": { @@ -19423,9 +19539,9 @@ "requires": {} }, "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true }, "diffie-hellman": { @@ -19586,14 +19702,6 @@ "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } } }, "emitter-listener": { @@ -19971,9 +20079,9 @@ } }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -20121,9 +20229,9 @@ } }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -20174,9 +20282,9 @@ "dev": true }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -20219,9 +20327,9 @@ "dev": true }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -20981,9 +21089,9 @@ }, "dependencies": { "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "requires": { "brace-expansion": "^1.1.7" } @@ -23015,39 +23123,34 @@ "optional": true }, "mocha": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", - "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", "dev": true, "requires": { - "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", + "chokidar": "^4.0.1", "debug": "^4.3.5", - "diff": "^5.2.0", + "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", + "minimatch": "^9.0.5", "ms": "^2.1.3", + "picocolors": "^1.1.1", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", - "workerpool": "^6.5.1", + "workerpool": "^9.2.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "dependencies": { - "ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true - }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -23063,6 +23166,15 @@ "balanced-match": "^1.0.0" } }, + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "requires": { + "readdirp": "^4.0.1" + } + }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -23095,9 +23207,9 @@ } }, "diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true }, "escape-string-regexp": { @@ -23127,9 +23239,9 @@ } }, "glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "requires": { "foreground-child": "^3.1.0", @@ -23138,17 +23250,6 @@ "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" - }, - "dependencies": { - "minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } } }, "has-flag": { @@ -23175,6 +23276,15 @@ "p-locate": "^5.0.0" } }, + "minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.2" + } + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -23205,6 +23315,12 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -23982,16 +24098,24 @@ } }, "parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", "dev": true, "requires": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.5", + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "parse-filepath": { @@ -24131,50 +24255,19 @@ "dev": true }, "pbkdf2": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz", - "integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", "dev": true, "requires": { - "create-hash": "~1.1.3", + "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "ripemd160": "=2.0.1", + "ripemd160": "^2.0.3", "safe-buffer": "^5.2.1", - "sha.js": "^2.4.11", - "to-buffer": "^1.2.0" + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" }, "dependencies": { - "create-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", - "integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "sha.js": "^2.4.0" - } - }, - "hash-base": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", - "integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==", - "dev": true, - "requires": { - "inherits": "^2.0.1" - } - }, - "ripemd160": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", - "integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==", - "dev": true, - "requires": { - "hash-base": "^2.0.0", - "inherits": "^2.0.1" - } - }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -24481,9 +24574,9 @@ } }, "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -24704,13 +24797,33 @@ } }, "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", "dev": true, "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "dependencies": { + "hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "run-parallel": { @@ -25030,9 +25143,9 @@ }, "dependencies": { "diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true }, "has-flag": { @@ -25504,22 +25617,21 @@ } }, "terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "dependencies": { "ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "requires": { "fast-deep-equal": "^3.1.3", @@ -25569,9 +25681,9 @@ }, "dependencies": { "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -25657,9 +25769,9 @@ "dev": true }, "to-buffer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", - "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "dev": true, "requires": { "isarray": "^2.0.5", @@ -26404,11 +26516,11 @@ } }, "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "requires": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" } } } @@ -26479,9 +26591,9 @@ }, "dependencies": { "ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "requires": { "fast-deep-equal": "^3.1.3", @@ -26744,9 +26856,9 @@ } }, "workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", "dev": true }, "wrap-ansi": { From 172d9e64f63b26b9b0674cb3eb75851fac4b0862 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:38:03 -0700 Subject: [PATCH 1113/1136] Prevent double/triple activation from two extensions (#25849) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves: https://github.com/microsoft/vscode-python-environments/issues/1325 Cause: The Python extension only backed off if the user explicitly opted into the envs extension (useEnvironmentsExtension: true). But the envs extension activates by default whenever it's installed — it only backs off if the setting is explicitly false. So in this case, (extension installed, setting not touched), both fired. Added shouldEnvExtHandleActivation() — a function that mirrors the envs extension's own logic: "am I installed and not explicitly disabled?" Used it in all 3 places the Python extension triggers terminal activation to bail out when the envs extension will handle it. This covers global, workspace, and folder-level settings. We want exactly one extension activates the terminal, never both, never neither. --- src/client/common/terminal/activator/index.ts | 6 +- src/client/envExt/api.internal.ts | 37 +++++++- src/client/providers/terminalProvider.ts | 4 +- src/client/terminals/activation.ts | 4 + .../common/terminals/activation.unit.test.ts | 6 ++ .../terminals/activator/index.unit.test.ts | 95 ++++++++++++++++++- src/test/providers/terminal.unit.test.ts | 3 + src/test/terminals/activation.unit.test.ts | 16 ++++ 8 files changed, 164 insertions(+), 7 deletions(-) diff --git a/src/client/common/terminal/activator/index.ts b/src/client/common/terminal/activator/index.ts index 24ffb5008364..cde04bdbf10d 100644 --- a/src/client/common/terminal/activator/index.ts +++ b/src/client/common/terminal/activator/index.ts @@ -9,7 +9,7 @@ import { IConfigurationService, IExperimentService } from '../../types'; import { ITerminalActivationHandler, ITerminalActivator, ITerminalHelper, TerminalActivationOptions } from '../types'; import { BaseTerminalActivator } from './base'; import { inTerminalEnvVarExperiment } from '../../experiments/helpers'; -import { useEnvExtension } from '../../../envExt/api.internal'; +import { shouldEnvExtHandleActivation } from '../../../envExt/api.internal'; import { EventName } from '../../../telemetry/constants'; import { sendTelemetryEvent } from '../../../telemetry'; @@ -44,8 +44,8 @@ export class TerminalActivator implements ITerminalActivator { const settings = this.configurationService.getSettings(options?.resource); const activateEnvironment = settings.terminal.activateEnvironment && !inTerminalEnvVarExperiment(this.experimentService); - if (!activateEnvironment || options?.hideFromUser || useEnvExtension()) { - if (useEnvExtension()) { + if (!activateEnvironment || options?.hideFromUser || shouldEnvExtHandleActivation()) { + if (shouldEnvExtHandleActivation()) { sendTelemetryEvent(EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL); } return false; diff --git a/src/client/envExt/api.internal.ts b/src/client/envExt/api.internal.ts index 5acdd5bba8e3..5edfb712072e 100644 --- a/src/client/envExt/api.internal.ts +++ b/src/client/envExt/api.internal.ts @@ -13,12 +13,47 @@ import { DidChangeEnvironmentEventArgs, } from './types'; import { executeCommand } from '../common/vscodeApis/commandApis'; -import { getConfiguration } from '../common/vscodeApis/workspaceApis'; +import { getConfiguration, getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; import { traceError, traceLog } from '../logging'; import { Interpreters } from '../common/utils/localize'; export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; +export function isEnvExtensionInstalled(): boolean { + return !!getExtension(ENVS_EXTENSION_ID); +} + +/** + * Returns true if the Python Environments extension is installed and not explicitly + * disabled by the user. Mirrors the envs extension's own activation logic: it + * deactivates only when `python.useEnvironmentsExtension` is explicitly set to false + * at the global, workspace, or workspace-folder level. + */ +export function shouldEnvExtHandleActivation(): boolean { + if (!isEnvExtensionInstalled()) { + return false; + } + const config = getConfiguration('python'); + const inspection = config.inspect('useEnvironmentsExtension'); + if (inspection?.globalValue === false || inspection?.workspaceValue === false) { + return false; + } + // The envs extension also checks folder-scoped settings in multi-root workspaces. + // Any single folder with the setting set to false causes the envs extension to + // deactivate entirely (window-wide), so we must mirror that here. + const workspaceFolders = getWorkspaceFolders(); + if (workspaceFolders) { + for (const folder of workspaceFolders) { + const folderConfig = getConfiguration('python', folder.uri); + const folderInspection = folderConfig.inspect('useEnvironmentsExtension'); + if (folderInspection?.workspaceFolderValue === false) { + return false; + } + } + } + return true; +} + let _useExt: boolean | undefined; export function useEnvExtension(): boolean { if (_useExt !== undefined) { diff --git a/src/client/providers/terminalProvider.ts b/src/client/providers/terminalProvider.ts index 841f479269ac..f68f151110ec 100644 --- a/src/client/providers/terminalProvider.ts +++ b/src/client/providers/terminalProvider.ts @@ -11,7 +11,7 @@ import { swallowExceptions } from '../common/utils/decorators'; import { IServiceContainer } from '../ioc/types'; import { captureTelemetry, sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; -import { useEnvExtension } from '../envExt/api.internal'; +import { useEnvExtension, shouldEnvExtHandleActivation } from '../envExt/api.internal'; export class TerminalProvider implements Disposable { private disposables: Disposable[] = []; @@ -33,7 +33,7 @@ export class TerminalProvider implements Disposable { currentTerminal && pythonSettings.terminal.activateEnvInCurrentTerminal && !inTerminalEnvVarExperiment(experimentService) && - !useEnvExtension() + !shouldEnvExtHandleActivation() ) { const hideFromUser = 'hideFromUser' in currentTerminal.creationOptions && currentTerminal.creationOptions.hideFromUser; diff --git a/src/client/terminals/activation.ts b/src/client/terminals/activation.ts index 143a2de14e5c..ed26916e3eaa 100644 --- a/src/client/terminals/activation.ts +++ b/src/client/terminals/activation.ts @@ -9,6 +9,7 @@ import { IActiveResourceService, ITerminalManager } from '../common/application/ import { ITerminalActivator } from '../common/terminal/types'; import { IDisposable, IDisposableRegistry } from '../common/types'; import { ITerminalAutoActivation } from './types'; +import { shouldEnvExtHandleActivation } from '../envExt/api.internal'; @injectable() export class TerminalAutoActivation implements ITerminalAutoActivation { @@ -49,6 +50,9 @@ export class TerminalAutoActivation implements ITerminalAutoActivation { if (this.terminalsNotToAutoActivate.has(terminal)) { return; } + if (shouldEnvExtHandleActivation()) { + return; + } if ('hideFromUser' in terminal.creationOptions && terminal.creationOptions.hideFromUser) { return; } diff --git a/src/test/common/terminals/activation.unit.test.ts b/src/test/common/terminals/activation.unit.test.ts index 49ada1c06b11..d87d33ea03e6 100644 --- a/src/test/common/terminals/activation.unit.test.ts +++ b/src/test/common/terminals/activation.unit.test.ts @@ -3,6 +3,7 @@ 'use strict'; import { expect } from 'chai'; +import * as sinon from 'sinon'; import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Terminal, Uri } from 'vscode'; @@ -15,6 +16,7 @@ import { IDisposable } from '../../../client/common/types'; import { TerminalAutoActivation } from '../../../client/terminals/activation'; import { ITerminalAutoActivation } from '../../../client/terminals/types'; import { noop } from '../../core'; +import * as extapi from '../../../client/envExt/api.internal'; suite('Terminal Auto Activation', () => { let activator: ITerminalActivator; @@ -25,6 +27,7 @@ suite('Terminal Auto Activation', () => { let terminal: Terminal; setup(() => { + sinon.stub(extapi, 'shouldEnvExtHandleActivation').returns(false); terminal = ({ dispose: noop, hide: noop, @@ -46,6 +49,9 @@ suite('Terminal Auto Activation', () => { instance(activeResourceService), ); }); + teardown(() => { + sinon.restore(); + }); test('New Terminals should be activated', async () => { type EventHandler = (e: Terminal) => void; diff --git a/src/test/common/terminals/activator/index.unit.test.ts b/src/test/common/terminals/activator/index.unit.test.ts index 6a50901bc99d..34d1cf8f1bcd 100644 --- a/src/test/common/terminals/activator/index.unit.test.ts +++ b/src/test/common/terminals/activator/index.unit.test.ts @@ -6,7 +6,7 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import { Terminal } from 'vscode'; +import { Terminal, Uri } from 'vscode'; import { TerminalActivator } from '../../../../client/common/terminal/activator'; import { ITerminalActivationHandler, @@ -20,6 +20,8 @@ import { ITerminalSettings, } from '../../../../client/common/types'; import * as extapi from '../../../../client/envExt/api.internal'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import * as extensionsApi from '../../../../client/common/vscodeApis/extensionsApi'; suite('Terminal Activator', () => { let activator: TerminalActivator; @@ -29,9 +31,12 @@ suite('Terminal Activator', () => { let terminalSettings: TypeMoq.IMock; let experimentService: TypeMoq.IMock; let useEnvExtensionStub: sinon.SinonStub; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; setup(() => { useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); useEnvExtensionStub.returns(false); + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); baseActivator = TypeMoq.Mock.ofType(); terminalSettings = TypeMoq.Mock.ofType(); @@ -113,4 +118,92 @@ suite('Terminal Activator', () => { test('Terminal is not activated if auto-activate setting is set to true but terminal is hidden', () => testActivationAndHandlers(false, true, true)); test('Terminal is not activated and handlers are invoked', () => testActivationAndHandlers(false, false)); + + test('Terminal is not activated from Python extension when Env extension should handle activation', async () => { + shouldEnvExtHandleActivationStub.returns(true); + terminalSettings.setup((b) => b.activateEnvironment).returns(() => true); + baseActivator + .setup((b) => b.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.never()); + + const terminal = TypeMoq.Mock.ofType(); + const activated = await activator.activateEnvironmentInTerminal(terminal.object, { + preserveFocus: true, + }); + + assert.strictEqual(activated, false); + baseActivator.verifyAll(); + }); +}); + +suite('shouldEnvExtHandleActivation', () => { + let getExtensionStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + + setup(() => { + getExtensionStub = sinon.stub(extensionsApi, 'getExtension'); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getWorkspaceFoldersStub.returns(undefined); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Returns false when envs extension is not installed', () => { + getExtensionStub.returns(undefined); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false); + }); + + test('Returns true when envs extension is installed and setting is not explicitly set', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + getConfigurationStub.returns({ + inspect: () => ({ globalValue: undefined, workspaceValue: undefined }), + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), true); + }); + + test('Returns false when envs extension is installed but globalValue is false', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + getConfigurationStub.returns({ + inspect: () => ({ globalValue: false, workspaceValue: undefined }), + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false); + }); + + test('Returns false when envs extension is installed but workspaceValue is false', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + getConfigurationStub.returns({ + inspect: () => ({ globalValue: undefined, workspaceValue: false }), + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false); + }); + + test('Returns true when envs extension is installed and setting is explicitly true', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + getConfigurationStub.returns({ + inspect: () => ({ globalValue: true, workspaceValue: undefined }), + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), true); + }); + + test('Returns false when a workspace folder has workspaceFolderValue set to false', () => { + getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID }); + const folderUri = Uri.parse('file:///workspace/folder1'); + getWorkspaceFoldersStub.returns([{ uri: folderUri, name: 'folder1', index: 0 }]); + getConfigurationStub.callsFake((_section: string, scope?: Uri) => { + if (scope) { + return { + inspect: () => ({ workspaceFolderValue: false }), + }; + } + return { + inspect: () => ({ globalValue: undefined, workspaceValue: undefined }), + }; + }); + assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false); + }); }); diff --git a/src/test/providers/terminal.unit.test.ts b/src/test/providers/terminal.unit.test.ts index ac39ded922c8..8f684835b7cf 100644 --- a/src/test/providers/terminal.unit.test.ts +++ b/src/test/providers/terminal.unit.test.ts @@ -29,10 +29,13 @@ suite('Terminal Provider', () => { let experimentService: TypeMoq.IMock; let terminalProvider: TerminalProvider; let useEnvExtensionStub: sinon.SinonStub; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; const resource = Uri.parse('a'); setup(() => { useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); useEnvExtensionStub.returns(false); + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); serviceContainer = TypeMoq.Mock.ofType(); commandManager = TypeMoq.Mock.ofType(); diff --git a/src/test/terminals/activation.unit.test.ts b/src/test/terminals/activation.unit.test.ts index dea0c891229d..4c5294a82f49 100644 --- a/src/test/terminals/activation.unit.test.ts +++ b/src/test/terminals/activation.unit.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as sinon from 'sinon'; import { EventEmitter, Terminal } from 'vscode'; import { ActiveResourceService } from '../../client/common/application/activeResource'; import { TerminalManager } from '../../client/common/application/terminalManager'; @@ -11,6 +12,7 @@ import { ITerminalActivator } from '../../client/common/terminal/types'; import { TerminalAutoActivation } from '../../client/terminals/activation'; import { ITerminalAutoActivation } from '../../client/terminals/types'; import { noop } from '../core'; +import * as extapi from '../../client/envExt/api.internal'; suite('Terminal', () => { suite('Terminal Auto Activation', () => { @@ -21,8 +23,12 @@ suite('Terminal', () => { let onDidOpenTerminalEventEmitter: EventEmitter; let terminal: Terminal; let nonActivatedTerminal: Terminal; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; setup(() => { + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); + manager = mock(TerminalManager); activator = mock(TerminalActivator); resourceService = mock(ActiveResourceService); @@ -60,6 +66,9 @@ suite('Terminal', () => { autoActivation.register(); }); // teardown(() => fakeTimer.uninstall()); + teardown(() => { + sinon.restore(); + }); test('Should activate terminal', async () => { // Trigger opening a terminal. @@ -77,5 +86,12 @@ suite('Terminal', () => { // The terminal should get activated. verify(activator.activateEnvironmentInTerminal(anything(), anything())).never(); }); + test('Should not activate terminal when envs extension should handle activation', async () => { + shouldEnvExtHandleActivationStub.returns(true); + + await ((onDidOpenTerminalEventEmitter.fire(terminal) as unknown) as Promise); + + verify(activator.activateEnvironmentInTerminal(anything(), anything())).never(); + }); }); }); From 63bb68e86b26dfca9fe53901ed61327c90653fdd Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:40:49 +0000 Subject: [PATCH 1114/1136] Bump PET version to 2026.4 in stable release pipeline (#25847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the Python Environment Tools (PET) artifact source branch in the stable release pipeline. - Changed `branchName` from `refs/heads/release/2026.0` to `refs/heads/release/2026.4` in `build/azure-pipeline.stable.yml` --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- build/azure-pipeline.stable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index 237ba08dbc99..cd66613eec8d 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -128,7 +128,7 @@ extends: project: 'Monaco' definition: 593 buildVersionToDownload: 'latestFromBranch' - branchName: 'refs/heads/release/2026.0' + branchName: 'refs/heads/release/2026.4' targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' artifactName: 'bin-$(buildTarget)' itemPattern: | From 7b8fad023f2d831ad58d6253af6be84c95d6d95b Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:01:25 -0700 Subject: [PATCH 1115/1136] bump to release v 2026.4.0 (#25852) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e015ad836067..a94a66bf6e3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2026.2.0", + "version": "2026.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2026.2.0", + "version": "2026.4.0", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/package.json b/package.json index 847926dccc64..b702c5876c08 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2026.2.0", + "version": "2026.4.0", "featureFlags": { "usingNewInterpreterStorage": true }, From db312e294e0d14459a0480c80dc86942b2c681c1 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:18:35 -0700 Subject: [PATCH 1116/1136] bump to v2026.5.0-dev (#25853) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a94a66bf6e3c..f86f2ba09aa2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python", - "version": "2026.4.0", + "version": "2026.5.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python", - "version": "2026.4.0", + "version": "2026.5.0-dev", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/package.json b/package.json index b702c5876c08..2a685fbc158e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Python language support with extension access points for IntelliSense (Pylance), Debugging (Python Debugger), linting, formatting, refactoring, unit tests, and more.", - "version": "2026.4.0", + "version": "2026.5.0-dev", "featureFlags": { "usingNewInterpreterStorage": true }, From 4cea6235e2f19da3c6aa42f0008243dfb6dd4974 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 07:52:33 -0700 Subject: [PATCH 1117/1136] Bump mheap/github-action-required-labels from 5.5.1 to 5.5.2 (#25864) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mheap/github-action-required-labels](https://github.com/mheap/github-action-required-labels) from 5.5.1 to 5.5.2.
Release notes

Sourced from mheap/github-action-required-labels's releases.

v5.5.2

What's Changed

Full Changelog: https://github.com/mheap/github-action-required-labels/compare/v5.5.1...v5.5.2

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=mheap/github-action-required-labels&package-manager=github_actions&previous-version=5.5.1&new-version=5.5.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 04f87a6d49ba..af24ac10772c 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -17,7 +17,7 @@ jobs: pull-requests: write steps: - name: 'PR impact specified' - uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5.5.1 + uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5.5.2 with: mode: exactly count: 1 From 29d3b6fefc6ed84b615d33a5ac5e3bf4ea6405f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:36:59 +0000 Subject: [PATCH 1118/1136] Bump flatted from 3.2.4 to 3.4.2 (#25874) Bumps [flatted](https://github.com/WebReflection/flatted) from 3.2.4 to 3.4.2.
Commits
  • 3bf0909 3.4.2
  • 885ddcc fix CWE-1321
  • 0bdba70 added flatted-view to the benchmark
  • 2a02dce 3.4.1
  • fba4e8f Merge pull request #89 from WebReflection/python-fix
  • 5fe8648 added "when in Rome" also a test for PHP
  • 53517ad some minor improvement
  • b3e2a0c Fixing recursion issue in Python too
  • c4b46db Add SECURITY.md for security policy and reporting
  • f86d071 Create dependabot.yml for version updates
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=flatted&package-manager=npm_and_yarn&previous-version=3.2.4&new-version=3.4.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index f86f2ba09aa2..9dbea4e9e328 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6993,9 +6993,9 @@ } }, "node_modules/flatted": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", - "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, "node_modules/flush-write-stream": { @@ -20783,9 +20783,9 @@ } }, "flatted": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", - "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, "flush-write-stream": { From 5c2c3948e1c8c8a1dfe848104773477e70d0b83b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 08:13:05 -0700 Subject: [PATCH 1119/1136] Remove debug print statements from unittest adapter output (#25854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `PROJECT_ROOT_PATH is set, using /... as cwd for execution payload` is printed to stdout during unittest discovery and execution, leaking into the Test Results panel. - Remove `print()` in `python_files/unittestadapter/execution.py` (execution path) - Remove `print()` in `python_files/unittestadapter/discovery.py` (discovery path) All functional logic (reading the env var, setting `top_level_dir`, updating the global) is unchanged. --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- python_files/unittestadapter/discovery.py | 3 --- python_files/unittestadapter/execution.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/python_files/unittestadapter/discovery.py b/python_files/unittestadapter/discovery.py index b3086d92b102..c864ac76916b 100644 --- a/python_files/unittestadapter/discovery.py +++ b/python_files/unittestadapter/discovery.py @@ -146,9 +146,6 @@ def discover_tests( project_root_path = os.environ.get("PROJECT_ROOT_PATH") if project_root_path: top_level_dir = project_root_path - print( - f"PROJECT_ROOT_PATH is set, using {project_root_path} as top_level_dir for discovery" - ) # Perform regular unittest test discovery. # Pass project_root_path so the payload's cwd matches the project root. diff --git a/python_files/unittestadapter/execution.py b/python_files/unittestadapter/execution.py index e031138b6f75..422f246d3476 100644 --- a/python_files/unittestadapter/execution.py +++ b/python_files/unittestadapter/execution.py @@ -373,9 +373,6 @@ def send_run_data(raw_data, test_run_pipe): # Update the module-level variable for send_run_data to use # pylint: disable=global-statement globals()["PROJECT_ROOT_PATH"] = project_root_path - print( - f"PROJECT_ROOT_PATH is set, using {project_root_path} as cwd for execution payload" - ) # Perform regular unittest execution. # Pass project_root_path so the payload's cwd matches the project root. From be7a8e45bc6e7eb221b4ec7dbe7cdd13558e37d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:45:29 -0700 Subject: [PATCH 1120/1136] Bump picomatch from 2.3.1 to 2.3.2 (#25883) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
Release notes

Sourced from picomatch's releases.

2.3.2

This is a security release fixing several security relevant issues.

What's Changed

Full Changelog: https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2

Changelog

Sourced from picomatch's changelog.

Release history

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

  • Changelogs are for humans, not machines.
  • There should be an entry for every single version.
  • The same types of changes should be grouped.
  • Versions and sections should be linkable.
  • The latest version comes first.
  • The release date of each versions is displayed.
  • Mention whether you follow Semantic Versioning.

Changelog entries are classified using the following labels (from keep-a-changelog):

  • Added for new features.
  • Changed for changes in existing functionality.
  • Deprecated for soon-to-be removed features.
  • Removed for now removed features.
  • Fixed for any bug fixes.
  • Security in case of vulnerabilities.

4.0.0 (2024-02-07)

Fixes

Changed

3.0.1

Fixes

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=picomatch&package-manager=npm_and_yarn&previous-version=2.3.1&new-version=2.3.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9dbea4e9e328..722f895ccec5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11705,9 +11705,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "engines": { "node": ">=8.6" @@ -24289,9 +24289,9 @@ "dev": true }, "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true }, "pify": { From 9ded8032f6a455289113026ed1dca4c5ed81e6e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:42:09 -0700 Subject: [PATCH 1121/1136] Bump tomli from 2.4.0 to 2.4.1 (#25884) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tomli](https://github.com/hukkin/tomli) from 2.4.0 to 2.4.1.
Changelog

Sourced from tomli's changelog.

2.4.1

  • Fixed
    • Limit number of parts of a TOML key to address quadratic time complexity
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tomli&package-manager=pip&previous-version=2.4.0&new-version=2.4.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 96 ++++++++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/requirements.txt b/requirements.txt index 68850210d58c..e0d5bc987a7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,54 +12,54 @@ packaging==26.0 \ --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 # via -r requirements.in -tomli==2.4.0 \ - --hash=sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729 \ - --hash=sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b \ - --hash=sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d \ - --hash=sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df \ - --hash=sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576 \ - --hash=sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d \ - --hash=sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1 \ - --hash=sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a \ - --hash=sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e \ - --hash=sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc \ - --hash=sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702 \ - --hash=sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6 \ - --hash=sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd \ - --hash=sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4 \ - --hash=sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776 \ - --hash=sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a \ - --hash=sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66 \ - --hash=sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87 \ - --hash=sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2 \ - --hash=sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f \ - --hash=sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475 \ - --hash=sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f \ - --hash=sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95 \ - --hash=sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9 \ - --hash=sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3 \ - --hash=sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9 \ - --hash=sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76 \ - --hash=sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da \ - --hash=sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8 \ - --hash=sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51 \ - --hash=sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86 \ - --hash=sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8 \ - --hash=sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0 \ - --hash=sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b \ - --hash=sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1 \ - --hash=sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e \ - --hash=sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d \ - --hash=sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c \ - --hash=sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867 \ - --hash=sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a \ - --hash=sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c \ - --hash=sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0 \ - --hash=sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4 \ - --hash=sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614 \ - --hash=sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132 \ - --hash=sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa \ - --hash=sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087 +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via -r requirements.in typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ From 8efca1f8b15a1408f2ee70e6c626e6ef78ea2475 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:57:14 -0700 Subject: [PATCH 1122/1136] Bump importlib-metadata from 8.7.1 to 9.0.0 (#25878) Bumps [importlib-metadata](https://github.com/python/importlib_metadata) from 8.7.1 to 9.0.0.
Changelog

Sourced from importlib-metadata's changelog.

v9.0.0

Deprecations and Removals

  • Added MetadataNotFound (subclass of FileNotFoundError) and updated Distribution.metadata/metadata() to raise it when the metadata files are missing instead of returning Nonepython/cpython#143387#532)

v8.9.0

Features

v8.8.0

Features

  • Removed Python 3.9 compatibility.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=importlib-metadata&package-manager=pip&previous-version=8.7.1&new-version=9.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --- .github/actions/build-vsix/action.yml | 4 ++-- .github/workflows/build.yml | 2 +- .github/workflows/pr-check.yml | 2 +- requirements.txt | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml index 40d8d73cb4c6..912ff2c34a74 100644 --- a/.github/actions/build-vsix/action.yml +++ b/.github/actions/build-vsix/action.yml @@ -31,10 +31,10 @@ runs: uses: dtolnay/rust-toolchain@stable # Jedi LS depends on dataclasses which is not in the stdlib in Python 3.7. - - name: Use Python 3.9 for JediLSP + - name: Use Python 3.10 for JediLSP uses: actions/setup-python@v6 with: - python-version: 3.9 + python-version: '3.10' cache: 'pip' cache-dependency-path: | requirements.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 42447b873b3b..09d019dec4a7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -174,7 +174,7 @@ jobs: # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. os: [ubuntu-latest, windows-latest] # Run the tests on the oldest and most recent versions of Python. - python: ['3.9', '3.x', '3.13'] + python: ['3.10', '3.x', '3.13'] steps: - name: Checkout diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 2a0bbb598cb0..c8a6f2dd416e 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -157,7 +157,7 @@ jobs: # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. os: [ubuntu-latest, windows-latest] # Run the tests on the oldest and most recent versions of Python. - python: ['3.9', '3.x', '3.13'] # run for 3 pytest versions, most recent stable, oldest version supported and pre-release + python: ['3.10', '3.x', '3.13'] # run for 3 pytest versions, most recent stable, oldest version supported and pre-release pytest-version: ['pytest', 'pytest@pre-release', 'pytest==6.2.0'] steps: diff --git a/requirements.txt b/requirements.txt index e0d5bc987a7c..c6f63fa1b42a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ # This file was autogenerated by uv via the following command: # uv pip compile --generate-hashes requirements.in -o requirements.txt -importlib-metadata==8.7.1 \ - --hash=sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb \ - --hash=sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151 +importlib-metadata==9.0.0 \ + --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \ + --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc # via -r requirements.in microvenv==2025.0 \ --hash=sha256:568155ec18af01c89f270d35d123ab803b09672b480c3702d15fd69e9cc5bd1e \ From 83615a7ed627e5b7f8d7e91f501ec6b63b2ffabc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:21:02 +0000 Subject: [PATCH 1123/1136] Bump brace-expansion (#25894) Bumps and [brace-expansion](https://github.com/juliangruber/brace-expansion). These dependencies needed to be updated together. Updates `brace-expansion` from 1.1.12 to 1.1.13
Commits

Updates `brace-expansion` from 2.0.2 to 2.0.3
Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 65 ++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index 722f895ccec5..9b6baf764738 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2201,11 +2201,10 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -3559,10 +3558,9 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -10064,10 +10062,9 @@ } }, "node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dependencies": { "balanced-match": "^1.0.0" } @@ -10200,11 +10197,10 @@ "dev": true }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -14701,10 +14697,9 @@ } }, "node_modules/vscode-languageclient/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dependencies": { "balanced-match": "^1.0.0" } @@ -17197,9 +17192,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -18223,9 +18218,9 @@ "dev": true }, "brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -23085,9 +23080,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "requires": { "balanced-match": "^1.0.0" } @@ -23158,9 +23153,9 @@ "dev": true }, "brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -26508,9 +26503,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "requires": { "balanced-match": "^1.0.0" } From 223dbaf694ef7a5711ca04dbe5b737d73ab6e3f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:18:01 -0700 Subject: [PATCH 1124/1136] Bump actions/github-script from 8 to 9 (#25906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
Release notes

Sourced from actions/github-script's releases.

v9.0.0

New features:

  • getOctokit factory function — Available directly in the script context. Create additional authenticated Octokit clients with different tokens for multi-token workflows, GitHub App tokens, and cross-org access. See Creating additional clients with getOctokit for details and examples.
  • Orchestration ID in user-agent — The ACTIONS_ORCHESTRATION_ID environment variable is automatically appended to the user-agent string for request tracing.

Breaking changes:

  • require('@actions/github') no longer works in scripts. The upgrade to @actions/github v9 (ESM-only) means require('@actions/github') will fail at runtime. If you previously used patterns like const { getOctokit } = require('@actions/github') to create secondary clients, use the new injected getOctokit function instead — it's available directly in the script context with no imports needed.
  • getOctokit is now an injected function parameter. Scripts that declare const getOctokit = ... or let getOctokit = ... will get a SyntaxError because JavaScript does not allow const/let redeclaration of function parameters. Use the injected getOctokit directly, or use var getOctokit = ... if you need to redeclare it.
  • If your script accesses other @actions/github internals beyond the standard github/octokit client, you may need to update those references for v9 compatibility.

What's Changed

New Contributors

Full Changelog: https://github.com/actions/github-script/compare/v8.0.0...v9.0.0

Commits
  • 3a2844b Merge pull request #700 from actions/salmanmkc/expose-getoctokit + prepare re...
  • ca10bbd fix: use @​octokit/core/types import for v7 compatibility
  • 86e48e2 merge: incorporate main branch changes
  • c108472 chore: rebuild dist for v9 upgrade and getOctokit factory
  • afff112 Merge pull request #712 from actions/salmanmkc/deployment-false + fix user-ag...
  • ff8117e ci: fix user-agent test to handle orchestration ID
  • 81c6b78 ci: use deployment: false to suppress deployment noise from integration tests
  • 3953caf docs: update README examples from @​v8 to @​v9, add getOctokit docs and v9 brea...
  • c17d55b ci: add getOctokit integration test job
  • a047196 test: add getOctokit integration tests via callAsyncFunction
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/github-script&package-manager=github_actions&previous-version=8&new-version=9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-issue-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-issue-check.yml b/.github/workflows/pr-issue-check.yml index 25ac91bbd279..5587227d2848 100644 --- a/.github/workflows/pr-issue-check.yml +++ b/.github/workflows/pr-issue-check.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Ensure PR has an associated issue' - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const labels = context.payload.pull_request.labels.map(label => label.name); From d5d4e5ed68c22fe52cb2af782e124d274ff56514 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:18:59 -0700 Subject: [PATCH 1125/1136] Bump lodash from 4.17.23 to 4.18.1 (#25905) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1.
Release notes

Sourced from lodash's releases.

4.18.1

Bugs

Fixes a ReferenceError issue in lodash lodash-es lodash-amd and lodash.template when using the template and fromPairs functions from the modular builds. See lodash/lodash#6167

These defects were related to how lodash distributions are built from the main branch using https://github.com/lodash-archive/lodash-cli. When internal dependencies change inside lodash functions, equivalent updates need to be made to a mapping in the lodash-cli. (hey, it was ahead of its time once upon a time!). We know this, but we missed it in the last release. It's the kind of thing that passes in CI, but fails bc the build is not the same thing you tested.

There is no diff on main for this, but you can see the diffs for each of the npm packages on their respective branches:

4.18.0

v4.18.0

Full Changelog: https://github.com/lodash/lodash/compare/4.17.23...4.18.0

Security

_.unset / _.omit: Fixed prototype pollution via constructor/prototype path traversal (GHSA-f23m-r3pf-42rh, fe8d32e). Previously, array-wrapped path segments and primitive roots could bypass the existing guards, allowing deletion of properties from built-in prototypes. Now constructor and prototype are blocked unconditionally as non-terminal path keys, matching baseSet. Calls that previously returned true and deleted the property now return false and leave the target untouched.

_.template: Fixed code injection via imports keys (GHSA-r5fr-rjxr-66jc, CVE-2026-4800, 879aaa9). Fixes an incomplete patch for CVE-2021-23337. The variable option was validated against reForbiddenIdentifierChars but importsKeys was left unguarded, allowing code injection via the same Function() constructor sink. imports keys containing forbidden identifier characters now throw "Invalid imports option passed into _.template".

Docs

  • Add security notice for _.template in threat model and API docs (#6099)
  • Document lower > upper behavior in _.random (#6115)
  • Fix quotes in _.compact jsdoc (#6090)

lodash.* modular packages

Diff

We have also regenerated and published a select number of the lodash.* modular packages.

These modular packages had fallen out of sync significantly from the minor/patch updates to lodash. Specifically, we have brought the following packages up to parity w/ the latest lodash release because they have had CVEs on them in the past:

Commits
  • cb0b9b9 release(patch): bump main to 4.18.1 (#6177)
  • 75535f5 chore: prune stale advisory refs (#6170)
  • 62e91bc docs: remove n_ Node.js < 6 REPL note from README (#6165)
  • 59be2de release(minor): bump to 4.18.0 (#6161)
  • af63457 fix: broken tests for _.template 879aaa9
  • 1073a76 fix: linting issues
  • 879aaa9 fix: validate imports keys in _.template
  • fe8d32e fix: block prototype pollution in baseUnset via constructor/prototype traversal
  • 18ba0a3 refactor(fromPairs): use baseAssignValue for consistent assignment (#6153)
  • b819080 ci: add dist sync validation workflow (#6137)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=lodash&package-manager=npm_and_yarn&previous-version=4.17.23&new-version=4.18.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9b6baf764738..6de6edae81c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "iconv-lite": "^0.6.3", "inversify": "^6.0.2", "jsonc-parser": "^3.0.0", - "lodash": "^4.17.23", + "lodash": "^4.18.1", "minimatch": "^5.1.8", "named-js-regexp": "^1.3.3", "node-stream-zip": "^1.6.0", @@ -9658,9 +9658,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, "node_modules/lodash.flattendeep": { "version": "4.4.0", @@ -22757,9 +22757,9 @@ } }, "lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, "lodash.flattendeep": { "version": "4.4.0", diff --git a/package.json b/package.json index 2a685fbc158e..2a27cddc0976 100644 --- a/package.json +++ b/package.json @@ -1713,7 +1713,7 @@ "iconv-lite": "^0.6.3", "inversify": "^6.0.2", "jsonc-parser": "^3.0.0", - "lodash": "^4.17.23", + "lodash": "^4.18.1", "minimatch": "^5.1.8", "named-js-regexp": "^1.3.3", "node-stream-zip": "^1.6.0", From 6bf57bdd6d02b293b7c671c34e8e9a35a6aa6e0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 08:21:12 -0700 Subject: [PATCH 1126/1136] Bump packaging from 26.0 to 26.1 (#25912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [packaging](https://github.com/pypa/packaging) from 26.0 to 26.1.
Release notes

Sourced from packaging's releases.

26.1

Features:

Behavior adaptations:

Pylock (PEP 751) updates:

Fixes:

Performance:

... (truncated)

Changelog

Sourced from packaging's changelog.

26.1 - 2026-04-14


Features:
  • PEP 783: add handling for Emscripten wheel tags in (:pull:804)
  • PEP 803: add handling for the abi3.abi3t free-threading tag in (:pull:1099)
  • PEP 723: add packaging.dependency_groups module, based on the dependency-groups package in (:pull:1065)
  • Add the packaging.direct_url module in (:pull:944)
  • Add the packaging.errors module in (:pull:1071)
  • Add SpecifierSet.is_unsatisfiable using ranges (new internals that will be expanded in future versions) in (:pull:1119)
  • Add create_compatible_tags_selector to select compatible tags in (:pull:1110)
  • Add a key argument to SpecifierSet.filter() in (:pull:1068)
  • Support &amp; and | for Marker's in (:pull:1146)
  • Normalize Version.__replace__ and add Version.from_parts in (:pull:1078)
  • Add an option to validate compressed tag set sort order in parse_wheel_filename in (:pull:1150)

Behavior adaptations:

  • Narrow exclusion of pre-releases for &lt;V.postN to match spec in (:pull:1140)
  • Narrow exclusion of post-releases for &gt;V to match spec in (:pull:1141)
  • Rename format_full_version to _format_full_version to make it visibly private in (:pull:1125)
  • Restrict local version to ASCII in (:pull:1102)

Pylock (PEP 751) updates:

  • Add pylock select function in (:pull:1092)
  • Document pylock select() method and PylockSelectError in (:pull:1153)
  • Add filename property to PackageSdist and PackageWheel, more validation in (:pull:1095)
  • Give preference to path over url in (:pull:1128)
  • Validate name/version consistency in file names in (:pull:1114)

Fixes:

  • Fix &gt; comparison for versions with dev+local segments in (:pull:1097)
  • Fix incorrect self-comparison for InfinityType and NegativeInfinityType in (:pull:1093)
  • Canonicalize when deduplicating specifiers in SpecifierSet in (:pull:1109)
  • Fix charset error message formatting in (:pull:1121)
  • Handle the key parameter in SpecifierSet.filter when specifiers are empty and prerelease is False in (:pull:1096)
  • Standardize inner components of repr output in (:pull:1090)
  • Specifier's === uses original string, not normalized, when available in (:pull:1124)
  • Propagate int-max-str-digits ValueError in (:pull:1155)

Performance:

  • Add fast path for parsing simple versions (digits and dots only) in (:pull:1082)
  • Add fast path for Version to Version comparison by skipping _key property in (:pull:1083)
  • Cache Version hash value in dedicated slot in (:pull:1118)
  • Overhaul _cmpkey to remove use of custom objects in (:pull:1116)
  • Skip __replace__ in Specifier comparison if not needed in (:pull:1081)
    </tr></table>

... (truncated)

Commits
  • c1a88a3 Bump for release
  • 702c25e docs: update changelog for 26.1 (#1156)
  • 3f4f5d4 Implement is_unsatisfiable on SpecifierSet using ranges (#1119)
  • 06c6555 Propagate int-max-str-digits ValueError (#1155)
  • 905c90c feat: option to validate compressed tag set sort order in `parse_wheel_filena...
  • af0026c docs(pylock): document select() method and PylockSelectError (#1153)
  • 668da86 Rename format_full_version to _format_full_version to make it visibly private...
  • f294d52 tests: do not reload the tags module (#1152)
  • 2c6c7df feat: add handling for Emscripten wheels tags per PEP 783 (#804)
  • 6762eea docs(markers): document & and | operators for combining Marker objects (#1151)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=packaging&package-manager=pip&previous-version=26.0&new-version=26.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index c6f63fa1b42a..5d325361bd4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,9 @@ microvenv==2025.0 \ --hash=sha256:568155ec18af01c89f270d35d123ab803b09672b480c3702d15fd69e9cc5bd1e \ --hash=sha256:8a2568a8390a4ffb5af2f05e7642454e03b887e582d192b6316326974eab5d0f # via -r requirements.in -packaging==26.0 \ - --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ - --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 +packaging==26.1 \ + --hash=sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f \ + --hash=sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de # via -r requirements.in tomli==2.4.1 \ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ From 437afb55c435fee1a62c760eb699855a3341847d Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:20:06 -0700 Subject: [PATCH 1127/1136] Refactor environment extension activation checks to use shouldEnvExtHandleActivation function (#25895) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem Fixes #1284 When the Python Environments extension (ms-python.vscode-python-envs) is installed but python.useEnvironmentsExtension is not explicitly set, users see two interpreter status bar items — one from each extension. Root Cause The interpreter display in InterpreterDisplay used useEnvExtension() to decide whether to create/show its status bar. That function requires python.useEnvironmentsExtension to be explicitly true (defaults to false). Meanwhile, the envs extension activates whenever the setting is not explicitly false — treating unset/undefined as enabled. --- src/client/interpreter/display/index.ts | 6 +++--- src/test/interpreters/display.unit.test.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index ebe7fc359cac..3a602093d4f9 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -24,7 +24,7 @@ import { IInterpreterService, IInterpreterStatusbarVisibilityFilter, } from '../contracts'; -import { useEnvExtension } from '../../envExt/api.internal'; +import { shouldEnvExtHandleActivation } from '../../envExt/api.internal'; /** * Based on https://github.com/microsoft/vscode-python/issues/18040#issuecomment-992567670. @@ -68,7 +68,7 @@ export class InterpreterDisplay implements IInterpreterDisplay, IExtensionSingle } public async activate(): Promise { - if (useEnvExtension()) { + if (shouldEnvExtHandleActivation()) { return; } const application = this.serviceContainer.get(IApplicationShell); @@ -115,7 +115,7 @@ export class InterpreterDisplay implements IInterpreterDisplay, IExtensionSingle } } private async updateDisplay(workspaceFolder?: Uri) { - if (useEnvExtension()) { + if (shouldEnvExtHandleActivation()) { this.statusBar?.hide(); this.languageStatus?.dispose(); this.languageStatus = undefined; diff --git a/src/test/interpreters/display.unit.test.ts b/src/test/interpreters/display.unit.test.ts index 7d53fbfc0561..d9be806ff709 100644 --- a/src/test/interpreters/display.unit.test.ts +++ b/src/test/interpreters/display.unit.test.ts @@ -59,7 +59,7 @@ suite('Interpreters Display', () => { let pathUtils: TypeMoq.IMock; let languageStatusItem: TypeMoq.IMock; let traceLogStub: sinon.SinonStub; - let useEnvExtensionStub: sinon.SinonStub; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; async function createInterpreterDisplay(filters: IInterpreterStatusbarVisibilityFilter[] = []) { interpreterDisplay = new InterpreterDisplay(serviceContainer.object); try { @@ -69,8 +69,8 @@ suite('Interpreters Display', () => { } async function setupMocks(useLanguageStatus: boolean) { - useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); - useEnvExtensionStub.returns(false); + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); serviceContainer = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); From f3fab838edbf5e0b1c82374b377845d2fa1a4d57 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 23 Apr 2026 09:06:25 -0700 Subject: [PATCH 1128/1136] Remove unused testresources import and related load_tests function from unittestadapter (#25928) goal to resolve CI issues Keeps coverage of the testscenarios-based dynamic test-ID pattern (which was the whole point of this fixture). Declaring testtools explicitly replaces the fragile transitive dependency that was causing the CI import failure described in test-scenarios-ci-failure.md. --------- Co-authored-by: Copilot --- build/test-requirements.txt | 2 +- .../.data/test_scenarios/tests/__init__.py | 13 ------------- .../.data/test_scenarios/tests/test_scene.py | 18 ++++++++++++++++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/build/test-requirements.txt b/build/test-requirements.txt index 6d64ff72ac7f..ff9afdfc8a2e 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -12,8 +12,8 @@ flask fastapi uvicorn django -testresources testscenarios +testtools # Integrated TensorBoard tests tensorboard diff --git a/python_files/tests/unittestadapter/.data/test_scenarios/tests/__init__.py b/python_files/tests/unittestadapter/.data/test_scenarios/tests/__init__.py index 6b8fbbc579ab..5b7f7a925cc0 100644 --- a/python_files/tests/unittestadapter/.data/test_scenarios/tests/__init__.py +++ b/python_files/tests/unittestadapter/.data/test_scenarios/tests/__init__.py @@ -1,15 +1,2 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - -import os - -import testresources -from testscenarios import generate_scenarios - -def load_tests(loader, tests, pattern): - this_dir = os.path.dirname(__file__) - mytests = loader.discover(start_dir=this_dir, pattern=pattern) - result = testresources.OptimisingTestSuite() - result.addTests(generate_scenarios(mytests)) - result.addTests(generate_scenarios(tests)) - return result diff --git a/python_files/tests/unittestadapter/.data/test_scenarios/tests/test_scene.py b/python_files/tests/unittestadapter/.data/test_scenarios/tests/test_scene.py index 1f66cbde4ef7..35c1c7002319 100644 --- a/python_files/tests/unittestadapter/.data/test_scenarios/tests/test_scene.py +++ b/python_files/tests/unittestadapter/.data/test_scenarios/tests/test_scene.py @@ -1,13 +1,27 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from testscenarios import TestWithScenarios +import unittest + +from testscenarios import TestWithScenarios, generate_scenarios + + +def load_tests(loader, standard_tests, pattern): # noqa: ARG001 + # Pre-expand ``TestWithScenarios`` scenarios at load time so individual + # scenario-multiplied test IDs (e.g. ``test_operations(add)``) can be + # resolved by ``unittest.TestLoader.loadTestsFromName``. Without this, + # ``TestWithScenarios`` only multiplies scenarios at ``run()`` time and + # loading a specific scenario by name raises ``AttributeError``. + result = unittest.TestSuite() + result.addTests(generate_scenarios(standard_tests)) + return result + class TestMathOperations(TestWithScenarios): scenarios = [ ('add', {'test_id': 'test_add', 'a': 5, 'b': 3, 'expected': 8}), ('subtract', {'test_id': 'test_subtract', 'a': 5, 'b': 3, 'expected': 2}), - ('multiply', {'test_id': 'test_multiply', 'a': 5, 'b': 3, 'expected': 15}) + ('multiply', {'test_id': 'test_multiply', 'a': 5, 'b': 3, 'expected': 15}), ] a: int = 0 b: int = 0 From 957444f54d0d716f0c86a26d3e57e40c47f02141 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:10:46 -0700 Subject: [PATCH 1129/1136] Update telemetry event name from "invokeTool" to "INVOKE_TOOL" for consistency (#25926) --- src/client/telemetry/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 738c5f8a2776..7fd586215145 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1988,7 +1988,7 @@ export interface IEventNamePropertyMapping { * Telemetry event sent when invoking a Tool */ /* __GDPR__ - "invokeTool" : { + "INVOKE_TOOL" : { "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, "toolName" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, "failed": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"Whether there was a failure. Common to most of the events.", "owner": "donjayamanne" }, From a8a4f70e24531b17b436559cffe7576fad1c8cfe Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:23:26 -0700 Subject: [PATCH 1130/1136] Add telemetry properties for environment resolution and package installation (#25927) Co-authored-by: Copilot 1. Add `duration` to `INVOKE_TOOL` 2. Add `resolveOutcome` to `configure_python_environment` 3. Add `envType` to all tools that resolve an environment 4. Add `packageCount` and `installerType` to `install_python_packages` 5. Add `responsePackageCount` to `get_python_environment_details` --------- Co-authored-by: Copilot --- src/client/chat/baseTool.ts | 7 +++++- src/client/chat/configurePythonEnvTool.ts | 10 ++++++++- src/client/chat/getExecutableTool.ts | 7 ++++++ src/client/chat/getPythonEnvTool.ts | 14 +++++++++++- src/client/chat/installPackagesTool.ts | 4 ++++ src/client/chat/utils.ts | 4 ++++ src/client/telemetry/index.ts | 27 ++++++++++++++++++++++- 7 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/client/chat/baseTool.ts b/src/client/chat/baseTool.ts index 2eedbbe226e3..d8e2e1d60d42 100644 --- a/src/client/chat/baseTool.ts +++ b/src/client/chat/baseTool.ts @@ -16,8 +16,10 @@ import { IResourceReference, isCancellationError, resolveFilePath } from './util import { ErrorWithTelemetrySafeReason } from '../common/errors/errorUtils'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; +import { StopWatch } from '../common/utils/stopWatch'; export abstract class BaseTool implements LanguageModelTool { + protected extraTelemetryProperties: Record = {}; constructor(private readonly toolName: string) {} async invoke( @@ -29,8 +31,10 @@ export abstract class BaseTool implements Language new LanguageModelTextPart('Cannot use this tool in an untrusted workspace.'), ]); } + this.extraTelemetryProperties = {}; let error: Error | undefined; const resource = resolveFilePath(options.input.resourcePath); + const stopWatch = new StopWatch(); try { return await this.invokeImpl(options, resource, token); } catch (ex) { @@ -46,10 +50,11 @@ export abstract class BaseTool implements Language ? error.telemetrySafeReason : 'error' : undefined; - sendTelemetryEvent(EventName.INVOKE_TOOL, undefined, { + sendTelemetryEvent(EventName.INVOKE_TOOL, stopWatch.elapsedTime, { toolName: this.toolName, failed, failureCategory, + ...this.extraTelemetryProperties, }); } } diff --git a/src/client/chat/configurePythonEnvTool.ts b/src/client/chat/configurePythonEnvTool.ts index 0634b9c9ac34..914a92f81c52 100644 --- a/src/client/chat/configurePythonEnvTool.ts +++ b/src/client/chat/configurePythonEnvTool.ts @@ -18,6 +18,7 @@ import { ICodeExecutionService } from '../terminals/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; import { getEnvDetailsForResponse, + getEnvTypeForTelemetry, getToolResponseIfNotebook, IResourceReference, isCancellationError, @@ -58,6 +59,7 @@ export class ConfigurePythonEnvTool extends BaseTool ): Promise { const notebookResponse = getToolResponseIfNotebook(resource); if (notebookResponse) { + this.extraTelemetryProperties.resolveOutcome = 'notebook'; return notebookResponse; } @@ -67,6 +69,8 @@ export class ConfigurePythonEnvTool extends BaseTool ); if (workspaceSpecificEnv) { + this.extraTelemetryProperties.resolveOutcome = 'existingWorkspaceEnv'; + this.extraTelemetryProperties.envType = getEnvTypeForTelemetry(workspaceSpecificEnv); return getEnvDetailsForResponse( workspaceSpecificEnv, this.api, @@ -79,7 +83,9 @@ export class ConfigurePythonEnvTool extends BaseTool if (await this.createEnvTool.shouldCreateNewVirtualEnv(resource, token)) { try { - return await lm.invokeTool(CreateVirtualEnvTool.toolName, options, token); + const result = await lm.invokeTool(CreateVirtualEnvTool.toolName, options, token); + this.extraTelemetryProperties.resolveOutcome = 'createdVirtualEnv'; + return result; } catch (ex) { if (isCancellationError(ex)) { const input: ISelectPythonEnvToolArguments = { @@ -87,6 +93,7 @@ export class ConfigurePythonEnvTool extends BaseTool reason: 'cancelled', }; // If the user cancelled the tool, then we should invoke the select env tool. + this.extraTelemetryProperties.resolveOutcome = 'selectedEnvAfterCancelledCreate'; return lm.invokeTool(SelectPythonEnvTool.toolName, { ...options, input }, token); } throw ex; @@ -95,6 +102,7 @@ export class ConfigurePythonEnvTool extends BaseTool const input: ISelectPythonEnvToolArguments = { ...options.input, }; + this.extraTelemetryProperties.resolveOutcome = 'selectedEnv'; return lm.invokeTool(SelectPythonEnvTool.toolName, { ...options, input }, token); } } diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts index 746a540d14f8..38dabce644a7 100644 --- a/src/client/chat/getExecutableTool.ts +++ b/src/client/chat/getExecutableTool.ts @@ -19,6 +19,7 @@ import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/termin import { getEnvDisplayName, getEnvironmentDetails, + getEnvTypeForTelemetry, getToolResponseIfNotebook, IResourceReference, raceCancellationError, @@ -53,6 +54,12 @@ export class GetExecutableTool extends BaseTool implements L return notebookResponse; } + const envPath = this.api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); + if (environment) { + this.extraTelemetryProperties.envType = getEnvTypeForTelemetry(environment); + } + const message = await getEnvironmentDetails( resourcePath, this.api, diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts index ed1dd0374424..d25d72baeba8 100644 --- a/src/client/chat/getPythonEnvTool.ts +++ b/src/client/chat/getPythonEnvTool.ts @@ -17,7 +17,13 @@ import { IServiceContainer } from '../ioc/types'; import { ICodeExecutionService } from '../terminals/types'; import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; -import { getEnvironmentDetails, getToolResponseIfNotebook, IResourceReference, raceCancellationError } from './utils'; +import { + getEnvironmentDetails, + getEnvTypeForTelemetry, + getToolResponseIfNotebook, + IResourceReference, + raceCancellationError, +} from './utils'; import { getPythonPackagesResponse } from './listPackagesTool'; import { ITerminalHelper } from '../common/terminal/types'; import { getEnvExtApi, useEnvExtension } from '../envExt/api.internal'; @@ -64,13 +70,16 @@ export class GetEnvironmentInfoTool extends BaseTool 'noEnvFound', ); } + this.extraTelemetryProperties.envType = getEnvTypeForTelemetry(environment); let packages = ''; + let responsePackageCount = 0; if (useEnvExtension()) { const api = await getEnvExtApi(); const env = await api.getEnvironment(resourcePath); const pkgs = env ? await api.getPackages(env) : []; if (pkgs && pkgs.length > 0) { + responsePackageCount = pkgs.length; // Installed Python packages, each in the format or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. const response = [ 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', @@ -90,7 +99,10 @@ export class GetEnvironmentInfoTool extends BaseTool resourcePath, token, ); + // Count lines starting with '- ' to get the number of packages + responsePackageCount = (packages.match(/^- /gm) || []).length; } + this.extraTelemetryProperties.responsePackageCount = String(responsePackageCount); const message = await getEnvironmentDetails( resourcePath, this.api, diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts index f7795620cf13..5d3d456361f9 100644 --- a/src/client/chat/installPackagesTool.ts +++ b/src/client/chat/installPackagesTool.ts @@ -16,6 +16,7 @@ import { PythonExtension } from '../api/types'; import { IServiceContainer } from '../ioc/types'; import { getEnvDisplayName, + getEnvTypeForTelemetry, getToolResponseIfNotebook, IResourceReference, isCancellationError, @@ -51,6 +52,7 @@ export class InstallPackagesTool extends BaseTool ): Promise { const packageCount = options.input.packageList.length; const packagePlurality = packageCount === 1 ? 'package' : 'packages'; + this.extraTelemetryProperties.packageCount = String(packageCount); const notebookResponse = getToolResponseIfNotebook(resourcePath); if (notebookResponse) { return notebookResponse; @@ -84,9 +86,11 @@ export class InstallPackagesTool extends BaseTool 'noEnvFound', ); } + this.extraTelemetryProperties.envType = getEnvTypeForTelemetry(environment); const isConda = isCondaEnv(environment); const installers = this.serviceContainer.getAll(IModuleInstaller); const installerType = isConda ? ModuleInstallerType.Conda : ModuleInstallerType.Pip; + this.extraTelemetryProperties.installerType = isConda ? 'conda' : 'pip'; const installer = installers.find((i) => i.type === installerType); if (!installer) { throw new ErrorWithTelemetrySafeReason( diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts index 84df2901341b..2309316bcbdd 100644 --- a/src/client/chat/utils.ts +++ b/src/client/chat/utils.ts @@ -76,6 +76,10 @@ export function isCondaEnv(env: ResolvedEnvironment) { return (env.environment?.type || '').toLowerCase() === 'conda'; } +export function getEnvTypeForTelemetry(env: ResolvedEnvironment): string { + return (env.environment?.type || 'unknown').toLowerCase(); +} + export async function getEnvironmentDetails( resourcePath: Uri | undefined, api: PythonExtension['environments'], diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 7fd586215145..763f7405aa0d 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1992,7 +1992,12 @@ export interface IEventNamePropertyMapping { "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "donjayamanne" }, "toolName" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, "failed": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"Whether there was a failure. Common to most of the events.", "owner": "donjayamanne" }, - "failureCategory": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"A reason that we generate (e.g. kerneldied, noipykernel, etc), more like a category of the error. Common to most of the events.", "owner": "donjayamanne" } + "failureCategory": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"A reason that we generate (e.g. kerneldied, noipykernel, etc), more like a category of the error. Common to most of the events.", "owner": "donjayamanne" }, + "resolveOutcome": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"Which code path resolved the environment in configure_python_environment.", "owner": "donjayamanne" }, + "envType": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"The type of Python environment (e.g. venv, conda, system).", "owner": "donjayamanne" }, + "packageCount": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"Number of packages requested for installation (install_python_packages only).", "owner": "donjayamanne" }, + "installerType": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"Which installer was used: pip or conda (install_python_packages only).", "owner": "donjayamanne" }, + "responsePackageCount": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"Number of packages in the environment response (get_python_environment_details only).", "owner": "donjayamanne" } } */ [EventName.INVOKE_TOOL]: { @@ -2009,6 +2014,26 @@ export interface IEventNamePropertyMapping { * A reason the error was thrown. */ failureCategory?: string; + /** + * Which code path resolved the environment (configure_python_environment only). + */ + resolveOutcome?: string; + /** + * The type of Python environment (e.g. venv, conda, system). + */ + envType?: string; + /** + * Number of packages requested for installation (install_python_packages only). + */ + packageCount?: string; + /** + * Which installer was used: pip or conda (install_python_packages only). + */ + installerType?: string; + /** + * Number of packages in the environment response (get_python_environment_details only). + */ + responsePackageCount?: string; }; /** * Telemetry event sent if and when user configure tests command. This command can be trigerred from multiple places in the extension. (Command palette, prompt etc.) From f255bef264750c859f303390e4396a1617be2f93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:26:07 -0700 Subject: [PATCH 1131/1136] Bump uuid from 8.3.2 to 14.0.0 (#25929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [uuid](https://github.com/uuidjs/uuid) from 8.3.2 to 14.0.0.
Release notes

Sourced from uuid's releases.

v14.0.0

14.0.0 (2026-04-19)

⚠ BREAKING CHANGES

  • expect crypto to be global everywhere (requires node@20+) (#935)
  • drop node@18 support (#934)

Features

Bug Fixes

  • expect crypto to be global everywhere (requires node@20+) (#935) (f2c235f)
  • Use GITHUB_TOKEN for release-please and enable npm provenance (#925) (ffa3138)

v13.0.0

13.0.0 (2025-09-08)

⚠ BREAKING CHANGES

  • make browser exports the default (#901)

Bug Fixes

v12.0.0

12.0.0 (2025-09-05)

⚠ BREAKING CHANGES

  • update to typescript@5.2 (#887)
  • remove CommonJS support (#886)
  • drop node@16 support (#883)

Features

Bug Fixes

... (truncated)

Changelog

Sourced from uuid's changelog.

14.0.0 (2026-04-19)

Security

  • Fixes GHSA-w5hq-g745-h8pq: v3(), v5(), and v6() did not validate that writes would remain within the bounds of a caller-supplied buffer, allowing out-of-bounds writes when an invalid offset was provided. A RangeError is now thrown if offset < 0 or offset + 16 > buf.length.

⚠ BREAKING CHANGES

  • crypto is now expected to be globally defined (requires node@20+) (#935)
  • drop node@18 support (#934)
  • upgrade minimum supported TypeScript version to 5.4.3, in keeping with the project's policy of supporting TypeScript versions released within the last two years

13.0.0 (2025-09-08)

⚠ BREAKING CHANGES

  • make browser exports the default (#901)

Bug Fixes

12.0.0 (2025-09-05)

⚠ BREAKING CHANGES

  • update to typescript@5.2 (#887)
  • remove CommonJS support (#886)
  • drop node@16 support (#883)

Features

Bug Fixes

11.1.0 (2025-02-19)

... (truncated)

Commits
  • 7c1ea08 chore(main): release 14.0.0 (#926)
  • 3d2c5b0 Merge commit from fork
  • f2c235f fix!: expect crypto to be global everywhere (requires node@20+) (#935)
  • 529ef08 chore: upgrade TypeScript and fixup types (#927)
  • 086fd79 chore: update dependencies (#933)
  • dc4ddb8 feat!: drop node@18 support (#934)
  • 0f1f9c9 chore: switch to Biome for parsing and linting (#932)
  • e2879e6 chore: use maintained version of npm-run-all (#930)
  • ffa3138 fix: Use GITHUB_TOKEN for release-please and enable npm provenance (#925)
  • 0423d49 docs: remove obsolete v1 option notes (#915)
  • Additional commits viewable in compare view
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for uuid since your current version.

Install script changes

This version adds prepare script that runs during installation. Review the package contents before updating.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=uuid&package-manager=npm_and_yarn&previous-version=8.3.2&new-version=14.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- package-lock.json | 65 +++++++++++++++++++++++++++++++++++++++++------ package.json | 2 +- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6de6edae81c0..258726777690 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,7 +103,7 @@ "tsconfig-paths-webpack-plugin": "^3.2.0", "typemoq": "^2.1.0", "typescript": "~5.2", - "uuid": "^8.3.2", + "uuid": "^14.0.0", "webpack": "^5.105.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", @@ -285,6 +285,14 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/@azure/core-rest-pipeline/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@azure/core-tracing": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", @@ -434,6 +442,15 @@ "node": ">=0.8.0" } }, + "node_modules/@azure/msal-node/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@azure/opentelemetry-instrumentation-azure-sdk": { "version": "1.0.0-beta.5", "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0-beta.5.tgz", @@ -9144,6 +9161,15 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-processinfo/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", @@ -14470,11 +14496,16 @@ "dev": true }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { @@ -15713,6 +15744,11 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" } } }, @@ -15844,6 +15880,12 @@ "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==", "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true } } }, @@ -22342,6 +22384,12 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true } } }, @@ -26326,9 +26374,10 @@ "dev": true }, "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "dev": true }, "v8-compile-cache-lib": { "version": "3.0.0", diff --git a/package.json b/package.json index 2a27cddc0976..9f689b60ff34 100644 --- a/package.json +++ b/package.json @@ -1799,7 +1799,7 @@ "tsconfig-paths-webpack-plugin": "^3.2.0", "typemoq": "^2.1.0", "typescript": "~5.2", - "uuid": "^8.3.2", + "uuid": "^14.0.0", "webpack": "^5.105.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", From 347b1ac4495da204c65bf938807c96ec90b1c651 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Sun, 26 Apr 2026 13:38:49 -0700 Subject: [PATCH 1132/1136] Update Python version from 3.9 to 3.10 in pipeline and conda environment configurations (#25933) --- build/azure-pipeline.pre-release.yml | 2 +- build/azure-pipeline.stable.yml | 2 +- build/ci/conda_env_1.yml | 2 +- build/ci/conda_env_2.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml index e7159618d3ae..c300039f4ef4 100644 --- a/build/azure-pipeline.pre-release.yml +++ b/build/azure-pipeline.pre-release.yml @@ -71,7 +71,7 @@ extends: - task: UsePythonVersion@0 inputs: - versionSpec: '3.9' + versionSpec: '3.10' addToPath: true architecture: 'x64' displayName: Select Python version diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml index cd66613eec8d..024417da0e00 100644 --- a/build/azure-pipeline.stable.yml +++ b/build/azure-pipeline.stable.yml @@ -65,7 +65,7 @@ extends: - task: UsePythonVersion@0 inputs: - versionSpec: '3.9' + versionSpec: '3.10' addToPath: true architecture: 'x64' displayName: Select Python version diff --git a/build/ci/conda_env_1.yml b/build/ci/conda_env_1.yml index 4f9ceefd27fb..e3230ad0096e 100644 --- a/build/ci/conda_env_1.yml +++ b/build/ci/conda_env_1.yml @@ -1,4 +1,4 @@ name: conda_env_1 dependencies: - - python=3.9 + - python=3.10 - pip diff --git a/build/ci/conda_env_2.yml b/build/ci/conda_env_2.yml index af9d7a46ba3e..38f551da2580 100644 --- a/build/ci/conda_env_2.yml +++ b/build/ci/conda_env_2.yml @@ -1,4 +1,4 @@ name: conda_env_2 dependencies: - - python=3.9 + - python=3.10 - pip From d48d1242833c3e579d9e08ea549ba7ac3a243357 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 7 May 2026 10:54:27 -0700 Subject: [PATCH 1133/1136] Fix duplicate command python.getRecommendedEnvironment (#25947) Adds guards when calling RecommendedEnvironmentService::activate per folder on a multiroot workspace to avoid registering the command several times. Fixes #25949 --- .../recommededEnvironmentService.ts | 5 ++ .../recommededEnvironmentService.unit.test.ts | 50 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/test/interpreters/recommededEnvironmentService.unit.test.ts diff --git a/src/client/interpreter/configuration/recommededEnvironmentService.ts b/src/client/interpreter/configuration/recommededEnvironmentService.ts index c5356409fcee..39f4edfde1d6 100644 --- a/src/client/interpreter/configuration/recommededEnvironmentService.ts +++ b/src/client/interpreter/configuration/recommededEnvironmentService.ts @@ -17,6 +17,7 @@ const MEMENTO_KEY = 'userSelectedEnvPath'; @injectable() export class RecommendedEnvironmentService implements IRecommendedEnvironmentService, IExtensionActivationService { private api?: PythonExtension['environments']; + private hasRegisteredCommand = false; constructor(@inject(IExtensionContext) private readonly extensionContext: IExtensionContext) {} supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean } = { untrustedWorkspace: true, @@ -24,6 +25,10 @@ export class RecommendedEnvironmentService implements IRecommendedEnvironmentSer }; async activate(_resource: Resource, _startupStopWatch?: StopWatch): Promise { + if (this.hasRegisteredCommand) { + return; + } + this.hasRegisteredCommand = true; this.extensionContext.subscriptions.push( commands.registerCommand('python.getRecommendedEnvironment', async (resource: Resource) => { return this.getRecommededEnvironment(resource); diff --git a/src/test/interpreters/recommededEnvironmentService.unit.test.ts b/src/test/interpreters/recommededEnvironmentService.unit.test.ts new file mode 100644 index 000000000000..7cb5aed58239 --- /dev/null +++ b/src/test/interpreters/recommededEnvironmentService.unit.test.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { anything, reset, verify, when } from 'ts-mockito'; +import { Disposable, Uri } from 'vscode'; +import { mockedVSCodeNamespaces } from '../vscode-mock'; +import { RecommendedEnvironmentService } from '../../client/interpreter/configuration/recommededEnvironmentService'; + +suite('RecommendedEnvironmentService - activate', () => { + let service: RecommendedEnvironmentService; + let subscriptions: Disposable[]; + + setup(() => { + subscriptions = []; + const extensionContext = { + subscriptions, + globalState: { + get: () => undefined, + update: () => Promise.resolve(), + }, + } as any; + + when(mockedVSCodeNamespaces.commands!.registerCommand(anything(), anything())).thenReturn({ + dispose: () => {}, + } as Disposable); + + service = new RecommendedEnvironmentService(extensionContext); + }); + + teardown(() => { + reset(mockedVSCodeNamespaces.commands!); + }); + + test('Multiroot workspace: command is registered only once across multiple activate calls', async () => { + // Simulate multiroot workspace where activate is called once per workspace root + const workspaceRoot1 = Uri.file('/workspace/root1'); + const workspaceRoot2 = Uri.file('/workspace/root2'); + const workspaceRoot3 = Uri.file('/workspace/root3'); + + await service.activate(workspaceRoot1); + await service.activate(workspaceRoot2); + await service.activate(workspaceRoot3); + + verify(mockedVSCodeNamespaces.commands!.registerCommand('python.getRecommendedEnvironment', anything())).once(); + expect(subscriptions).to.have.lengthOf(1); + }); +}); From 6642ccef67ce81b624b33526cf57a4cf90f3455e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 18:25:29 +0000 Subject: [PATCH 1134/1136] Bump packaging from 26.1 to 26.2 (#25934) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [packaging](https://github.com/pypa/packaging) from 26.1 to 26.2.
Release notes

Sourced from packaging's releases.

26.2

What's Changed

Fixes:

Documentation:

Internal:

New Contributors

Full Changelog: https://github.com/pypa/packaging/compare/26.1...26.2

Changelog

Sourced from packaging's changelog.

26.2 - 2026-04-24


Fixes:
  • Fix incorrect sysconfig var name for pyemscripten in (:pull:1160)
  • Make Version, Specifier, SpecifierSet, Tag, Marker, and Requirement pickle-safe
    and backward-compatible with pickles created in 25.0-26.1 (including references to the removed
    packaging._structures module) (:pull:1163, :pull:1168, :pull:1170, :pull:1171)
  • Re-export ExceptionGroup in metadata for now in (:pull:1164)

Documentation:

  • Add errors section and fix missing details in (:pull:1159)
  • Document our property-based test suite in (:pull:1167)
  • Fix a DirectUrl typo in (:pull:1167)
  • Add example of is_unsatisfiable in (:pull:1166)

Internal:

  • Enable the auditor persona on zizmor in (:pull:1158)
  • Test new pickle guarantees in (:pull:1174)
  • Use new native ReadTheDocs uv integration in (:pull:1175)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=packaging&package-manager=pip&previous-version=26.1&new-version=26.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5d325361bd4c..540590ed2ae7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,9 @@ microvenv==2025.0 \ --hash=sha256:568155ec18af01c89f270d35d123ab803b09672b480c3702d15fd69e9cc5bd1e \ --hash=sha256:8a2568a8390a4ffb5af2f05e7642454e03b887e582d192b6316326974eab5d0f # via -r requirements.in -packaging==26.1 \ - --hash=sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f \ - --hash=sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via -r requirements.in tomli==2.4.1 \ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ From 166f7a97d09b41718a5e384d30c2c3428a533ac5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 18:35:41 +0000 Subject: [PATCH 1135/1136] Bump fast-uri from 3.1.0 to 3.1.2 (#25951) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.0 to 3.1.2.
Release notes

Sourced from fast-uri's releases.

v3.1.2

⚠️ Security Release

What's Changed

Full Changelog: https://github.com/fastify/fast-uri/compare/v3.1.1...v3.1.2

v3.1.1

⚠️ Security Release

What's Changed

New Contributors

Full Changelog: https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.1

Commits
  • 919dd8e Bumped v3.1.2
  • c65ba57 fixup: linting
  • 6c86c17 Merge commit from fork
  • a95158a Handle malformed fragment decoding without throwing (#171)
  • cea547c Bumped v3.1.1
  • 876ce79 Merge commit from fork
  • dcdf690 ci: add lock-threads workflow (#169)
  • c860e65 build(deps-dev): bump neostandard from 0.12.2 to 0.13.0 (#167)
  • 9b4c6dc build(deps): bump fastify/workflows/.github/workflows/plugins-ci.yml (#166)
  • 85d09a9 build(deps): bump fastify/workflows/.github/workflows/plugins-ci-package-mana...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=fast-uri&package-manager=npm_and_yarn&previous-version=3.1.0&new-version=3.1.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/vscode-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 258726777690..82053df77576 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6799,9 +6799,9 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -20667,9 +20667,9 @@ "dev": true }, "fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true }, "fastest-levenshtein": { From 1df2dfdc13b26ce94643be237cba1e839c2ecab7 Mon Sep 17 00:00:00 2001 From: Ben Villalobos Date: Thu, 14 May 2026 13:56:37 -0700 Subject: [PATCH 1136/1136] Register spam label closure (#25955) Fixes https://github.com/microsoft/vscode-engineering/issues/2790 --- .github/commands.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/commands.json b/.github/commands.json index 2fb6684a7ee6..171f33f380c3 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -153,5 +153,14 @@ "addLabel": "info-needed", "removeLabel": "~confirmation-needed", "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "~spam", + "removeLabel": "~spam", + "addLabel": "spam", + "action": "close", + "reason": "not_planned", + "comment": "Thank you for your submission. This issue has been closed as it doesn't meet our community guidelines or appears to be spam.\n\n**If you believe this was closed in error:**\n- Please review our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/)\n- Ensure your issue contains a clear description of the problem or feature request\n- Feel free to open a new issue with appropriate detail if this was a legitimate concern\n\n**For legitimate issues, please include:**\n- Clear description of the problem\n- Steps to reproduce (for bugs)\n- Expected vs actual behavior\n- VS Code version and environment details\n\nThank you for helping us maintain a welcoming and productive community." } ]
Release notes

Sourced from brettcannon/check-for-changed-files's releases.

v1.1.1

What's Changed

Full Changelog: https://github.com/brettcannon/check-for-changed-files/compare/v1...v1.1.1