Skip to content
Draft
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
3 changes: 3 additions & 0 deletions build/test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ namedpipe; platform_system == "Windows"
# typing for Django files
django-stubs

# Required for NotRequired import in adapter modules
typing_extensions

coverage
pytest-cov
pytest-json
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import os

from .helpers import TEST_DATA_PATH, find_test_line_number, get_absolute_test_id
from .helpers import (
TEST_DATA_PATH,
find_class_line_number,
find_test_line_number,
get_absolute_test_id,
)

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

Expand Down Expand Up @@ -95,6 +100,7 @@
"unittest_pytest_same_file.py::TestExample",
unit_pytest_same_file_path,
),
"lineno": find_class_line_number("TestExample", unit_pytest_same_file_path),
},
{
"name": "test_true_pytest",
Expand Down Expand Up @@ -207,6 +213,7 @@
"unittest_folder/test_add.py::TestAddFunction",
test_add_path,
),
"lineno": find_class_line_number("TestAddFunction", test_add_path),
},
{
"name": "TestDuplicateFunction",
Expand Down Expand Up @@ -235,6 +242,9 @@
"unittest_folder/test_add.py::TestDuplicateFunction",
test_add_path,
),
"lineno": find_class_line_number(
"TestDuplicateFunction", test_add_path
),
},
],
},
Expand Down Expand Up @@ -288,6 +298,9 @@
"unittest_folder/test_subtract.py::TestSubtractFunction",
test_subtract_path,
),
"lineno": find_class_line_number(
"TestSubtractFunction", test_subtract_path
),
},
{
"name": "TestDuplicateFunction",
Expand Down Expand Up @@ -316,6 +329,9 @@
"unittest_folder/test_subtract.py::TestDuplicateFunction",
test_subtract_path,
),
"lineno": find_class_line_number(
"TestDuplicateFunction", test_subtract_path
),
},
],
},
Expand Down Expand Up @@ -553,6 +569,7 @@
"parametrize_tests.py::TestClass",
parameterize_tests_path,
),
"lineno": find_class_line_number("TestClass", parameterize_tests_path),
"children": [
{
"name": "test_adding",
Expand Down Expand Up @@ -929,6 +946,7 @@
"test_multi_class_nest.py::TestFirstClass",
TEST_MULTI_CLASS_NEST_PATH,
),
"lineno": find_class_line_number("TestFirstClass", TEST_MULTI_CLASS_NEST_PATH),
"children": [
{
"name": "TestSecondClass",
Expand All @@ -938,6 +956,9 @@
"test_multi_class_nest.py::TestFirstClass::TestSecondClass",
TEST_MULTI_CLASS_NEST_PATH,
),
"lineno": find_class_line_number(
"TestSecondClass", TEST_MULTI_CLASS_NEST_PATH
),
"children": [
{
"name": "test_second",
Expand Down Expand Up @@ -982,6 +1003,9 @@
"test_multi_class_nest.py::TestFirstClass::TestSecondClass2",
TEST_MULTI_CLASS_NEST_PATH,
),
"lineno": find_class_line_number(
"TestSecondClass2", TEST_MULTI_CLASS_NEST_PATH
),
"children": [
{
"name": "test_second2",
Expand Down Expand Up @@ -1227,6 +1251,9 @@
"same_function_new_class_param.py::TestNotEmpty",
TEST_DATA_PATH / "same_function_new_class_param.py",
),
"lineno": find_class_line_number(
"TestNotEmpty", TEST_DATA_PATH / "same_function_new_class_param.py"
),
},
{
"name": "TestEmpty",
Expand Down Expand Up @@ -1298,6 +1325,9 @@
"same_function_new_class_param.py::TestEmpty",
TEST_DATA_PATH / "same_function_new_class_param.py",
),
"lineno": find_class_line_number(
"TestEmpty", TEST_DATA_PATH / "same_function_new_class_param.py"
),
},
],
}
Expand Down Expand Up @@ -1371,6 +1401,9 @@
"test_param_span_class.py::TestClass1",
TEST_DATA_PATH / "test_param_span_class.py",
),
"lineno": find_class_line_number(
"TestClass1", TEST_DATA_PATH / "test_param_span_class.py"
),
},
{
"name": "TestClass2",
Expand Down Expand Up @@ -1427,6 +1460,9 @@
"test_param_span_class.py::TestClass2",
TEST_DATA_PATH / "test_param_span_class.py",
),
"lineno": find_class_line_number(
"TestClass2", TEST_DATA_PATH / "test_param_span_class.py"
),
},
],
}
Expand Down Expand Up @@ -1503,6 +1539,7 @@
"pytest_describe_plugin/describe_only.py::describe_A",
describe_only_path,
),
"lineno": find_class_line_number("describe_A", describe_only_path),
}
],
}
Expand Down Expand Up @@ -1586,6 +1623,9 @@
"pytest_describe_plugin/nested_describe.py::describe_list::describe_append",
nested_describe_path,
),
"lineno": find_class_line_number(
"describe_append", nested_describe_path
),
},
{
"name": "describe_remove",
Expand Down Expand Up @@ -1614,12 +1654,16 @@
"pytest_describe_plugin/nested_describe.py::describe_list::describe_remove",
nested_describe_path,
),
"lineno": find_class_line_number(
"describe_remove", nested_describe_path
),
},
],
"id_": get_absolute_test_id(
"pytest_describe_plugin/nested_describe.py::describe_list",
nested_describe_path,
),
"lineno": find_class_line_number("describe_list", nested_describe_path),
}
],
}
Expand Down
22 changes: 22 additions & 0 deletions python_files/tests/pytestadapter/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,28 @@ def find_test_line_number(test_name: str, test_file_path) -> str:
raise ValueError(error_str)


