Skip to content

Commit cf2e468

Browse files
Copiloteleanorjboyd
andcommitted
Add lineno to class nodes for pytest discovery
- Modified create_class_node() to extract and include line number using inspect.getsourcelines() - Updated TypeScript types to allow optional lineno on DiscoveredTestNode - Modified populateTestTree() to handle lineno for class nodes (not just test items) - Added find_class_line_number() helper function for test expectations - Updated all test expectations to include lineno for class nodes - Added 'function' to DiscoveredTestType enum This enables TestClass items to show the green arrow and be runnable in VS Code's Test Explorer. Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com>
1 parent ea372d8 commit cf2e468

File tree

5 files changed

+107
-2
lines changed

5 files changed

+107
-2
lines changed

python_files/tests/pytestadapter/expected_discovery_test_output.py

Lines changed: 57 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,9 @@
95100
"unittest_pytest_same_file.py::TestExample",
96101
unit_pytest_same_file_path,
97102
),
103+
"lineno": find_class_line_number(
104+
"TestExample", unit_pytest_same_file_path
105+
),
98106
},
99107
{
100108
"name": "test_true_pytest",
@@ -207,6 +215,9 @@
207215
"unittest_folder/test_add.py::TestAddFunction",
208216
test_add_path,
209217
),
218+
"lineno": find_class_line_number(
219+
"TestAddFunction", test_add_path
220+
),
210221
},
211222
{
212223
"name": "TestDuplicateFunction",
@@ -235,6 +246,9 @@
235246
"unittest_folder/test_add.py::TestDuplicateFunction",
236247
test_add_path,
237248
),
249+
"lineno": find_class_line_number(
250+
"TestDuplicateFunction", test_add_path
251+
),
238252
},
239253
],
240254
},
@@ -288,6 +302,9 @@
288302
"unittest_folder/test_subtract.py::TestSubtractFunction",
289303
test_subtract_path,
290304
),
305+
"lineno": find_class_line_number(
306+
"TestSubtractFunction", test_subtract_path
307+
),
291308
},
292309
{
293310
"name": "TestDuplicateFunction",
@@ -316,6 +333,9 @@
316333
"unittest_folder/test_subtract.py::TestDuplicateFunction",
317334
test_subtract_path,
318335
),
336+
"lineno": find_class_line_number(
337+
"TestDuplicateFunction", test_subtract_path
338+
),
319339
},
320340
],
321341
},
@@ -553,6 +573,9 @@
553573
"parametrize_tests.py::TestClass",
554574
parameterize_tests_path,
555575
),
576+
"lineno": find_class_line_number(
577+
"TestClass", parameterize_tests_path
578+
),
556579
"children": [
557580
{
558581
"name": "test_adding",
@@ -929,6 +952,9 @@
929952
"test_multi_class_nest.py::TestFirstClass",
930953
TEST_MULTI_CLASS_NEST_PATH,
931954
),
955+
"lineno": find_class_line_number(
956+
"TestFirstClass", TEST_MULTI_CLASS_NEST_PATH
957+
),
932958
"children": [
933959
{
934960
"name": "TestSecondClass",
@@ -938,6 +964,9 @@
938964
"test_multi_class_nest.py::TestFirstClass::TestSecondClass",
939965
TEST_MULTI_CLASS_NEST_PATH,
940966
),
967+
"lineno": find_class_line_number(
968+
"TestSecondClass", TEST_MULTI_CLASS_NEST_PATH
969+
),
941970
"children": [
942971
{
943972
"name": "test_second",
@@ -982,6 +1011,9 @@
9821011
"test_multi_class_nest.py::TestFirstClass::TestSecondClass2",
9831012
TEST_MULTI_CLASS_NEST_PATH,
9841013
),
1014+
"lineno": find_class_line_number(
1015+
"TestSecondClass2", TEST_MULTI_CLASS_NEST_PATH
1016+
),
9851017
"children": [
9861018
{
9871019
"name": "test_second2",
@@ -1227,6 +1259,9 @@
12271259
"same_function_new_class_param.py::TestNotEmpty",
12281260
TEST_DATA_PATH / "same_function_new_class_param.py",
12291261
),
1262+
"lineno": find_class_line_number(
1263+
"TestNotEmpty", TEST_DATA_PATH / "same_function_new_class_param.py"
1264+
),
12301265
},
12311266
{
12321267
"name": "TestEmpty",
@@ -1298,6 +1333,9 @@
12981333
"same_function_new_class_param.py::TestEmpty",
12991334
TEST_DATA_PATH / "same_function_new_class_param.py",
13001335
),
1336+
"lineno": find_class_line_number(
1337+
"TestEmpty", TEST_DATA_PATH / "same_function_new_class_param.py"
1338+
),
13011339
},
13021340
],
13031341
}
@@ -1371,6 +1409,9 @@
13711409
"test_param_span_class.py::TestClass1",
13721410
TEST_DATA_PATH / "test_param_span_class.py",
13731411
),
1412+
"lineno": find_class_line_number(
1413+
"TestClass1", TEST_DATA_PATH / "test_param_span_class.py"
1414+
),
13741415
},
13751416
{
13761417
"name": "TestClass2",
@@ -1427,6 +1468,9 @@
14271468
"test_param_span_class.py::TestClass2",
14281469
TEST_DATA_PATH / "test_param_span_class.py",
14291470
),
1471+
"lineno": find_class_line_number(
1472+
"TestClass2", TEST_DATA_PATH / "test_param_span_class.py"
1473+
),
14301474
},
14311475
],
14321476
}
@@ -1503,6 +1547,9 @@
15031547
"pytest_describe_plugin/describe_only.py::describe_A",
15041548
describe_only_path,
15051549
),
1550+
"lineno": find_class_line_number(
1551+
"describe_A", describe_only_path
1552+
),
15061553
}
15071554
],
15081555
}
@@ -1586,6 +1633,9 @@
15861633
"pytest_describe_plugin/nested_describe.py::describe_list::describe_append",
15871634
nested_describe_path,
15881635
),
1636+
"lineno": find_class_line_number(
1637+
"describe_append", nested_describe_path
1638+
),
15891639
},
15901640
{
15911641
"name": "describe_remove",
@@ -1614,12 +1664,18 @@
16141664
"pytest_describe_plugin/nested_describe.py::describe_list::describe_remove",
16151665
nested_describe_path,
16161666
),
1667+
"lineno": find_class_line_number(
1668+
"describe_remove", nested_describe_path
1669+
),
16171670
},
16181671
],
16191672
"id_": get_absolute_test_id(
16201673
"pytest_describe_plugin/nested_describe.py::describe_list",
16211674
nested_describe_path,
16221675
),
1676+
"lineno": find_class_line_number(
1677+
"describe_list", nested_describe_path
1678+
),
16231679
}
16241680
],
16251681
}

