Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions python_files/tests/pytestadapter/.data/test_marks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import pytest


@pytest.mark.slow # test_marker--test_with_single_mark
def test_with_single_mark():
assert True


@pytest.mark.slow # test_marker--test_with_multiple_marks
@pytest.mark.integration
def test_with_multiple_marks():
assert True


def test_with_no_marks(): # test_marker--test_with_no_marks
assert True


@pytest.mark.slow # test_marker--test_with_duplicate_marks
@pytest.mark.slow
def test_with_duplicate_marks():
assert True


@pytest.mark.parametrize("x", [1, 2]) # test_marker--test_parametrize_with_mark
@pytest.mark.slow
def test_parametrize_with_mark(x):
assert x > 0
145 changes: 145 additions & 0 deletions python_files/tests/pytestadapter/expected_discovery_test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,7 @@
"tests/test_a.py::test_a_function",
SYMLINK_FOLDER_PATH_TESTS_TEST_A,
),
"tags": [],
}
],
},
Expand All @@ -1119,6 +1120,7 @@
"tests/test_b.py::test_b_function",
SYMLINK_FOLDER_PATH_TESTS_TEST_B,
),
"tags": [],
}
],
},
Expand Down Expand Up @@ -2081,3 +2083,146 @@
],
"id_": os.fspath(unittest_folder_path),
}

# This is the expected output for the test_marks.py file.
# └── test_marks.py
# └── test_with_single_mark (tags: ["slow"])
# └── test_with_multiple_marks (tags: ["integration", "slow"])
# └── test_with_no_marks (tags: [])
# └── test_with_duplicate_marks (tags: ["slow"])
# └── test_parametrize_with_mark (function)
# └── [1] (tags: ["slow"])
# └── [2] (tags: ["slow"])
marks_test_file_path = TEST_DATA_PATH / "test_marks.py"
marks_test_expected_output = {
"name": ".data",
"path": TEST_DATA_PATH_STR,
"type_": "folder",
"children": [
{
"name": "test_marks.py",
"path": os.fspath(marks_test_file_path),
"type_": "file",
"id_": os.fspath(marks_test_file_path),
"children": [
{
"name": "test_with_single_mark",
"path": os.fspath(marks_test_file_path),
"lineno": find_test_line_number(
"test_with_single_mark",
marks_test_file_path,
),
"type_": "test",
"id_": get_absolute_test_id(
"test_marks.py::test_with_single_mark",
marks_test_file_path,
),
"runID": get_absolute_test_id(
"test_marks.py::test_with_single_mark",
marks_test_file_path,
),
"tags": ["slow"],
},
{
"name": "test_with_multiple_marks",
"path": os.fspath(marks_test_file_path),
"lineno": find_test_line_number(
"test_with_multiple_marks",
marks_test_file_path,
),
"type_": "test",
"id_": get_absolute_test_id(
"test_marks.py::test_with_multiple_marks",
marks_test_file_path,
),
"runID": get_absolute_test_id(
"test_marks.py::test_with_multiple_marks",
marks_test_file_path,
),
"tags": ["integration", "slow"],
},
{
"name": "test_with_no_marks",
"path": os.fspath(marks_test_file_path),
"lineno": find_test_line_number(
"test_with_no_marks",
marks_test_file_path,
),
"type_": "test",
"id_": get_absolute_test_id(
"test_marks.py::test_with_no_marks",
marks_test_file_path,
),
"runID": get_absolute_test_id(
"test_marks.py::test_with_no_marks",
marks_test_file_path,
),
"tags": [],
},
{
"name": "test_with_duplicate_marks",
"path": os.fspath(marks_test_file_path),
"lineno": find_test_line_number(
"test_with_duplicate_marks",
marks_test_file_path,
),
"type_": "test",
"id_": get_absolute_test_id(
"test_marks.py::test_with_duplicate_marks",
marks_test_file_path,
),
"runID": get_absolute_test_id(
"test_marks.py::test_with_duplicate_marks",
marks_test_file_path,
),
"tags": ["slow"],
},
{
"name": "test_parametrize_with_mark",
"path": os.fspath(marks_test_file_path),
"type_": "function",
"id_": os.fspath(marks_test_file_path) + "::test_parametrize_with_mark",
"children": [
{
"name": "[1]",
"path": os.fspath(marks_test_file_path),
"lineno": find_test_line_number(
"test_parametrize_with_mark[1]",
marks_test_file_path,
),
"type_": "test",
"id_": get_absolute_test_id(
"test_marks.py::test_parametrize_with_mark[1]",
marks_test_file_path,
),
"runID": get_absolute_test_id(
"test_marks.py::test_parametrize_with_mark[1]",
marks_test_file_path,
),
"tags": ["slow"],
},
{
"name": "[2]",
"path": os.fspath(marks_test_file_path),
"lineno": find_test_line_number(
"test_parametrize_with_mark[2]",
marks_test_file_path,
),
"type_": "test",
"id_": get_absolute_test_id(
"test_marks.py::test_parametrize_with_mark[2]",
marks_test_file_path,
),
"runID": get_absolute_test_id(
"test_marks.py::test_parametrize_with_mark[2]",
marks_test_file_path,
),
"tags": ["slow"],
},
],
},
],
}
],
"id_": TEST_DATA_PATH_STR,
}
32 changes: 32 additions & 0 deletions python_files/tests/pytestadapter/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,38 @@ def test_pytest_collect(file, expected_const):
)


