diff --git a/pythonFiles/testing_tools/run_adapter.py b/pythonFiles/testing_tools/run_adapter.py index 1eeef194f8f5..4d888ffb60b0 100644 --- a/pythonFiles/testing_tools/run_adapter.py +++ b/pythonFiles/testing_tools/run_adapter.py @@ -14,8 +14,7 @@ ), ) -from testing_tools.adapter.__main__ import parse_args, main - +from testing_tools.adapter.__main__ import main, parse_args if __name__ == "__main__": tool, cmd, subargs, toolargs = parse_args() diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index adf5bba1a33c..6a2622da3944 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -71,6 +71,17 @@ export class PythonTestServer implements ITestServer, Disposable { return (this.server.address() as net.AddressInfo).port; } + /** + * creates a UUID using crypto library given test command options + * @param options test command options + * @returns a UUID as a string + */ + public createUUID(cwd: string): string { + const uuid = crypto.randomUUID(); + this.uuids.set(uuid, cwd); + return uuid; + } + public dispose(): void { this.server.close(); this._onDataReceived.dispose(); @@ -81,15 +92,13 @@ export class PythonTestServer implements ITestServer, Disposable { } async sendCommand(options: TestCommandOptions): Promise { - const uuid = crypto.randomUUID(); + const uuid = this.createUUID(options.cwd); const spawnOptions: SpawnOptions = { token: options.token, cwd: options.cwd, throwOnStdErr: true, }; - this.uuids.set(uuid, options.cwd); - // Create the Python environment in which to execute the command. const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { allowEnvironmentFetchExceptions: false, diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 064307ca8d9a..291772397b55 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -151,6 +151,17 @@ export type TestCommandOptions = { testIds?: string[]; }; +export type TestCommandOptionsPytest = { + workspaceFolder: Uri; + cwd: string; + commandStr: string; + token?: CancellationToken; + outChannel?: OutputChannel; + debugBool?: boolean; + testIds?: string[]; + env: { [key: string]: string | undefined }; +}; + /** * Interface describing the server that will send test commands to the Python side, and process responses. * @@ -161,10 +172,13 @@ export interface ITestServer { readonly onDataReceived: Event; sendCommand(options: TestCommandOptions): Promise; serverReady(): Promise; + getPort(): number; + createUUID(cwd: string): string; } export interface ITestDiscoveryAdapter { discoverTests(uri: Uri): Promise; + // discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise; testing rewrite } // interface for execution/runner adapter diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index fafdd3fafe7e..7b3f179b7f4a 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -39,8 +39,10 @@ import { ITestExecutionAdapter, } from './common/types'; import { UnittestTestDiscoveryAdapter } from './unittest/testDiscoveryAdapter'; -import { WorkspaceTestAdapter } from './workspaceTestAdapter'; import { UnittestTestExecutionAdapter } from './unittest/testExecutionAdapter'; +import { PytestTestDiscoveryAdapter } from './pytest/pytestDiscoveryAdapter'; +import { PytestTestExecutionAdapter } from './pytest/pytestExecutionAdapter'; +import { WorkspaceTestAdapter } from './workspaceTestAdapter'; import { ITestDebugLauncher } from '../common/types'; // Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. @@ -141,7 +143,6 @@ export class PythonTestController implements ITestController, IExtensionSingleAc }); return this.refreshTestData(undefined, { forceRefresh: true }); }; - this.pythonTestServer = new PythonTestServer(this.pythonExecFactory, this.debugLauncher); } @@ -161,13 +162,10 @@ export class PythonTestController implements ITestController, IExtensionSingleAc executionAdapter = new UnittestTestExecutionAdapter(this.pythonTestServer, this.configSettings); testProvider = UNITTEST_PROVIDER; } else { - // TODO: PYTEST DISCOVERY ADAPTER - // this is a placeholder for now - discoveryAdapter = new UnittestTestDiscoveryAdapter(this.pythonTestServer, { ...this.configSettings }); - executionAdapter = new UnittestTestExecutionAdapter(this.pythonTestServer, this.configSettings); + discoveryAdapter = new PytestTestDiscoveryAdapter(this.pythonTestServer, { ...this.configSettings }); + executionAdapter = new PytestTestExecutionAdapter(this.pythonTestServer, this.configSettings); testProvider = PYTEST_PROVIDER; } - const workspaceTestAdapter = new WorkspaceTestAdapter( testProvider, discoveryAdapter, @@ -224,18 +222,23 @@ 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); + console.warn(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); if (settings.testing.pytestEnabled) { - traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`); - + // const testAdapter = + // this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); + // testAdapter.discoverTests( + // this.testController, + // this.refreshCancellation.token, + // this.testAdapters.size > 1, + // this.workspaceService.workspaceFile?.fsPath, + // ); // Ensure we send test telemetry if it gets disabled again this.sendTestDisabledTelemetry = true; - + // comment the line 240 and uncomment the lines 229-236 to run the new way await this.pytest.refreshTestData(this.testController, uri, this.refreshCancellation.token); } else if (settings.testing.unittestEnabled) { - // TODO: Use new test discovery mechanism - // traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`); - // const workspace = this.workspaceService.getWorkspaceFolder(uri); - // console.warn(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); // const testAdapter = // this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); // testAdapter.discoverTests( @@ -244,8 +247,8 @@ export class PythonTestController implements ITestController, IExtensionSingleAc // this.testAdapters.size > 1, // this.workspaceService.workspaceFile?.fsPath, // ); - // // Ensure we send test telemetry if it gets disabled again - // this.sendTestDisabledTelemetry = true; + // Ensure we send test telemetry if it gets disabled again + this.sendTestDisabledTelemetry = true; // comment below 229 to run the new way and uncomment above 212 ~ 227 await this.unittest.refreshTestData(this.testController, uri, this.refreshCancellation.token); } else { @@ -256,7 +259,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 new file mode 100644 index 000000000000..97cece32a948 --- /dev/null +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import { Uri } from 'vscode'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + IPythonExecutionFactory, + SpawnOptions, +} from '../../../common/process/types'; +import { IConfigurationService } from '../../../common/types'; +import { createDeferred, Deferred } from '../../../common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { traceVerbose } from '../../../logging'; +import { DataReceivedEvent, DiscoveredTestPayload, ITestDiscoveryAdapter, ITestServer } from '../common/types'; + +/** + * Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied + */ +export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { + private deferred: Deferred | undefined; + + private cwd: string | undefined; + + constructor(public testServer: ITestServer, public configSettings: IConfigurationService) { + testServer.onDataReceived(this.onDataReceivedHandler, this); + } + + discoverTests(uri: Uri): Promise { + traceVerbose(uri); + this.deferred = createDeferred(); + return this.deferred.promise; + } + + public onDataReceivedHandler({ cwd, data }: DataReceivedEvent): void { + if (this.deferred && cwd === this.cwd) { + const testData: DiscoveredTestPayload = JSON.parse(data); + + this.deferred.resolve(testData); + this.deferred = undefined; + } + } + + // public async discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { + // const settings = this.configSettings.getSettings(uri); + // const { pytestArgs } = settings.testing; + // traceVerbose(pytestArgs); // do we use pytestArgs anywhere? + + // this.cwd = uri.fsPath; + // return this.runPytestDiscovery(uri, executionFactory); + // } + + async runPytestDiscovery(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { + if (!this.deferred) { + this.deferred = createDeferred(); + const relativePathToPytest = 'pythonFiles/pytest-vscode-integration'; + 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 pythonPathCommand = `${fullPluginPath}${path.delimiter}`.concat(process.env.PYTHONPATH ?? ''); + + const spawnOptions: SpawnOptions = { + cwd: uri.fsPath, + throwOnStdErr: true, + extraVariables: { + PYTHONPATH: pythonPathCommand, + TEST_UUID: uuid.toString(), + TEST_PORT: this.testServer.getPort().toString(), + }, + }; + + // Create the Python environment in which to execute the command. + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + }; + const execService = await executionFactory.createActivatedEnvironment(creationOptions); + + try { + execService.exec(['-m', 'pytest', '--collect-only'].concat(pytestArgs), spawnOptions); + } catch (ex) { + console.error(ex); + } + } + return this.deferred.promise; + } +} diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts new file mode 100644 index 000000000000..566b7670b256 --- /dev/null +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { IConfigurationService } from '../../../common/types'; +import { createDeferred, Deferred } from '../../../common/utils/async'; +import { + DataReceivedEvent, + ExecutionTestPayload, + ITestExecutionAdapter, + ITestServer, + TestCommandOptions, + TestExecutionCommand, +} from '../common/types'; + +/** + * Wrapper Class for pytest test execution. This is where we call `runTestCommand`? + */ + +export class PytestTestExecutionAdapter implements ITestExecutionAdapter { + private deferred: Deferred | undefined; + + private cwd: string | undefined; + + constructor(public testServer: ITestServer, public configSettings: IConfigurationService) { + testServer.onDataReceived(this.onDataReceivedHandler, this); + } + + public onDataReceivedHandler({ cwd, data }: DataReceivedEvent): void { + if (this.deferred && cwd === this.cwd) { + const testData: ExecutionTestPayload = JSON.parse(data); + + this.deferred.resolve(testData); + this.deferred = undefined; + } + } + + public async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { + if (!this.deferred) { + const settings = this.configSettings.getSettings(uri); + const { pytestArgs } = settings.testing; + + const command = buildExecutionCommand(pytestArgs); + this.cwd = uri.fsPath; + + const options: TestCommandOptions = { + workspaceFolder: uri, + command, + cwd: this.cwd, + debugBool, + testIds, + }; + + this.deferred = createDeferred(); + + // send test command to server + // server fire onDataReceived event once it gets response + this.testServer.sendCommand(options); + } + return this.deferred.promise; + } +} + +function buildExecutionCommand(args: string[]): TestExecutionCommand { + const executionScript = ''; + + return { + script: executionScript, + args: ['--udiscovery', ...args], + }; +}