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 |
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/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/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",
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..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():
@@ -329,3 +335,32 @@ 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/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(
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()
diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py
index 1e812e41d2ae..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
@@ -495,7 +488,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 +537,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 +575,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 +750,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": [],
}
diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts
index b02670836015..674d243e415d 100644
--- a/src/client/common/terminal/service.ts
+++ b/src/client/common/terminal/service.ts
@@ -2,7 +2,7 @@
// Licensed under the MIT License.
import { inject, injectable } from 'inversify';
-import { CancellationToken, Disposable, Event, EventEmitter, Terminal, TerminalShellExecution } from 'vscode';
+import { window, CancellationToken, Disposable, Event, EventEmitter, Terminal, TerminalShellExecution } from 'vscode';
import '../../common/extensions';
import { IInterpreterService } from '../../interpreter/contracts';
import { IServiceContainer } from '../../ioc/types';
@@ -88,6 +88,18 @@ export class TerminalService implements ITerminalService, Disposable {
terminal.show(true);
}
+ window.onDidChangeTerminalState((e) => {
+ const ourTerminalState = e.state;
+ console.log(ourTerminalState);
+ traceVerbose('Printing our terminal state from service.ts', ourTerminalState);
+
+ const ourStateTypeless = (ourTerminalState as unknown) as { isInteractedWith: any; shellType: any };
+ const ourState = ourStateTypeless.isInteractedWith;
+ traceVerbose('Printing our terminal state from service.ts', ourState);
+ const ourShellType = ourStateTypeless.shellType;
+ traceVerbose('Printing our terminal state from service.ts', ourShellType);
+ traceVerbose('finished printing our terminal state');
+ });
// If terminal was just launched, wait some time for shell integration to onDidChangeShellIntegration.
if (!terminal.shellIntegration && this._terminalFirstLaunched) {
this._terminalFirstLaunched = false;
@@ -96,6 +108,12 @@ export class TerminalService implements ITerminalService, Disposable {
clearTimeout(timer);
disposable.dispose();
resolve(true);
+
+ const shellIntegration = (terminal.shellIntegration as unknown) as { env: any };
+ const tempEnv = shellIntegration.env;
+ console.log(tempEnv);
+ traceVerbose('Printing temp env from service.ts in terminal1', tempEnv);
+ traceVerbose('finished printing temp env ');
});
const TIMEOUT_DURATION = 500;
const timer = setTimeout(() => {
@@ -114,7 +132,13 @@ export class TerminalService implements ITerminalService, Disposable {
return undefined;
} else if (terminal.shellIntegration) {
const execution = terminal.shellIntegration.executeCommand(commandLine);
- traceVerbose(`Shell Integration is enabled, executeCommand: ${commandLine}`);
+
+ const shellIntegration = (terminal.shellIntegration as unknown) as { env: any };
+ const tempEnv = shellIntegration.env;
+ console.log(tempEnv);
+ traceVerbose('Printing temp env from service.ts in terminal2', tempEnv);
+ traceVerbose('finished printing temp env ');
+ // traceVerbose(`Shell Integration is enabled, executeCommand: ${commandLine}`);
return execution;
} else {
terminal.sendText(commandLine);
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/extensionActivation.ts b/src/client/extensionActivation.ts
index 38f2d6a56277..b325e66acd34 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 { traceVerbose } from './logging';
export async function activateComponents(
// `ext` is passed to any extra activation funcs.
@@ -115,6 +116,20 @@ export function activateFeatures(ext: ExtensionState, _components: Components):
registerStartNativeReplCommand(ext.disposables, interpreterService);
registerReplCommands(ext.disposables, interpreterService, executionHelper, commandManager);
registerReplExecuteOnEnter(ext.disposables, interpreterService, commandManager);
+
+ window.onDidChangeTerminalState((e) => {
+ const ourTerminalState = e.state;
+ console.log(ourTerminalState);
+ traceVerbose('Printing our terminal state from service.ts', ourTerminalState);
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const ourStateTypeless = (ourTerminalState as unknown) as { isInteractedWith: any; shellType: any };
+ const ourState = ourStateTypeless.isInteractedWith;
+ traceVerbose('Printing our terminal state from service.ts', ourState);
+ const ourShellType = ourStateTypeless.shellType;
+ traceVerbose('Printing our terminal state from service.ts', ourShellType);
+ traceVerbose('finished printing our terminal state');
+ });
}
/// //////////////////////////
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/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/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/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];
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/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())
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