def find_class_line_number(class_name: str, test_file_path) -> str:
"""Function which finds the correct line number for a class definition.

Args:
class_name: The name of the class to find the line number for.
test_file_path: The path to the test file where the class is located.
"""
# Look for the class definition line (or function for pytest-describe)
with open(test_file_path) as f: # noqa: PTH123
for i, line in enumerate(f):
# Match "class ClassName" or "class ClassName(" or "class ClassName:"
# Also match "def ClassName(" for pytest-describe blocks
if (
line.strip().startswith(f"class {class_name}")
or line.strip().startswith(f"class {class_name}(")
or line.strip().startswith(f"def {class_name}(")
):
return str(i + 1)
error_str: str = f"Class {class_name!r} not found on any line in {test_file_path}"
raise ValueError(error_str)


def get_absolute_test_id(test_id: str, test_path: pathlib.Path) -> str:
"""Get the absolute test id by joining the testPath with the test_id."""
split_id = test_id.split("::")[1:]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@
TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data"


def find_class_line_number(class_name: str, test_file_path) -> str:
"""Function which finds the correct line number for a class definition.

Args:
class_name: The name of the class to find the line number for.
test_file_path: The path to the test file where the class is located.
"""
# Look for the class definition line
with pathlib.Path(test_file_path).open() as f:
for i, line in enumerate(f):
# Match "class ClassName" or "class ClassName(" or "class ClassName:"
if line.strip().startswith(f"class {class_name}") or line.strip().startswith(
f"class {class_name}("
):
return str(i + 1)
error_str: str = f"Class {class_name!r} not found on any line in {test_file_path}"
raise ValueError(error_str)


skip_unittest_folder_discovery_output = {
"path": os.fspath(TEST_DATA_PATH / "unittest_skip"),
"name": "unittest_skip",
Expand Down Expand Up @@ -49,6 +68,10 @@
],
"id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py")
+ "\\SimpleTest",
"lineno": find_class_line_number(
"SimpleTest",
TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py",
),
}
],
"id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py"),
Expand Down Expand Up @@ -114,6 +137,16 @@
},
],
"id_": complex_tree_file_path + "\\" + "TreeOne",
"lineno": find_class_line_number(
"TreeOne",
pathlib.PurePath(
TEST_DATA_PATH,
"utils_complex_tree",
"test_outer_folder",
"test_inner_folder",
"test_utils_complex_tree.py",
),
),
}
],
"id_": complex_tree_file_path,
Expand Down
2 changes: 2 additions & 0 deletions python_files/unittestadapter/django_test_runner.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from __future__ import annotations

