Skip to content

Commit 7edd6e4

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 62ed3db commit 7edd6e4

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

0 commit comments

Comments
 (0)