Skip to content

Commit 0b26b21

Browse files
authored
Merge branch 'main' into patch-1
2 parents c7a00c9 + 8989323 commit 0b26b21

File tree

15 files changed

+348
-9
lines changed

15 files changed

+348
-9
lines changed

.github/instructions/python-quality-checks.instructions.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,29 @@ nox --session install_python_libs
6969

7070
**Type errors in ignored files**: Legacy files in `pyproject.toml` ignore list—fix if working on them
7171

72+
## When Writing Tests
73+
74+
**Always format your test files before committing:**
75+
76+
```bash
77+
cd python_files
78+
ruff format tests/ # Format all test files
79+
# or format specific files:
80+
ruff format tests/unittestadapter/test_utils.py
81+
```
82+
83+
**Best practice workflow:**
84+
85+
1. Write your test code
86+
2. Run `ruff format` on the test files
87+
3. Run the tests to verify they pass
88+
4. Run `npm run check-python` to catch any remaining issues
89+
90+
This ensures your tests pass both functional checks and quality checks in CI.
91+
7292
## Learnings
7393

7494
- Always run `npm run check-python` before pushing to catch CI failures early (1)
7595
- Use `ruff check . --fix` to auto-fix most linting issues before manual review (1)
7696
- Pyright version must match CI (1.1.308) to avoid inconsistent results between local and CI runs (1)
97+
- Always run `ruff format` on test files after writing them to avoid formatting CI failures (1)

python_files/tests/pytestadapter/expected_discovery_test_output.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import os
22

3-
from .helpers import TEST_DATA_PATH, find_test_line_number, get_absolute_test_id
3+
from .helpers import (
4+
TEST_DATA_PATH,
5+
find_class_line_number,
6+
find_test_line_number,
7+
get_absolute_test_id,
8+
)
49

510
# This file contains the expected output dictionaries for tests discovery and is used in test_discovery.py.
611