import os
import pathlib
import sys
Expand Down
19 changes: 19 additions & 0 deletions python_files/unittestadapter/pvsc_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from __future__ import annotations

import argparse
import atexit
import doctest
Expand Down Expand Up @@ -44,6 +46,7 @@ class TestItem(TestData):

class TestNode(TestData):
children: "List[TestNode | TestItem]"
lineno: NotRequired[str] # Optional field for class nodes


class TestExecutionStatus(str, enum.Enum):
Expand Down Expand Up @@ -101,6 +104,16 @@ def get_test_case(suite):
yield from get_test_case(test)


def get_class_line(test_case: unittest.TestCase) -> str | None:
"""Get the line number where a test class is defined."""
try:
test_class = test_case.__class__
_sourcelines, lineno = inspect.getsourcelines(test_class)
return str(lineno)
except Exception:
return None


def get_source_line(obj) -> str:
"""Get the line number of a test case start line."""
try:
Expand Down Expand Up @@ -249,6 +262,12 @@ def build_test_tree(
class_name, file_path, TestNodeTypeEnum.class_, current_node
)

# Add line number to class node if not already present.
if "lineno" not in current_node:
class_lineno = get_class_line(test_case)
if class_lineno is not None:
current_node["lineno"] = class_lineno

# Get test line number.
test_method = getattr(test_case, test_case._testMethodName) # noqa: SLF001
lineno = get_source_line(test_method)
Expand Down
32 changes: 31 additions & 1 deletion python_files/vscode_pytest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,23 @@
import pathlib
import sys
import traceback
from typing import TYPE_CHECKING, Any, Dict, Generator, Literal, Protocol, TypedDict, cast
from typing import (
TYPE_CHECKING,
Any,
Dict,
Generator,
Literal,
Protocol,
TypedDict,
cast,
)

# NotRequired is only available in typing from Python 3.11+
# For earlier versions, use typing_extensions
try:
from typing import NotRequired

Check failure on line 27 in python_files/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

"NotRequired" is unknown import symbol (reportGeneralTypeIssues)

Check failure on line 27 in python_files/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

"NotRequired" is unknown import symbol (reportGeneralTypeIssues)
except ImportError:
from typing_extensions import NotRequired

import pytest

Expand Down Expand Up @@ -52,6 +68,7 @@
"""A general class that handles all test data which contains children."""

children: list[TestNode | TestItem | None]
lineno: NotRequired[str] # Optional field for class/function nodes


class VSCodePytestError(Exception):
Expand Down Expand Up @@ -398,7 +415,7 @@

