Skip to content

Commit 6138eda

Browse files
andravinclaude
andcommitted
test(testing): add tests for pytest marks as tags feature
Add TypeScript unit tests verifying populateTestTree correctly converts tags to TestTag objects with mark. prefix, and handles empty/undefined tags. Add Python discovery test verifying mark extraction, deduplication, parametrize filtering, and tag ordering. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2407f30 commit 6138eda

File tree

4 files changed

+343
-0
lines changed

4 files changed

+343
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import pytest
5+
6+
7+
@pytest.mark.slow # test_marker--test_with_single_mark
8+
def test_with_single_mark():
9+
assert True
10+
11+
12+
@pytest.mark.slow # test_marker--test_with_multiple_marks
13+
@pytest.mark.integration
14+
def test_with_multiple_marks():
15+
assert True
16+
17+
18+
def test_with_no_marks(): # test_marker--test_with_no_marks
19+
assert True
20+
21+
22+
@pytest.mark.slow # test_marker--test_with_duplicate_marks
23+
@pytest.mark.slow
24+
def test_with_duplicate_marks():
25+
assert True
26+
27+
28+
@pytest.mark.parametrize("x", [1, 2]) # test_marker--test_parametrize_with_mark
29+
@pytest.mark.slow
30+
def test_parametrize_with_mark(x):
31+
assert x > 0

python_files/tests/pytestadapter/expected_discovery_test_output.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1776,6 +1776,149 @@
17761776
black_formatter_folder_path = TEST_DATA_PATH / "2496-black-formatter"
17771777
black_app_path = black_formatter_folder_path / "app.py"
17781778
black_test_app_path = black_formatter_folder_path / "test_app.py"
1779+
# This is the expected output for the test_marks.py file.
1780+
# └── test_marks.py
1781+
# └── test_with_single_mark (tags: ["slow"])
1782+
# └── test_with_multiple_marks (tags: ["slow", "integration"])
1783+
# └── test_with_no_marks (tags: [])
1784+
# └── test_with_duplicate_marks (tags: ["slow"])
1785+
# └── test_parametrize_with_mark (function)
1786+
# └── [1] (tags: ["slow"])
1787+
# └── [2] (tags: ["slow"])
1788+
marks_test_file_path = TEST_DATA_PATH / "test_marks.py"
1789+
marks_test_expected_output = {
1790+
"name": ".data",
1791+
"path": TEST_DATA_PATH_STR,
1792+
"type_": "folder",
1793+
"children": [
1794+
{
1795+
"name": "test_marks.py",
1796+
"path": os.fspath(marks_test_file_path),
1797+
"type_": "file",
1798+
"id_": os.fspath(marks_test_file_path),
1799+
"children": [
1800+
{
1801+
"name": "test_with_single_mark",
1802+
"path": os.fspath(marks_test_file_path),
1803+
"lineno": find_test_line_number(
1804+
"test_with_single_mark",
1805+
marks_test_file_path,
1806+
),
1807+
"type_": "test",
1808+
"id_": get_absolute_test_id(
1809+
"test_marks.py::test_with_single_mark",
1810+
marks_test_file_path,
1811+
),
1812+
"runID": get_absolute_test_id(
1813+
"test_marks.py::test_with_single_mark",
1814+
marks_test_file_path,
1815+
),
1816+
"tags": ["slow"],
1817+
},
1818+
{
1819+
"name": "test_with_multiple_marks",
1820+
"path": os.fspath(marks_test_file_path),
1821+
"lineno": find_test_line_number(
1822+
"test_with_multiple_marks",
1823+
marks_test_file_path,
1824+
),
1825+
"type_": "test",
1826+
"id_": get_absolute_test_id(
1827+
"test_marks.py::test_with_multiple_marks",
1828+
marks_test_file_path,
1829+
),
1830+
"runID": get_absolute_test_id(
1831+
"test_marks.py::test_with_multiple_marks",
1832+
marks_test_file_path,
1833+
),
1834+
"tags": ["integration", "slow"],
1835+
},
1836+
{
1837+
"name": "test_with_no_marks",
1838+
"path": os.fspath(marks_test_file_path),
1839+
"lineno": find_test_line_number(
1840+
"test_with_no_marks",
1841+
marks_test_file_path,
1842+
),
1843+
"type_": "test",
1844+
"id_": get_absolute_test_id(
1845+
"test_marks.py::test_with_no_marks",
1846+
marks_test_file_path,
1847+
),
1848+
"runID": get_absolute_test_id(
1849+
"test_marks.py::test_with_no_marks",
1850+
marks_test_file_path,
1851+
),
1852+
"tags": [],
1853+
},
1854+
{
1855+
"name": "test_with_duplicate_marks",
1856+
"path": os.fspath(marks_test_file_path),
1857+
"lineno": find_test_line_number(
1858+
"test_with_duplicate_marks",
1859+
marks_test_file_path,
1860+
),
1861+
"type_": "test",
1862+
"id_": get_absolute_test_id(
1863+
"test_marks.py::test_with_duplicate_marks",
1864+
marks_test_file_path,
1865+
),
1866+
"runID": get_absolute_test_id(
1867+
"test_marks.py::test_with_duplicate_marks",
1868+
marks_test_file_path,
1869+
),
1870+
"tags": ["slow"],
1871+
},
1872+
{
1873+
"name": "test_parametrize_with_mark",
1874+
"path": os.fspath(marks_test_file_path),
1875+
"type_": "function",
1876+
"id_": os.fspath(marks_test_file_path) + "::test_parametrize_with_mark",
1877+
"children": [
1878+
{
1879+
"name": "[1]",
1880+
"path": os.fspath(marks_test_file_path),
1881+
"lineno": find_test_line_number(
1882+
"test_parametrize_with_mark",
1883+
marks_test_file_path,
1884+
),
1885+
"type_": "test",
1886+
"id_": get_absolute_test_id(
1887+
"test_marks.py::test_parametrize_with_mark[1]",
1888+
marks_test_file_path,
1889+
),
1890+
"runID": get_absolute_test_id(
1891+
"test_marks.py::test_parametrize_with_mark[1]",
1892+
marks_test_file_path,
1893+
),
1894+
"tags": ["slow"],
1895+
},
1896+
{
1897+
"name": "[2]",
1898+
"path": os.fspath(marks_test_file_path),
1899+
"lineno": find_test_line_number(
1900+
"test_parametrize_with_mark",
1901+
marks_test_file_path,
1902+
),
1903+
"type_": "test",
1904+
"id_": get_absolute_test_id(
1905+
"test_marks.py::test_parametrize_with_mark[2]",
1906+
marks_test_file_path,
1907+
),
1908+
"runID": get_absolute_test_id(
1909+
"test_marks.py::test_parametrize_with_mark[2]",
1910+
marks_test_file_path,
1911+
),
1912+
"tags": ["slow"],
1913+
},
1914+
],
1915+
},
1916+
],
1917+
}
1918+
],
1919+
"id_": TEST_DATA_PATH_STR,
1920+
}
1921+
17791922
black_formatter_expected_output = {
17801923
"name": ".data",
17811924
"path": TEST_DATA_PATH_STR,

python_files/tests/pytestadapter/test_discovery.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,39 @@ def test_pytest_collect(file, expected_const):
208208
)
209209

