From 26c56a2d67131c5d9be2416372ab4717f310c0ef Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Thu, 2 Feb 2023 13:09:11 -0800 Subject: [PATCH 1/3] discovery code --- pythonFiles/vscode_pytest/__init__.py | 226 ++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 pythonFiles/vscode_pytest/__init__.py diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py new file mode 100644 index 000000000000..e4a54104ecaf --- /dev/null +++ b/pythonFiles/vscode_pytest/__init__.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- +import enum +import json +import os +import pathlib +import sys +from typing import List, Literal, Tuple, TypedDict, Union + +import pytest +from _pytest.doctest import DoctestItem, DoctestTextfile + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) + +# Inherit from str so it's JSON serializable. +class TestNodeTypeEnum(str, enum.Enum): + class_ = "class" + file = "file" + folder = "folder" + test = "test" + doc_file = "doc_file" + + +class TestData(TypedDict): + name: str + path: str + type_: TestNodeTypeEnum + id_: str + + +class TestItem(TestData): + lineno: str + runID: str + + +class TestNode(TestData): + children: "List[Union[TestNode, TestItem]]" + + +from testing_tools import socket_manager +from typing_extensions import NotRequired + +DEFAULT_PORT = "45454" + + +def pytest_collection_finish(session): + # Called after collection has been performed. + node: Union[TestNode, None] = build_test_tree(session)[0] + cwd = pathlib.Path.cwd() + if node: + sendPost(str(cwd), node) + # TODO: add error checking. + + +def build_test_tree(session) -> Tuple[Union[TestNode, None], List[str]]: + # Builds a tree of tests from the pytest session. + errors: List[str] = [] + session_node: TestNode = create_session_node(session) + session_children_dict: dict[str, TestNode] = {} + file_nodes_dict: dict[pytest.Module, TestNode] = {} + class_nodes_dict: dict[str, TestNode] = {} + + for test_case in session.items: + test_node: TestItem = create_test_node(test_case) + if type(test_case.parent) == DoctestTextfile: + try: + parent_test_case: TestNode = file_nodes_dict[test_case.parent] + except KeyError: + parent_test_case: TestNode = create_doc_file_node(test_case.parent) + file_nodes_dict[test_case.parent] = parent_test_case + parent_test_case["children"].append(test_node) + elif type(test_case.parent) is pytest.Module: + try: + parent_test_case: TestNode = file_nodes_dict[test_case.parent] + except KeyError: + parent_test_case: TestNode = create_file_node(test_case.parent) + file_nodes_dict[test_case.parent] = parent_test_case + parent_test_case["children"].append(test_node) + else: # should be a pytest.Class + try: + test_class_node: TestNode = class_nodes_dict[test_case.parent.name] + except KeyError: + test_class_node: TestNode = create_class_node(test_case.parent) + class_nodes_dict[test_case.parent.name] = test_class_node + test_class_node["children"].append(test_node) + parent_module: pytest.Module = test_case.parent.parent + # Create a file node that has the class as a child. + try: + test_file_node: TestNode = file_nodes_dict[parent_module] + except KeyError: + test_file_node: TestNode = create_file_node(parent_module) + file_nodes_dict[parent_module] = test_file_node + test_file_node["children"].append(test_class_node) + # 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) + created_files_folders_dict: dict[str, TestNode] = {} + for file_module, 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 + ) + # The final folder we get to is the highest folder in the path and therefore we add this as a child to the session. + if root_folder_node.get("id_") not in session_children_dict: + session_children_dict[root_folder_node.get("id_")] = root_folder_node + session_node["children"] = list(session_children_dict.values()) + return session_node, errors + + +def build_nested_folders( + file_module: pytest.Module, + file_node: TestNode, + created_files_folders_dict: dict[str, TestNode], + session: pytest.Session, +) -> TestNode: + prev_folder_node: TestNode = file_node + + # Begin the i_path iteration one level above the current file. + iterator_path: pathlib.Path = file_module.path.parent + while iterator_path != session.path: + curr_folder_name: str = iterator_path.name + try: + curr_folder_node: TestNode = created_files_folders_dict[curr_folder_name] + except KeyError: + curr_folder_node: TestNode = create_folder_node( + curr_folder_name, iterator_path + ) + created_files_folders_dict[curr_folder_name] = 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 + prev_folder_node = curr_folder_node + return prev_folder_node + + +def create_test_node( + test_case: pytest.Item, +) -> TestItem: + test_case_loc: str = ( + "" if test_case.location[1] is None else str(test_case.location[1] + 1) + ) + return { + "name": test_case.name, + "path": str(test_case.path), + "lineno": test_case_loc, + "type_": TestNodeTypeEnum.test, + "id_": test_case.nodeid, + "runID": test_case.nodeid, + } + + +def create_session_node(session: pytest.Session) -> TestNode: + return { + "name": session.name, + "path": str(session.path), + "type_": TestNodeTypeEnum.folder, + "children": [], + "id_": str(session.path), + } + + +def create_class_node(class_module: pytest.Class) -> TestNode: + return { + "name": class_module.name, + "path": str(class_module.path), + "type_": TestNodeTypeEnum.class_, + "children": [], + "id_": class_module.nodeid, + } + + +def create_file_node(file_module: pytest.Module) -> TestNode: + return { + "name": str(file_module.path.name), + "path": str(file_module.path), + "type_": TestNodeTypeEnum.file, + "id_": str(file_module.path), + "children": [], + } + + +def create_doc_file_node(file_module: pytest.Module) -> TestNode: + return { + "name": str(file_module.path.name), + "path": str(file_module.path), + "type_": TestNodeTypeEnum.doc_file, + "id_": str(file_module.path), + "children": [], + } + + +def create_folder_node(folderName: str, path_iterator: pathlib.Path) -> TestNode: + return { + "name": folderName, + "path": str(path_iterator), + "type_": TestNodeTypeEnum.folder, + "id_": str(path_iterator), + "children": [], + } + + +class PayloadDict(TypedDict): + cwd: str + status: Literal["success", "error"] + tests: NotRequired[TestNode] + errors: NotRequired[List[str]] + + +def sendPost(cwd: str, tests: TestNode) -> None: + # Sends a post request as a response to the server. + payload: PayloadDict = {"cwd": cwd, "status": "success", "tests": tests} + 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) + request = f"""POST / HTTP/1.1 +Host: localhost:{testPort} +Content-Length: {len(data)} +Content-Type: application/json +Request-uuid: {testuuid} + +{data}""" + with socket_manager.SocketManager(addr) as s: + if s.socket is not None: + s.socket.sendall(request.encode("utf-8")) # type: ignore From d8daedd2693b4ec69d1ad675f2fc0b37e8daf72f Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Mon, 6 Feb 2023 13:53:34 -0800 Subject: [PATCH 2/3] discovery + run engaged --- .../testing/testController/common/types.ts | 14 ++- .../testing/testController/controller.ts | 101 ++++++++++-------- .../pytest/pytestDiscoveryAdapter.ts | 24 ++--- .../pytest/pytestExecutionAdapter.ts | 90 ++++++++++------ .../testController/workspaceTestAdapter.ts | 31 ++++-- 5 files changed, 162 insertions(+), 98 deletions(-) diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 579c11d5ef25..eb95509cd14b 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -12,6 +12,7 @@ import { Uri, WorkspaceFolder, } from 'vscode'; +import { IPythonExecutionFactory } from '../../../common/process/types'; import { TestDiscoveryOptions } from '../../common/types'; export type TestRunInstanceOptions = TestRunOptions & { @@ -178,13 +179,20 @@ export interface ITestServer { export interface ITestDiscoveryAdapter { // ** Uncomment second line and comment out first line to use the new discovery method. - discoverTests(uri: Uri): Promise; - // discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise + // discoverTests(uri: Uri): Promise; + discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise; } // interface for execution/runner adapter export interface ITestExecutionAdapter { - runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise; + // ** Uncomment second line and comment out first line to use the new execution method. + // runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise; + runTests( + uri: Uri, + testIds: string[], + debugBool?: boolean, + executionFactory?: IPythonExecutionFactory, + ): Promise; } // Same types as in pythonFiles/unittestadapter/utils.py diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 8cba671277d0..3bbff727376e 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -162,8 +162,8 @@ export class PythonTestController implements ITestController, IExtensionSingleAc executionAdapter = new UnittestTestExecutionAdapter(this.pythonTestServer, this.configSettings); testProvider = UNITTEST_PROVIDER; } else { - discoveryAdapter = new PytestTestDiscoveryAdapter(this.pythonTestServer, { ...this.configSettings }); - executionAdapter = new PytestTestExecutionAdapter(this.pythonTestServer, this.configSettings); + discoveryAdapter = new PytestTestDiscoveryAdapter(this.pythonTestServer, this.configSettings); + executionAdapter = new PytestTestExecutionAdapter(this.pythonTestServer, this.configSettings); // ** why this?? testProvider = PYTEST_PROVIDER; } @@ -228,19 +228,19 @@ export class PythonTestController implements ITestController, IExtensionSingleAc // Ensure we send test telemetry if it gets disabled again this.sendTestDisabledTelemetry = true; // ** uncomment ~231 - 241 to NEW new test discovery mechanism - // const workspace = this.workspaceService.getWorkspaceFolder(uri); - // traceVerbose(`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( - // this.testController, - // this.refreshCancellation.token, - // this.testAdapters.size > 1, - // this.workspaceService.workspaceFile?.fsPath, - // this.pythonExecFactory, - // ); + const workspace = this.workspaceService.getWorkspaceFolder(uri); + traceVerbose(`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( + this.testController, + this.refreshCancellation.token, + this.testAdapters.size > 1, + this.workspaceService.workspaceFile?.fsPath, + this.pythonExecFactory, + ); // uncomment ~243 to use OLD test discovery mechanism - await this.pytest.refreshTestData(this.testController, uri, this.refreshCancellation.token); + // 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; @@ -372,16 +372,30 @@ export class PythonTestController implements ITestController, IExtensionSingleAc tool: 'pytest', debugging: request.profile?.kind === TestRunProfileKind.Debug, }); - return this.pytest.runTests( - { - includes: testItems, - excludes: request.exclude ?? [], - runKind: request.profile?.kind ?? TestRunProfileKind.Run, - runInstance, - }, - workspace, + // ** new execution runner/adapter + const testAdapter = + this.testAdapters.get(workspace.uri) || + (this.testAdapters.values().next().value as WorkspaceTestAdapter); + return testAdapter.executeTests( + this.testController, + runInstance, + testItems, token, + request.profile?.kind === TestRunProfileKind.Debug, + this.pythonExecFactory, ); + + // below is old way of running pytest execution + // return this.pytest.runTests( + // { + // includes: testItems, + // excludes: request.exclude ?? [], + // runKind: request.profile?.kind ?? TestRunProfileKind.Run, + // runInstance, + // }, + // workspace, + // token, + // ); } if (settings.testing.unittestEnabled) { // potentially squeeze in the new execution way here? @@ -390,30 +404,29 @@ export class PythonTestController implements ITestController, IExtensionSingleAc debugging: request.profile?.kind === TestRunProfileKind.Debug, }); // new execution runner/adapter - // const testAdapter = - // this.testAdapters.get(workspace.uri) || - // (this.testAdapters.values().next().value as WorkspaceTestAdapter); - // return testAdapter.executeTests( - // this.testController, - // runInstance, - // testItems, - // token, - // request.profile?.kind === TestRunProfileKind.Debug, - // ); - - // below is old way of running unittest execution - - return this.unittest.runTests( - { - includes: testItems, - excludes: request.exclude ?? [], - runKind: request.profile?.kind ?? TestRunProfileKind.Run, - runInstance, - }, - workspace, - token, + const testAdapter = + this.testAdapters.get(workspace.uri) || + (this.testAdapters.values().next().value as WorkspaceTestAdapter); + return testAdapter.executeTests( this.testController, + runInstance, + testItems, + token, + request.profile?.kind === TestRunProfileKind.Debug, ); + + // below is old way of running unittest execution + // return this.unittest.runTests( + // { + // includes: testItems, + // excludes: request.exclude ?? [], + // runKind: request.profile?.kind ?? TestRunProfileKind.Run, + // runInstance, + // }, + // workspace, + // token, + // this.testController, + // ); } } diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index e2108b872845..4339f1ac067d 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -35,20 +35,20 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } // ** Old version of discover tests. - discoverTests(uri: Uri): Promise { - traceVerbose(uri); - this.deferred = createDeferred(); - return this.deferred.promise; - } + // discoverTests(uri: Uri): Promise { + // traceVerbose(uri); + // this.deferred = createDeferred(); + // return this.deferred.promise; + // } // Uncomment this version of the function discoverTests to use the new discovery method. - // public async discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { - // const settings = this.configSettings.getSettings(uri); - // const { pytestArgs } = settings.testing; - // traceVerbose(pytestArgs); + public async discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { + const settings = this.configSettings.getSettings(uri); + const { pytestArgs } = settings.testing; + traceVerbose(pytestArgs); - // this.cwd = uri.fsPath; - // return this.runPytestDiscovery(uri, executionFactory); - // } + this.cwd = uri.fsPath; + return this.runPytestDiscovery(uri, executionFactory); + } async runPytestDiscovery(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { if (!this.deferred) { diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 35d62c50e774..babdb201426d 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -3,20 +3,18 @@ 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 { - DataReceivedEvent, - ExecutionTestPayload, - ITestExecutionAdapter, - ITestServer, - TestCommandOptions, - TestExecutionCommand, -} from '../common/types'; +import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; /** - * Wrapper Class for unittest test execution. This is where we call `runTestCommand`? + * Wrapper Class for pytest test execution. This is where we call `runTestCommand`? */ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { @@ -37,37 +35,69 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } } - public async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { + // ** Old version of discover tests. + // async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise{ + // traceVerbose(uri, testIds, debugBool); + // this.deferred = createDeferred(); + // return this.deferred.promise; + // } + + public async runTests( + uri: Uri, + testIds: string[], + debugBool?: boolean, + executionFactory?: IPythonExecutionFactory, + ): Promise { if (!this.deferred) { + this.deferred = createDeferred(); + const relativePathToPytest = 'pythonFiles'; + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); + this.configSettings.isTestExecution(); + const uuid = this.testServer.createUUID(uri.fsPath); const settings = this.configSettings.getSettings(uri); - const { unittestArgs } = settings.testing; + const { pytestArgs } = settings.testing; - const command = buildExecutionCommand(unittestArgs); - this.cwd = uri.fsPath; + const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - const options: TestCommandOptions = { - workspaceFolder: uri, - command, - cwd: this.cwd, - debugBool, - testIds, + const spawnOptions: SpawnOptions = { + cwd: uri.fsPath, + throwOnStdErr: true, + extraVariables: { + PYTHONPATH: pythonPathCommand, + TEST_UUID: uuid.toString(), + TEST_PORT: this.testServer.getPort().toString(), + }, }; - this.deferred = createDeferred(); + // Create the Python environment in which to execute the command. + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + }; + // need to check what will happen in the exec service is NOT defined and is null + const execService = await executionFactory?.createActivatedEnvironment(creationOptions); - // send test command to server - // server fire onDataReceived event once it gets response - this.testServer.sendCommand(options); + const testIdsString = testIds.join(' '); + console.debug('what to do with debug bool?', debugBool); + try { + execService?.exec( + ['-m', 'pytest', '-p', 'vscode_pytest', testIdsString].concat(pytestArgs), + spawnOptions, + ); + } catch (ex) { + console.error(ex); + } } return this.deferred.promise; } } -function buildExecutionCommand(args: string[]): TestExecutionCommand { - const executionScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); +// function buildExecutionCommand(args: string[]): TestExecutionCommand { +// const executionScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); - return { - script: executionScript, - args: ['--udiscovery', ...args], - }; -} +// return { +// script: executionScript, +// args: ['--udiscovery', ...args], +// }; +// } diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index 32bc0d5c29ef..36494ec3ab5b 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -14,10 +14,11 @@ import { Uri, Location, } from 'vscode'; +import { IPythonExecutionFactory } from '../../common/process/types'; import { splitLines } from '../../common/stringUtils'; import { createDeferred, Deferred } from '../../common/utils/async'; import { Testing } from '../../common/utils/localize'; -import { traceError } from '../../logging'; +import { traceError, traceVerbose } from '../../logging'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { TestProvider } from '../types'; @@ -74,6 +75,7 @@ export class WorkspaceTestAdapter { includes: TestItem[], token?: CancellationToken, debugBool?: boolean, + executionFactory?: IPythonExecutionFactory, ): Promise { if (this.executing) { return this.executing.promise; @@ -100,8 +102,18 @@ export class WorkspaceTestAdapter { } }); - // need to get the testItems runIds so that we can pass in here. - rawTestExecData = await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); + // ** First line is old way, section with if statement below is new way. + // rawTestExecData = await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); + if (executionFactory !== undefined) { + rawTestExecData = await this.executionAdapter.runTests( + this.workspaceUri, + testCaseIds, + debugBool, + executionFactory, + ); + } else { + traceVerbose('executionFactory is undefined'); + } deferred.resolve(); } catch (ex) { // handle token and telemetry here @@ -204,6 +216,7 @@ export class WorkspaceTestAdapter { token?: CancellationToken, isMultiroot?: boolean, workspaceFilePath?: string, + executionFactory?: IPythonExecutionFactory, ): Promise { sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: this.testProvider }); @@ -220,12 +233,12 @@ export class WorkspaceTestAdapter { let rawTestData; try { // ** First line is old way, section with if statement below is new way. - rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri); - // if (executionFactory !== undefined) { - // rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); - // } else { - // traceVerbose('executionFactory is undefined'); - // } + // rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri); + if (executionFactory !== undefined) { + rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); + } else { + traceVerbose('executionFactory is undefined'); + } deferred.resolve(); } catch (ex) { sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: true }); From ae355de11f54c95e8c554926dd683df4feac00b5 Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Mon, 6 Feb 2023 14:02:34 -0800 Subject: [PATCH 3/3] remove to inactivate --- pythonFiles/vscode_pytest/__init__.py | 226 ------------------ .../testing/testController/common/types.ts | 20 +- .../testing/testController/controller.ts | 110 ++++----- .../pytest/pytestDiscoveryAdapter.ts | 24 +- .../pytest/pytestExecutionAdapter.ts | 124 +++++----- .../testController/workspaceTestAdapter.ts | 40 ++-- 6 files changed, 155 insertions(+), 389 deletions(-) delete mode 100644 pythonFiles/vscode_pytest/__init__.py diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py deleted file mode 100644 index e4a54104ecaf..000000000000 --- a/pythonFiles/vscode_pytest/__init__.py +++ /dev/null @@ -1,226 +0,0 @@ -# -*- coding: utf-8 -*- -import enum -import json -import os -import pathlib -import sys -from typing import List, Literal, Tuple, TypedDict, Union - -import pytest -from _pytest.doctest import DoctestItem, DoctestTextfile - -script_dir = pathlib.Path(__file__).parent.parent -sys.path.append(os.fspath(script_dir)) -sys.path.append(os.fspath(script_dir / "lib" / "python")) - -# Inherit from str so it's JSON serializable. -class TestNodeTypeEnum(str, enum.Enum): - class_ = "class" - file = "file" - folder = "folder" - test = "test" - doc_file = "doc_file" - - -class TestData(TypedDict): - name: str - path: str - type_: TestNodeTypeEnum - id_: str - - -class TestItem(TestData): - lineno: str - runID: str - - -class TestNode(TestData): - children: "List[Union[TestNode, TestItem]]" - - -from testing_tools import socket_manager -from typing_extensions import NotRequired - -DEFAULT_PORT = "45454" - - -def pytest_collection_finish(session): - # Called after collection has been performed. - node: Union[TestNode, None] = build_test_tree(session)[0] - cwd = pathlib.Path.cwd() - if node: - sendPost(str(cwd), node) - # TODO: add error checking. - - -def build_test_tree(session) -> Tuple[Union[TestNode, None], List[str]]: - # Builds a tree of tests from the pytest session. - errors: List[str] = [] - session_node: TestNode = create_session_node(session) - session_children_dict: dict[str, TestNode] = {} - file_nodes_dict: dict[pytest.Module, TestNode] = {} - class_nodes_dict: dict[str, TestNode] = {} - - for test_case in session.items: - test_node: TestItem = create_test_node(test_case) - if type(test_case.parent) == DoctestTextfile: - try: - parent_test_case: TestNode = file_nodes_dict[test_case.parent] - except KeyError: - parent_test_case: TestNode = create_doc_file_node(test_case.parent) - file_nodes_dict[test_case.parent] = parent_test_case - parent_test_case["children"].append(test_node) - elif type(test_case.parent) is pytest.Module: - try: - parent_test_case: TestNode = file_nodes_dict[test_case.parent] - except KeyError: - parent_test_case: TestNode = create_file_node(test_case.parent) - file_nodes_dict[test_case.parent] = parent_test_case - parent_test_case["children"].append(test_node) - else: # should be a pytest.Class - try: - test_class_node: TestNode = class_nodes_dict[test_case.parent.name] - except KeyError: - test_class_node: TestNode = create_class_node(test_case.parent) - class_nodes_dict[test_case.parent.name] = test_class_node - test_class_node["children"].append(test_node) - parent_module: pytest.Module = test_case.parent.parent - # Create a file node that has the class as a child. - try: - test_file_node: TestNode = file_nodes_dict[parent_module] - except KeyError: - test_file_node: TestNode = create_file_node(parent_module) - file_nodes_dict[parent_module] = test_file_node - test_file_node["children"].append(test_class_node) - # 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) - created_files_folders_dict: dict[str, TestNode] = {} - for file_module, 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 - ) - # The final folder we get to is the highest folder in the path and therefore we add this as a child to the session. - if root_folder_node.get("id_") not in session_children_dict: - session_children_dict[root_folder_node.get("id_")] = root_folder_node - session_node["children"] = list(session_children_dict.values()) - return session_node, errors - - -def build_nested_folders( - file_module: pytest.Module, - file_node: TestNode, - created_files_folders_dict: dict[str, TestNode], - session: pytest.Session, -) -> TestNode: - prev_folder_node: TestNode = file_node - - # Begin the i_path iteration one level above the current file. - iterator_path: pathlib.Path = file_module.path.parent - while iterator_path != session.path: - curr_folder_name: str = iterator_path.name - try: - curr_folder_node: TestNode = created_files_folders_dict[curr_folder_name] - except KeyError: - curr_folder_node: TestNode = create_folder_node( - curr_folder_name, iterator_path - ) - created_files_folders_dict[curr_folder_name] = 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 - prev_folder_node = curr_folder_node - return prev_folder_node - - -def create_test_node( - test_case: pytest.Item, -) -> TestItem: - test_case_loc: str = ( - "" if test_case.location[1] is None else str(test_case.location[1] + 1) - ) - return { - "name": test_case.name, - "path": str(test_case.path), - "lineno": test_case_loc, - "type_": TestNodeTypeEnum.test, - "id_": test_case.nodeid, - "runID": test_case.nodeid, - } - - -def create_session_node(session: pytest.Session) -> TestNode: - return { - "name": session.name, - "path": str(session.path), - "type_": TestNodeTypeEnum.folder, - "children": [], - "id_": str(session.path), - } - - -def create_class_node(class_module: pytest.Class) -> TestNode: - return { - "name": class_module.name, - "path": str(class_module.path), - "type_": TestNodeTypeEnum.class_, - "children": [], - "id_": class_module.nodeid, - } - - -def create_file_node(file_module: pytest.Module) -> TestNode: - return { - "name": str(file_module.path.name), - "path": str(file_module.path), - "type_": TestNodeTypeEnum.file, - "id_": str(file_module.path), - "children": [], - } - - -def create_doc_file_node(file_module: pytest.Module) -> TestNode: - return { - "name": str(file_module.path.name), - "path": str(file_module.path), - "type_": TestNodeTypeEnum.doc_file, - "id_": str(file_module.path), - "children": [], - } - - -def create_folder_node(folderName: str, path_iterator: pathlib.Path) -> TestNode: - return { - "name": folderName, - "path": str(path_iterator), - "type_": TestNodeTypeEnum.folder, - "id_": str(path_iterator), - "children": [], - } - - -class PayloadDict(TypedDict): - cwd: str - status: Literal["success", "error"] - tests: NotRequired[TestNode] - errors: NotRequired[List[str]] - - -def sendPost(cwd: str, tests: TestNode) -> None: - # Sends a post request as a response to the server. - payload: PayloadDict = {"cwd": cwd, "status": "success", "tests": tests} - 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) - request = f"""POST / HTTP/1.1 -Host: localhost:{testPort} -Content-Length: {len(data)} -Content-Type: application/json -Request-uuid: {testuuid} - -{data}""" - with socket_manager.SocketManager(addr) as s: - if s.socket is not None: - s.socket.sendall(request.encode("utf-8")) # type: ignore diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index eb95509cd14b..e9eebb4c44d2 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 { IPythonExecutionFactory } from '../../../common/process/types'; +// ** import { IPythonExecutionFactory } from '../../../common/process/types'; import { TestDiscoveryOptions } from '../../common/types'; export type TestRunInstanceOptions = TestRunOptions & { @@ -179,20 +179,20 @@ export interface ITestServer { export interface ITestDiscoveryAdapter { // ** Uncomment second line and comment out first line to use the new discovery method. - // discoverTests(uri: Uri): Promise; - discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise; + discoverTests(uri: Uri): Promise; + // discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise; } // interface for execution/runner adapter export interface ITestExecutionAdapter { // ** Uncomment second line and comment out first line to use the new execution method. - // runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise; - runTests( - uri: Uri, - testIds: string[], - debugBool?: boolean, - executionFactory?: IPythonExecutionFactory, - ): Promise; + runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise; + // runTests( + // uri: Uri, + // testIds: string[], + // debugBool?: boolean, + // executionFactory?: IPythonExecutionFactory, + // ): Promise; } // Same types as in pythonFiles/unittestadapter/utils.py diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 3bbff727376e..b2be2d9c3054 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -163,7 +163,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc testProvider = UNITTEST_PROVIDER; } else { discoveryAdapter = new PytestTestDiscoveryAdapter(this.pythonTestServer, this.configSettings); - executionAdapter = new PytestTestExecutionAdapter(this.pythonTestServer, this.configSettings); // ** why this?? + executionAdapter = new PytestTestExecutionAdapter(this.pythonTestServer, this.configSettings); testProvider = PYTEST_PROVIDER; } @@ -228,19 +228,19 @@ export class PythonTestController implements ITestController, IExtensionSingleAc // Ensure we send test telemetry if it gets disabled again this.sendTestDisabledTelemetry = true; // ** uncomment ~231 - 241 to NEW new test discovery mechanism - const workspace = this.workspaceService.getWorkspaceFolder(uri); - traceVerbose(`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( - this.testController, - this.refreshCancellation.token, - this.testAdapters.size > 1, - this.workspaceService.workspaceFile?.fsPath, - this.pythonExecFactory, - ); + // const workspace = this.workspaceService.getWorkspaceFolder(uri); + // traceVerbose(`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( + // this.testController, + // this.refreshCancellation.token, + // this.testAdapters.size > 1, + // this.workspaceService.workspaceFile?.fsPath, + // this.pythonExecFactory, + // ); // uncomment ~243 to use OLD test discovery mechanism - // await this.pytest.refreshTestData(this.testController, uri, this.refreshCancellation.token); + 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; @@ -373,29 +373,29 @@ export class PythonTestController implements ITestController, IExtensionSingleAc debugging: request.profile?.kind === TestRunProfileKind.Debug, }); // ** new execution runner/adapter - const testAdapter = - this.testAdapters.get(workspace.uri) || - (this.testAdapters.values().next().value as WorkspaceTestAdapter); - return testAdapter.executeTests( - this.testController, - runInstance, - testItems, - token, - request.profile?.kind === TestRunProfileKind.Debug, - this.pythonExecFactory, - ); - - // below is old way of running pytest execution - // return this.pytest.runTests( - // { - // includes: testItems, - // excludes: request.exclude ?? [], - // runKind: request.profile?.kind ?? TestRunProfileKind.Run, - // runInstance, - // }, - // workspace, + // const testAdapter = + // this.testAdapters.get(workspace.uri) || + // (this.testAdapters.values().next().value as WorkspaceTestAdapter); + // return testAdapter.executeTests( + // this.testController, + // runInstance, + // testItems, // token, + // request.profile?.kind === TestRunProfileKind.Debug, + // this.pythonExecFactory, // ); + + // below is old way of running pytest execution + return this.pytest.runTests( + { + includes: testItems, + excludes: request.exclude ?? [], + runKind: request.profile?.kind ?? TestRunProfileKind.Run, + runInstance, + }, + workspace, + token, + ); } if (settings.testing.unittestEnabled) { // potentially squeeze in the new execution way here? @@ -404,29 +404,29 @@ export class PythonTestController implements ITestController, IExtensionSingleAc debugging: request.profile?.kind === TestRunProfileKind.Debug, }); // new execution runner/adapter - const testAdapter = - this.testAdapters.get(workspace.uri) || - (this.testAdapters.values().next().value as WorkspaceTestAdapter); - return testAdapter.executeTests( - this.testController, - runInstance, - testItems, - token, - request.profile?.kind === TestRunProfileKind.Debug, - ); - - // below is old way of running unittest execution - // return this.unittest.runTests( - // { - // includes: testItems, - // excludes: request.exclude ?? [], - // runKind: request.profile?.kind ?? TestRunProfileKind.Run, - // runInstance, - // }, - // workspace, - // token, + // const testAdapter = + // this.testAdapters.get(workspace.uri) || + // (this.testAdapters.values().next().value as WorkspaceTestAdapter); + // return testAdapter.executeTests( // this.testController, + // runInstance, + // testItems, + // token, + // request.profile?.kind === TestRunProfileKind.Debug, // ); + + // below is old way of running unittest execution + return this.unittest.runTests( + { + includes: testItems, + excludes: request.exclude ?? [], + runKind: request.profile?.kind ?? TestRunProfileKind.Run, + runInstance, + }, + workspace, + token, + this.testController, + ); } } diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 4339f1ac067d..e2108b872845 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -35,20 +35,20 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } // ** Old version of discover tests. - // discoverTests(uri: Uri): Promise { - // traceVerbose(uri); - // this.deferred = createDeferred(); - // return this.deferred.promise; - // } + discoverTests(uri: Uri): Promise { + traceVerbose(uri); + this.deferred = createDeferred(); + return this.deferred.promise; + } // Uncomment this version of the function discoverTests to use the new discovery method. - public async discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { - const settings = this.configSettings.getSettings(uri); - const { pytestArgs } = settings.testing; - traceVerbose(pytestArgs); + // public async discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { + // const settings = this.configSettings.getSettings(uri); + // const { pytestArgs } = settings.testing; + // traceVerbose(pytestArgs); - this.cwd = uri.fsPath; - return this.runPytestDiscovery(uri, executionFactory); - } + // this.cwd = uri.fsPath; + // return this.runPytestDiscovery(uri, executionFactory); + // } async runPytestDiscovery(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { if (!this.deferred) { diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index babdb201426d..eaabb57691d0 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -1,16 +1,10 @@ // 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, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; /** @@ -36,68 +30,68 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } // ** Old version of discover tests. - // async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise{ - // traceVerbose(uri, testIds, debugBool); - // this.deferred = createDeferred(); - // return this.deferred.promise; - // } + async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { + traceVerbose(uri, testIds, debugBool); + this.deferred = createDeferred(); + return this.deferred.promise; + } - public async runTests( - uri: Uri, - testIds: string[], - debugBool?: boolean, - executionFactory?: IPythonExecutionFactory, - ): Promise { - if (!this.deferred) { - this.deferred = createDeferred(); - const relativePathToPytest = 'pythonFiles'; - const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); - this.configSettings.isTestExecution(); - const uuid = this.testServer.createUUID(uri.fsPath); - const settings = this.configSettings.getSettings(uri); - const { pytestArgs } = settings.testing; + // public async runTests( + // uri: Uri, + // testIds: string[], + // debugBool?: boolean, + // executionFactory?: IPythonExecutionFactory, + // ): Promise { + // if (!this.deferred) { + // this.deferred = createDeferred(); + // const relativePathToPytest = 'pythonFiles'; + // const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); + // this.configSettings.isTestExecution(); + // const uuid = this.testServer.createUUID(uri.fsPath); + // const settings = this.configSettings.getSettings(uri); + // const { pytestArgs } = settings.testing; - const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; - const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); + // const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; + // const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); - const spawnOptions: SpawnOptions = { - cwd: uri.fsPath, - throwOnStdErr: true, - extraVariables: { - PYTHONPATH: pythonPathCommand, - TEST_UUID: uuid.toString(), - TEST_PORT: this.testServer.getPort().toString(), - }, - }; + // 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, - }; - // need to check what will happen in the exec service is NOT defined and is null - const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + // // Create the Python environment in which to execute the command. + // const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + // allowEnvironmentFetchExceptions: false, + // resource: uri, + // }; + // // need to check what will happen in the exec service is NOT defined and is null + // const execService = await executionFactory?.createActivatedEnvironment(creationOptions); - const testIdsString = testIds.join(' '); - console.debug('what to do with debug bool?', debugBool); - try { - execService?.exec( - ['-m', 'pytest', '-p', 'vscode_pytest', testIdsString].concat(pytestArgs), - spawnOptions, - ); - } catch (ex) { - console.error(ex); - } - } - return this.deferred.promise; - } -} + // const testIdsString = testIds.join(' '); + // console.debug('what to do with debug bool?', debugBool); + // try { + // execService?.exec( + // ['-m', 'pytest', '-p', 'vscode_pytest', testIdsString].concat(pytestArgs), + // spawnOptions, + // ); + // } catch (ex) { + // console.error(ex); + // } + // } + // return this.deferred.promise; + // } + // } -// function buildExecutionCommand(args: string[]): TestExecutionCommand { -// const executionScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); + // function buildExecutionCommand(args: string[]): TestExecutionCommand { + // const executionScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); -// return { -// script: executionScript, -// args: ['--udiscovery', ...args], -// }; -// } + // return { + // script: executionScript, + // args: ['--udiscovery', ...args], + // }; +} diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index 36494ec3ab5b..dc9c65c431fd 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -14,11 +14,10 @@ import { Uri, Location, } from 'vscode'; -import { IPythonExecutionFactory } from '../../common/process/types'; import { splitLines } from '../../common/stringUtils'; 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'; @@ -69,13 +68,13 @@ export class WorkspaceTestAdapter { this.vsIdToRunId = new Map(); } + // ** add executionFactory?: IPythonExecutionFactory, to the parameters public async executeTests( testController: TestController, runInstance: TestRun, includes: TestItem[], token?: CancellationToken, debugBool?: boolean, - executionFactory?: IPythonExecutionFactory, ): Promise { if (this.executing) { return this.executing.promise; @@ -103,17 +102,17 @@ export class WorkspaceTestAdapter { }); // ** First line is old way, section with if statement below is new way. - // rawTestExecData = await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); - if (executionFactory !== undefined) { - rawTestExecData = await this.executionAdapter.runTests( - this.workspaceUri, - testCaseIds, - debugBool, - executionFactory, - ); - } else { - traceVerbose('executionFactory is undefined'); - } + rawTestExecData = await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); + // if (executionFactory !== undefined) { + // rawTestExecData = await this.executionAdapter.runTests( + // this.workspaceUri, + // testCaseIds, + // debugBool, + // executionFactory, + // ); + // } else { + // traceVerbose('executionFactory is undefined'); + // } deferred.resolve(); } catch (ex) { // handle token and telemetry here @@ -216,7 +215,6 @@ export class WorkspaceTestAdapter { token?: CancellationToken, isMultiroot?: boolean, workspaceFilePath?: string, - executionFactory?: IPythonExecutionFactory, ): Promise { sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: this.testProvider }); @@ -233,12 +231,12 @@ export class WorkspaceTestAdapter { let rawTestData; try { // ** First line is old way, section with if statement below is new way. - // rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri); - if (executionFactory !== undefined) { - rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); - } else { - traceVerbose('executionFactory is undefined'); - } + rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri); + // if (executionFactory !== undefined) { + // rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); + // } else { + // traceVerbose('executionFactory is undefined'); + // } deferred.resolve(); } catch (ex) { sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: true });