def test_pytest_marks_discovery():
"""Test that pytest marks are exposed as tags during discovery.

Covers single marks, multiple marks (in discovery order), unmarked
tests, duplicate marks (deduplicated), and parametrized tests with
marks, excluding the `parametrize` marker itself.
"""
actual = helpers.runner(
[
os.fspath(helpers.TEST_DATA_PATH / "test_marks.py"),
"--collect-only",
]
)

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.marks_test_expected_output,
["id_", "lineno", "name", "runID", "tags"],
), (
f"Tests tree does not match expected value. \\n Expected: {json.dumps(expected_discovery_test_output.marks_test_expected_output, indent=4)}. \\n Actual: {json.dumps(actual_item.get('tests'), indent=4)}"
)


@pytest.mark.skipif(
sys.platform == "win32",
reason="See https://stackoverflow.com/questions/32877260/privlege-error-trying-to-create-symlink-using-python-on-windows-10",
Expand Down
13 changes: 13 additions & 0 deletions python_files/vscode_pytest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class TestItem(TestData):

lineno: str
runID: str
tags: list[str]


class TestNode(TestData):
Expand Down Expand Up @@ -847,13 +848,25 @@ def create_test_node(
str(test_case.location[1] + 1) if (test_case.location[1] is not None) else ""
)
absolute_test_id = get_absolute_test_id(test_case.nodeid, get_node_path(test_case))

# Extract pytest marks as tags (deduplicated, preserving discovery order).
# `parametrize` is excluded: it describes parameterization, not a user mark.
tags: list[str] = list(
dict.fromkeys(
marker.name
for marker in (getattr(test_case, "own_markers", None) or [])
if marker.name and marker.name != "parametrize"
)
)

return {
"name": test_case.name,
"path": get_node_path(test_case),
"lineno": test_case_loc,
"type_": "test",
"id_": absolute_test_id,
"runID": absolute_test_id,
"tags": tags,
}


Expand Down
3 changes: 3 additions & 0 deletions src/client/testing/testController/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ export type DiscoveredTestCommon = {
export type DiscoveredTestItem = DiscoveredTestCommon & {
lineno: number | string;
runID: string;
// Pytest marks exposed as tags (e.g. ["slow", "integration"]).
// Absent for items from adapters that do not report marks.
tags?: string[];
};

export type DiscoveredTestNode = DiscoveredTestCommon & {
Expand Down
6 changes: 4 additions & 2 deletions src/client/testing/testController/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,10 @@ export function populateTestTree(
// Create project-scoped vsId
const vsId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${child.id_}` : child.id_;
const testItem = testController.createTestItem(vsId, child.name, Uri.file(child.path));
testItem.tags = [RunTestTag, DebugTestTag];

// Create tags from pytest marks (if available) and combine with default tags.
const pytestMarkTags = (child.tags ?? []).map((tag) => ({ id: `mark.${tag}` }));
testItem.tags = [RunTestTag, DebugTestTag, ...pytestMarkTags];

let range: Range | undefined;
if (child.lineno) {
Expand All @@ -297,7 +300,6 @@ export function populateTestTree(
}
testItem.canResolveChildren = false;
testItem.range = range;
testItem.tags = [RunTestTag, DebugTestTag];

testRoot!.children.add(testItem);
// add to our map - use runID as key, vsId as value
Expand Down
Loading