210210

211+
def test_pytest_marks_as_tags():
212+
"""Test that pytest marks are extracted as tags during discovery.
213+
214+
Verifies that:
215+
- Single marks produce a single tag
216+
- Multiple marks produce multiple tags
217+
- Duplicate marks are deduplicated
218+
- @pytest.mark.parametrize is excluded from tags
219+
- Tests with no marks have an empty tags list
220+
"""
221+
actual = helpers.runner(
222+
[
223+
os.fspath(helpers.TEST_DATA_PATH / "test_marks.py"),
224+
"--collect-only",
225+
]
226+
)
227+
228+
assert actual
229+
actual_list: List[Dict[str, Any]] = actual
230+
actual_item = actual_list.pop(0)
231+
assert actual_item.get("status") == "success", (
232+
f"Status is not 'success', error is: {actual_item.get('error')}"
233+
)
234+
assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH)
235+
assert is_same_tree(
236+
actual_item.get("tests"),
237+
expected_discovery_test_output.marks_test_expected_output,
238+
["id_", "lineno", "name", "runID", "tags"],
239+
), (
240+
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)}"
241+
)
242+
243+
211244
@pytest.mark.skipif(
212245
sys.platform == "win32",
213246
reason="See https://stackoverflow.com/questions/32877260/privlege-error-trying-to-create-symlink-using-python-on-windows-10",

src/test/testing/testController/utils.unit.test.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,142 @@ suite('populateTestTree tests', () => {
625625
assert.deepStrictEqual(mockNestedNode.tags, [RunTestTag, DebugTestTag]);
626626
assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]);
627627
});
628+
629+
test('should add pytest mark tags with mark. prefix to test items', () => {
630+
// Arrange
631+
const testItem: DiscoveredTestItem = {
632+
path: '/test/path/test.py',
633+
name: 'test_marked',
634+
type_: 'test',
635+
id_: 'test-marked-id',
636+
lineno: 5,
637+
runID: 'run-marked',
638+
tags: ['slow', 'integration'],
639+
};
640+
641+
const testTreeData: DiscoveredTestNode = {
642+
path: '/test/path/root',
643+
name: 'RootTest',
644+
type_: 'folder',
645+
id_: 'root-id',
646+
children: [testItem],
647+
};
648+
649+
const mockRootItem: TestItem = {
650+
id: 'root-id',
651+
tags: [],
652+
canResolveChildren: true,
653+
children: { add: sandbox.stub() },
654+
} as any;
655+
656+
const mockTestItem: TestItem = {
657+
id: 'test-marked-id',
658+
tags: [],
659+
canResolveChildren: false,
660+
} as any;
661+
662+
createTestItemStub.onCall(0).returns(mockRootItem);
663+
createTestItemStub.onCall(1).returns(mockTestItem);
664+
665+
// Act
666+
populateTestTree(testController, testTreeData, undefined, resultResolver, cancelationToken);
667+
668+
// Assert
669+
assert.deepStrictEqual(mockRootItem.tags, [RunTestTag, DebugTestTag]);
670+
assert.deepStrictEqual(mockTestItem.tags, [
671+
RunTestTag,
672+
DebugTestTag,
673+
{ id: 'mark.slow' },
674+
{ id: 'mark.integration' },
675+
]);
676+
});
677+
678+
test('should handle test items with empty tags array', () => {
679+
// Arrange
680+
const testItem: DiscoveredTestItem = {
681+
path: '/test/path/test.py',
682+
name: 'test_no_tags',
683+
type_: 'test',
684+
id_: 'test-no-tags-id',
685+
lineno: 5,
686+
runID: 'run-no-tags',
687+
tags: [],
688+
};
689+
690+
const testTreeData: DiscoveredTestNode = {
691+
path: '/test/path/root',
692+
name: 'RootTest',
693+
type_: 'folder',
694+
id_: 'root-id',
695+
children: [testItem],
696+
};
697+
698+
const mockRootItem: TestItem = {
699+
id: 'root-id',
700+
tags: [],
701+
canResolveChildren: true,
702+
children: { add: sandbox.stub() },
703+
} as any;
704+
705+
const mockTestItem: TestItem = {
706+
id: 'test-no-tags-id',
707+
tags: [],
708+
canResolveChildren: false,
709+
} as any;
710+
711+
createTestItemStub.onCall(0).returns(mockRootItem);
712+
createTestItemStub.onCall(1).returns(mockTestItem);
713+
714+
// Act
715+
populateTestTree(testController, testTreeData, undefined, resultResolver, cancelationToken);
716+
717+
// Assert
718+
assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]);
719+
});
720+
721+
test('should handle test items with undefined tags', () => {
722+
// Arrange
723+
const testItem: DiscoveredTestItem = {
724+
path: '/test/path/test.py',
725+
name: 'test_undef_tags',
726+
type_: 'test',
727+
id_: 'test-undef-id',
728+
lineno: 5,
729+
runID: 'run-undef',
730+
// tags intentionally omitted
731+
};
732+
733+
const testTreeData: DiscoveredTestNode = {
734+
path: '/test/path/root',
735+
name: 'RootTest',
736+
type_: 'folder',
737+
id_: 'root-id',
738+
children: [testItem],
739+
};
740+
741+
const mockRootItem: TestItem = {
742+
id: 'root-id',
743+
tags: [],
744+
canResolveChildren: true,
745+
children: { add: sandbox.stub() },
746+
} as any;
747+
748+
const mockTestItem: TestItem = {
749+
id: 'test-undef-id',
750+
tags: [],
751+
canResolveChildren: false,
752+
} as any;
753+
754+
createTestItemStub.onCall(0).returns(mockRootItem);
755+
createTestItemStub.onCall(1).returns(mockTestItem);
756+
757+
// Act
758+
populateTestTree(testController, testTreeData, undefined, resultResolver, cancelationToken);
759+
760+
// Assert
761+
assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]);
762+
});
763+
628764
test('should handle a test node with no lineno property', () => {
629765
// Arrange
630766
// Tree structure:

0 commit comments

Comments
 (0)