Skip to content

Commit e9fb2bf

Browse files
Django Test Runs with Coverage (#24927)
#24199 Co-authored-by: Danila Grobov (s4642g) <[email protected]>
1 parent 70a36f5 commit e9fb2bf

File tree

4 files changed

+79
-24
lines changed

4 files changed

+79
-24
lines changed

build/test-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ django-stubs
3232
coverage
3333
pytest-cov
3434
pytest-json
35+
pytest-timeout
3536

3637

3738
# for pytest-describe related tests

python_files/tests/pytestadapter/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ def runner_with_cwd_env(
244244
"""
245245
process_args: List[str]
246246
pipe_name: str
247-
if "MANAGE_PY_PATH" in env_add:
247+
if "MANAGE_PY_PATH" in env_add and "COVERAGE_ENABLED" not in env_add:
248248
# If we are running Django, generate a unittest-specific pipe name.
249249
process_args = [sys.executable, *args]
250250
pipe_name = generate_random_pipe_name("unittest-discovery-test")

python_files/tests/unittestadapter/test_coverage.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import pathlib
99
import sys
1010

11+
import pytest
12+
1113
sys.path.append(os.fspath(pathlib.Path(__file__).parent))
1214

1315
python_files_path = pathlib.Path(__file__).parent.parent.parent
@@ -49,3 +51,41 @@ def test_basic_coverage():
4951
assert focal_function_coverage.get("lines_missed") is not None
5052
assert set(focal_function_coverage.get("lines_covered")) == {4, 5, 7, 9, 10, 11, 12, 13, 14}
5153
assert set(focal_function_coverage.get("lines_missed")) == {6}
54+
55+
56+
@pytest.mark.timeout(30)
57+
def test_basic_django_coverage():
58+
"""This test validates that the coverage is correctly calculated for a Django project."""
59+
data_path: pathlib.Path = TEST_DATA_PATH / "simple_django"
60+
manage_py_path: str = os.fsdecode(data_path / "manage.py")
61+
execution_script: pathlib.Path = python_files_path / "unittestadapter" / "execution.py"
62+
63+
test_ids = [
64+
"polls.tests.QuestionModelTests.test_was_published_recently_with_future_question",
65+
"polls.tests.QuestionModelTests.test_was_published_recently_with_future_question_2",
66+
"polls.tests.QuestionModelTests.test_question_creation_and_retrieval",
67+
]
68+
69+
script_str = os.fsdecode(execution_script)
70+
actual = helpers.runner_with_cwd_env(
71+
[script_str, "--udiscovery", "-p", "*test*.py", *test_ids],
72+
data_path,
73+
{
74+
"MANAGE_PY_PATH": manage_py_path,
75+
"_TEST_VAR_UNITTEST": "True",
76+
"COVERAGE_ENABLED": os.fspath(data_path),
77+
},
78+
)
79+
80+
assert actual
81+
coverage = actual[-1]
82+
assert coverage
83+
results = coverage["result"]
84+
assert results
85+
assert len(results) == 15
86+
polls_views_coverage = results.get(str(data_path / "polls" / "views.py"))
87+
assert polls_views_coverage
88+
assert polls_views_coverage.get("lines_covered") is not None
89+
assert polls_views_coverage.get("lines_missed") is not None
90+
assert set(polls_views_coverage.get("lines_covered")) == {3, 4, 6}
91+
assert set(polls_views_coverage.get("lines_missed")) == {7}

python_files/unittestadapter/django_handler.py

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
33

4+
import importlib.util
45
import os
56
import pathlib
67
import subprocess
78
import sys
8-
from typing import List
9+
from contextlib import contextmanager, suppress
10+
from typing import TYPE_CHECKING, Generator, List
11+
12+
if TYPE_CHECKING:
13+
from importlib.machinery import ModuleSpec
14+
915

1016
script_dir = pathlib.Path(__file__).parent
1117
sys.path.append(os.fspath(script_dir))
@@ -16,6 +22,17 @@
1622
)
1723

1824

25+
@contextmanager
26+
def override_argv(argv: List[str]) -> Generator:
27+
"""Context manager to temporarily override sys.argv with the provided arguments."""
28+
original_argv = sys.argv
29+
sys.argv = argv
30+
try:
31+
yield
32+
finally:
33+
sys.argv = original_argv
34+
35+
1936
def django_discovery_runner(manage_py_path: str, args: List[str]) -> None:
2037
# Attempt a small amount of validation on the manage.py path.
2138
if not pathlib.Path(manage_py_path).exists():
@@ -72,31 +89,28 @@ def django_execution_runner(manage_py_path: str, test_ids: List[str], args: List
7289
else:
7390
env["PYTHONPATH"] = os.fspath(custom_test_runner_dir)
7491

75-
# Build command to run 'python manage.py test'.
76-
command: List[str] = [
77-
sys.executable,
92+
django_project_dir: pathlib.Path = pathlib.Path(manage_py_path).parent
93+
sys.path.insert(0, os.fspath(django_project_dir))
94+
print(f"Django project directory: {django_project_dir}")
95+
96+
manage_spec: ModuleSpec | None = importlib.util.spec_from_file_location(
97+
"manage", manage_py_path
98+
)
99+
if manage_spec is None or manage_spec.loader is None:
100+
raise VSCodeUnittestError("Error importing manage.py when running Django testing.")
101+
manage_module = importlib.util.module_from_spec(manage_spec)
102+
manage_spec.loader.exec_module(manage_module)
103+
104+
manage_argv: List[str] = [
78105
manage_py_path,
79106
"test",
80107
"--testrunner=django_test_runner.CustomExecutionTestRunner",
108+
*args,
109+
*test_ids,
81110
]
82-
# Add any additional arguments to the command provided by the user.
83-
command.extend(args)
84-
# Add the test_ids to the command.
85-
print("Test IDs: ", test_ids)
86-
print("args: ", args)
87-
command.extend(test_ids)
88-
print("Running Django run tests with command: ", command)
89-
subprocess_execution = subprocess.run(
90-
command,
91-
capture_output=True,
92-
text=True,
93-
env=env,
94-
)
95-
print(subprocess_execution.stderr, file=sys.stderr)
96-
print(subprocess_execution.stdout, file=sys.stdout)
97-
# Zero return code indicates success, 1 indicates test failures, so both are considered successful.
98-
if subprocess_execution.returncode not in (0, 1):
99-
error_msg = "Django test execution process exited with non-zero error code See stderr above for more details."
100-
print(error_msg, file=sys.stderr)
111+
print(f"Django manage.py arguments: {manage_argv}")
112+
113+
with override_argv(manage_argv), suppress(SystemExit):
114+
manage_module.main()
101115
except Exception as e:
102116
print(f"Error during Django test execution: {e}", file=sys.stderr)

0 commit comments

Comments
 (0)