@@ -95,6 +100,7 @@
95100
"unittest_pytest_same_file.py::TestExample",
96101
unit_pytest_same_file_path,
97102
),
103+
"lineno": find_class_line_number("TestExample", unit_pytest_same_file_path),
98104
},
99105
{
100106
"name": "test_true_pytest",
@@ -207,6 +213,7 @@
207213
"unittest_folder/test_add.py::TestAddFunction",
208214
test_add_path,
209215
),
216+
"lineno": find_class_line_number("TestAddFunction", test_add_path),
210217
},
211218
{
212219
"name": "TestDuplicateFunction",
@@ -235,6 +242,9 @@
235242
"unittest_folder/test_add.py::TestDuplicateFunction",
236243
test_add_path,
237244
),
245+
"lineno": find_class_line_number(
246+
"TestDuplicateFunction", test_add_path
247+
),
238248
},
239249
],
240250
},
@@ -288,6 +298,9 @@
288298
"unittest_folder/test_subtract.py::TestSubtractFunction",
289299
test_subtract_path,
290300
),
301+
"lineno": find_class_line_number(
302+
"TestSubtractFunction", test_subtract_path
303+
),
291304
},
292305
{
293306
"name": "TestDuplicateFunction",
@@ -316,6 +329,9 @@
316329
"unittest_folder/test_subtract.py::TestDuplicateFunction",
317330
test_subtract_path,
318331
),
332+
"lineno": find_class_line_number(
333+
"TestDuplicateFunction", test_subtract_path
334+
),
319335
},
320336
],
321337
},
@@ -553,6 +569,7 @@
553569
"parametrize_tests.py::TestClass",
554570
parameterize_tests_path,
555571
),
572+
"lineno": find_class_line_number("TestClass", parameterize_tests_path),
556573
"children": [
557574
{
558575
"name": "test_adding",
@@ -929,6 +946,7 @@
929946
"test_multi_class_nest.py::TestFirstClass",
930947
TEST_MULTI_CLASS_NEST_PATH,
931948
),
949+
"lineno": find_class_line_number("TestFirstClass", TEST_MULTI_CLASS_NEST_PATH),
932950
"children": [
933951
{
934952
"name": "TestSecondClass",
@@ -938,6 +956,9 @@
938956
"test_multi_class_nest.py::TestFirstClass::TestSecondClass",
939957
TEST_MULTI_CLASS_NEST_PATH,
940958
),
959+
"lineno": find_class_line_number(
960+
"TestSecondClass", TEST_MULTI_CLASS_NEST_PATH
961+
),
941962
"children": [
942963
{
943964
"name": "test_second",
@@ -982,6 +1003,9 @@
9821003
"test_multi_class_nest.py::TestFirstClass::TestSecondClass2",
9831004
TEST_MULTI_CLASS_NEST_PATH,
9841005
),
1006+
"lineno": find_class_line_number(
1007+
"TestSecondClass2", TEST_MULTI_CLASS_NEST_PATH
1008+
),
9851009
"children": [
9861010
{
9871011
"name": "test_second2",
@@ -1227,6 +1251,9 @@
12271251
"same_function_new_class_param.py::TestNotEmpty",
12281252
TEST_DATA_PATH / "same_function_new_class_param.py",
12291253
),
1254+
"lineno": find_class_line_number(
1255+
"TestNotEmpty", TEST_DATA_PATH / "same_function_new_class_param.py"
1256+
),
12301257
},
12311258
{
12321259
"name": "TestEmpty",
@@ -1298,6 +1325,9 @@
12981325
"same_function_new_class_param.py::TestEmpty",
12991326
TEST_DATA_PATH / "same_function_new_class_param.py",
13001327
),
1328+
"lineno": find_class_line_number(
1329+
"TestEmpty", TEST_DATA_PATH / "same_function_new_class_param.py"
1330+
),
13011331
},
13021332
],
13031333
}
@@ -1371,6 +1401,9 @@
13711401
"test_param_span_class.py::TestClass1",
13721402
TEST_DATA_PATH / "test_param_span_class.py",
13731403
),
1404+
"lineno": find_class_line_number(
1405+
"TestClass1", TEST_DATA_PATH / "test_param_span_class.py"
1406+
),
13741407
},
13751408
{
13761409
"name": "TestClass2",
@@ -1427,6 +1460,9 @@
14271460
"test_param_span_class.py::TestClass2",
14281461
TEST_DATA_PATH / "test_param_span_class.py",
14291462
),
1463+
"lineno": find_class_line_number(
1464+
"TestClass2", TEST_DATA_PATH / "test_param_span_class.py"
1465+
),
14301466
},
14311467
],
14321468
}
@@ -1503,6 +1539,7 @@
15031539
"pytest_describe_plugin/describe_only.py::describe_A",
15041540
describe_only_path,
15051541
),
1542+
"lineno": find_class_line_number("describe_A", describe_only_path),
15061543
}
15071544
],
15081545
}
@@ -1586,6 +1623,9 @@
15861623
"pytest_describe_plugin/nested_describe.py::describe_list::describe_append",
15871624
nested_describe_path,
15881625
),
1626+
"lineno": find_class_line_number(
1627+
"describe_append", nested_describe_path
1628+
),
15891629
},
15901630
{
15911631
"name": "describe_remove",
@@ -1614,12 +1654,16 @@
16141654
"pytest_describe_plugin/nested_describe.py::describe_list::describe_remove",
16151655
nested_describe_path,
16161656
),
1657+
"lineno": find_class_line_number(
1658+
"describe_remove", nested_describe_path
1659+
),
16171660
},
16181661
],
16191662
"id_": get_absolute_test_id(
16201663
"pytest_describe_plugin/nested_describe.py::describe_list",
16211664
nested_describe_path,
16221665
),
1666+
"lineno": find_class_line_number("describe_list", nested_describe_path),
16231667
}
16241668
],
16251669
}

python_files/tests/pytestadapter/helpers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,28 @@ def find_test_line_number(test_name: str, test_file_path) -> str:
370370
raise ValueError(error_str)
371371

372372