if IS_DISCOVERY:
if not (exitstatus == 0 or exitstatus == 1 or exitstatus == 5):
error_node: TestNode = {

Check failure on line 418 in python_files/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

Expression of type "dict[str, str | Path | list[TestNode | TestItem | None]]" cannot be assigned to declared type "TestNode"   "lineno" is required in "TestNode" (reportGeneralTypeIssues)

Check failure on line 418 in python_files/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

Expression of type "dict[str, str | Path | list[TestNode | TestItem | None]]" cannot be assigned to declared type "TestNode"   "lineno" is required in "TestNode" (reportGeneralTypeIssues)
"name": "",
"path": cwd,
"type_": "error",
Expand All @@ -418,7 +435,7 @@
ERRORS.append(
f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}"
)
error_node: TestNode = {

Check failure on line 438 in python_files/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

Expression of type "dict[str, str | Path | list[TestNode | TestItem | None]]" cannot be assigned to declared type "TestNode"   "lineno" is required in "TestNode" (reportGeneralTypeIssues)

Check failure on line 438 in python_files/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

Expression of type "dict[str, str | Path | list[TestNode | TestItem | None]]" cannot be assigned to declared type "TestNode"   "lineno" is required in "TestNode" (reportGeneralTypeIssues)
"name": "",
"path": cwd,
"type_": "error",
Expand Down Expand Up @@ -815,7 +832,7 @@
session -- the pytest session.
"""
node_path = get_node_path(session)
return {

Check failure on line 835 in python_files/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

Expression of type "dict[str, str | Path | list[TestNode | TestItem | None]]" cannot be assigned to return type "TestNode"   "lineno" is required in "TestNode" (reportGeneralTypeIssues)

Check failure on line 835 in python_files/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

Expression of type "dict[str, str | Path | list[TestNode | TestItem | None]]" cannot be assigned to return type "TestNode"   "lineno" is required in "TestNode" (reportGeneralTypeIssues)
"name": node_path.name,
"path": node_path,
"type_": "folder",
Expand All @@ -830,12 +847,25 @@
Keyword arguments:
class_module -- the pytest object representing a class module.
"""
# Get line number for the class definition
class_line = ""
try:
if hasattr(class_module, "obj"):
import inspect

_, lineno = inspect.getsourcelines(class_module.obj)
class_line = str(lineno)
except (OSError, TypeError):
# If we can't get the source lines, leave lineno empty
pass

return {
"name": class_module.name,
"path": get_node_path(class_module),
"type_": "class",
"children": [],
"id_": get_absolute_test_id(class_module.nodeid, get_node_path(class_module)),
"lineno": class_line,
}


Expand All @@ -850,7 +880,7 @@
function_id -- the previously constructed function id that fits the pattern- absolute path :: any class and method :: parent_part
must be edited to get a unique id for the function node.
"""
return {

Check failure on line 883 in python_files/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

Expression of type "dict[str, str | Path | list[TestNode | TestItem | None]]" cannot be assigned to return type "TestNode"   "lineno" is required in "TestNode" (reportGeneralTypeIssues)

Check failure on line 883 in python_files/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

Expression of type "dict[str, str | Path | list[TestNode | TestItem | None]]" cannot be assigned to return type "TestNode"   "lineno" is required in "TestNode" (reportGeneralTypeIssues)
"name": function_name,
"path": test_path,
"type_": "function",
Expand All @@ -865,7 +895,7 @@
Keyword arguments:
calculated_node_path -- the pytest file path.
"""
return {

Check failure on line 898 in python_files/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

Expression of type "dict[str, str | Path | list[TestNode | TestItem | None]]" cannot be assigned to return type "TestNode"   "lineno" is required in "TestNode" (reportGeneralTypeIssues)

Check failure on line 898 in python_files/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

Expression of type "dict[str, str | Path | list[TestNode | TestItem | None]]" cannot be assigned to return type "TestNode"   "lineno" is required in "TestNode" (reportGeneralTypeIssues)
"name": calculated_node_path.name,
"path": calculated_node_path,
"type_": "file",
Expand All @@ -881,7 +911,7 @@
folderName -- the name of the folder.
path_iterator -- the path of the folder.
"""
return {

Check failure on line 914 in python_files/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

Expression of type "dict[str, str | Path | list[TestNode | TestItem | None]]" cannot be assigned to return type "TestNode"   "lineno" is required in "TestNode" (reportGeneralTypeIssues)

Check failure on line 914 in python_files/vscode_pytest/__init__.py

View workflow job for this annotation

GitHub Actions / Check Python types

Expression of type "dict[str, str | Path | list[TestNode | TestItem | None]]" cannot be assigned to return type "TestNode"   "lineno" is required in "TestNode" (reportGeneralTypeIssues)
"name": folder_name,
"path": path_iterator,
"type_": "folder",
Expand Down
3 changes: 2 additions & 1 deletion src/client/testing/testController/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export interface ITestExecutionAdapter {
}

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

export type DiscoveredTestCommon = {
path: string;
Expand All @@ -194,6 +194,7 @@ export type DiscoveredTestItem = DiscoveredTestCommon & {

export type DiscoveredTestNode = DiscoveredTestCommon & {
children: (DiscoveredTestNode | DiscoveredTestItem)[];
lineno?: number | string;
};

export type DiscoveredTestPayload = {
Expand Down
Loading
Loading