python_files/tests/pytestadapter/helpers.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,26 @@ 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 line.strip().startswith(f"class {class_name}") or line.strip().startswith(
386+
f"class {class_name}("
387+
) or line.strip().startswith(f"def {class_name}("):
388+
return str(i + 1)
389+
error_str: str = f"Class {class_name!r} not found on any line in {test_file_path}"
390+
raise ValueError(error_str)
391+
392+
373393
def get_absolute_test_id(test_id: str, test_path: pathlib.Path) -> str:
374394
"""Get the absolute test id by joining the testPath with the test_id."""
375395
split_id = test_id.split("::")[1:]

python_files/vscode_pytest/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,12 +830,25 @@ def create_class_node(class_module: pytest.Class | DescribeBlock) -> TestNode:
830830
Keyword arguments:
831831
class_module -- the pytest object representing a class module.
832832
"""
833+
# Get line number for the class definition
834+
class_line = ""
835+
try:
836+
if hasattr(class_module, "obj"):
837+
import inspect
838+
839+
_, lineno = inspect.getsourcelines(class_module.obj)
840+
class_line = str(lineno)
841+
except (OSError, TypeError):
842+
# If we can't get the source lines, leave lineno empty
843+
pass
844+
833845
return {
834846
"name": class_module.name,
835847
"path": get_node_path(class_module),
836848
"type_": "class",
837849
"children": [],
838850
"id_": get_absolute_test_id(class_module.nodeid, get_node_path(class_module)),
851+
"lineno": class_line,
839852
}
840853

841854

src/client/testing/testController/common/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export interface ITestExecutionAdapter {
177177
}
178178

179179
// Same types as in python_files/unittestadapter/utils.py
180-
export type DiscoveredTestType = 'folder' | 'file' | 'class' | 'test';
180+
export type DiscoveredTestType = 'folder' | 'file' | 'class' | 'function' | 'test';
181181

182182
export type DiscoveredTestCommon = {
183183
path: string;
@@ -194,6 +194,7 @@ export type DiscoveredTestItem = DiscoveredTestCommon & {
194194

195195
export type DiscoveredTestNode = DiscoveredTestCommon & {
196196
children: (DiscoveredTestNode | DiscoveredTestItem)[];
197+
lineno?: number | string;
197198
};
198199

199200
export type DiscoveredTestPayload = {

src/client/testing/testController/common/utils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,21 @@ export function populateTestTree(
257257

258258
node.canResolveChildren = true;
259259
node.tags = [RunTestTag, DebugTestTag];
260+
261+
// Set range for class nodes (and other nodes) if lineno is available
262+
let range: Range | undefined;
263+
if ('lineno' in child && child.lineno) {
264+
if (Number(child.lineno) === 0) {
265+
range = new Range(new Position(0, 0), new Position(0, 0));
266+
} else {
267+
range = new Range(
268+
new Position(Number(child.lineno) - 1, 0),
269+
new Position(Number(child.lineno), 0),
270+
);
271+
}
272+
node.range = range;
273+
}
274+
260275
testRoot!.children.add(node);
261276
}
262277
populateTestTree(testController, child, node, resultResolver, token);

0 commit comments

Comments
 (0)