373+
def find_class_line_number(class_name: str, test_file_path) -> str:
374+
"""Function which finds the correct line number for a class definition.
375+
376+
Args:
377+
class_name: The name of the class to find the line number for.
378+
test_file_path: The path to the test file where the class is located.
379+
"""
380+
# Look for the class definition line (or function for pytest-describe)
381+
with open(test_file_path) as f: # noqa: PTH123
382+
for i, line in enumerate(f):
383+
# Match "class ClassName" or "class ClassName(" or "class ClassName:"
384+
# Also match "def ClassName(" for pytest-describe blocks
385+
if (
386+
line.strip().startswith(f"class {class_name}")
387+
or line.strip().startswith(f"class {class_name}(")
388+
or line.strip().startswith(f"def {class_name}(")
389+
):
390+
return str(i + 1)
391+
error_str: str = f"Class {class_name!r} not found on any line in {test_file_path}"
392+
raise ValueError(error_str)
393+
394+
373395
def get_absolute_test_id(test_id: str, test_path: pathlib.Path) -> str:
374396
"""Get the absolute test id by joining the testPath with the test_id."""
375397
split_id = test_id.split("::")[1:]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
Patched doctest module.
3+
This module's doctests will be patched to have proper IDs.
4+
5+
>>> 2 + 2
6+
4
7+
"""
8+
9+
10+
def example_function():
11+
"""
12+
Example function with doctest.
13+
14+
>>> example_function()
15+
'works'
16+
"""
17+
return "works"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""
2+
Standard doctest module that should be blocked.
3+
This has a simple doctest with short ID.
4+
5+
>>> 2 + 2
6+
4
7+
"""
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
"""Test file with patched doctest integration that should work."""
4+
5+
import unittest
6+
import doctest
7+
import sys
8+
import doctest_patched_module
9+
10+
11+
# Patch DocTestCase to modify test IDs to be compatible with the extension
12+
original_init = doctest.DocTestCase.__init__
13+
14+
15+
def patched_init(self, test, optionflags=0, setUp=None, tearDown=None, checker=None):
16+
"""Patch to modify doctest names to have proper hierarchy."""
17+
if hasattr(test, 'name'):
18+
# Get module name
19+
module_hierarchy = test.name.split('.')
20+
module_name = module_hierarchy[0] if module_hierarchy else 'unknown'
21+
22+
# Reconstruct with proper formatting to have enough components
23+
# Format: module.file.class.function
24+
if test.filename.endswith('.py'):
25+
file_base = test.filename.split('/')[-1].replace('.py', '')
26+
test_name = test.name.split('.')[-1] if '.' in test.name else test.name
27+
# Create a properly formatted ID with enough components
28+
test.name = f"{module_name}.{file_base}._DocTests.{test_name}"
29+
30+
# Call original init
31+
original_init(self, test, optionflags, setUp, tearDown, checker)
32+
33+
34+
# Apply the patch
35+
doctest.DocTestCase.__init__ = patched_init
36+
37+
38+
def load_tests(loader, tests, ignore):
39+
"""
40+
Standard hook for unittest to load tests.
41+
This uses patched doctest to create compatible test IDs.
42+
"""
43+
tests.addTests(doctest.DocTestSuite(doctest_patched_module))
44+
return tests
45+
46+
47+
# Clean up the patch after loading
48+
def tearDownModule():
49+
"""Restore original DocTestCase.__init__"""
50+
doctest.DocTestCase.__init__ = original_init
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
"""Test file with standard doctest integration that should be blocked."""
4+
5+
import unittest
6+
import doctest
7+
import doctest_standard
8+
9+
10+
def load_tests(loader, tests, ignore):
11+
"""
12+
Standard hook for unittest to load tests.
13+
This uses standard doctest without any patching.
14+
"""
15+
tests.addTests(doctest.DocTestSuite(doctest_standard))
16+
return tests

python_files/tests/unittestadapter/expected_discovery_test_output.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@
99
TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data"
1010

1111

12+
def find_class_line_number(class_name: str, test_file_path) -> str:
13+
"""Function which finds the correct line number for a class definition.
14+
15+
Args:
16+
class_name: The name of the class to find the line number for.
17+
test_file_path: The path to the test file where the class is located.
18+
"""
19+
# Look for the class definition line
20+
with pathlib.Path(test_file_path).open() as f:
21+
for i, line in enumerate(f):
22+
# Match "class ClassName" or "class ClassName(" or "class ClassName:"
23+
if line.strip().startswith(f"class {class_name}") or line.strip().startswith(
24+
f"class {class_name}("
25+
):
26+
return str(i + 1)
27+
error_str: str = f"Class {class_name!r} not found on any line in {test_file_path}"
28+
raise ValueError(error_str)
29+
30+
1231
skip_unittest_folder_discovery_output = {
1332
"path": os.fspath(TEST_DATA_PATH / "unittest_skip"),
1433
"name": "unittest_skip",
@@ -49,6 +68,10 @@
4968
],
5069
"id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py")
5170
+ "\\SimpleTest",
71+
"lineno": find_class_line_number(
72+
"SimpleTest",
73+
TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py",
74+
),
5275
}
5376
],
5477
"id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py"),
@@ -114,6 +137,16 @@
114137
},
115138
],
116139
"id_": complex_tree_file_path + "\\" + "TreeOne",
140+
"lineno": find_class_line_number(
141+
"TreeOne",
142+
pathlib.PurePath(
143+
TEST_DATA_PATH,
144+
"utils_complex_tree",
145+
"test_outer_folder",
146+
"test_inner_folder",
147+
"test_utils_complex_tree.py",
148+
),
149+
),
117150
}
118151
],
119152
"id_": complex_tree_file_path,

0 commit comments

Comments
 (0)