mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 00:07:03 +01:00
_challenge_support.py 402 -> 237. _pytest_clean and _patch_and_test go to _pytest_runner.py with the subprocess/os/sys imports they solely used; _stream_verdict and _parse_verdict go to _verdict.py with rich's Live. code_tutor patches by absolute string rather than a MOD constant, so the seam cost 15 per-name rewrites instead of one sed. Two intermediate failures caught by running it: the test modules import the moved names directly, and five patch strings still named the old module. Pass count is 216 before and after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
112 lines
3.2 KiB
Python
112 lines
3.2 KiB
Python
"""Running the user's tests in an isolated pytest subprocess.
|
|
|
|
Split out of :mod:`python_pkg.code_tutor._challenge_support` to keep it under
|
|
the 250-line cap. ``_challenge`` imports both entry points back, because
|
|
``tests/test_challenge_part3.py`` patches them on that module.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
from typing import TYPE_CHECKING
|
|
|
|
from python_pkg.code_tutor._challenge_support import _project_root
|
|
|
|
if TYPE_CHECKING:
|
|
from rich.console import Console
|
|
|
|
from python_pkg.code_tutor._analyzer import CodeItem
|
|
|
|
|
|
def _pytest_clean(
|
|
test_ids_or_file: list[str],
|
|
project_root: Path,
|
|
console: Console,
|
|
extra_env: dict[str, str] | None = None,
|
|
) -> bool:
|
|
"""Run pytest on *test_ids_or_file* and return True if all pass.
|
|
|
|
Coverage is disabled so the project's ``fail_under`` threshold does not
|
|
interfere with isolated challenge runs.
|
|
|
|
Args:
|
|
test_ids_or_file: Pytest node IDs or a single temp-file path.
|
|
project_root: Working directory for the pytest subprocess.
|
|
console: Rich console for output.
|
|
extra_env: Extra environment variables (e.g. PYTHONPATH override).
|
|
|
|
Returns:
|
|
``True`` when all collected tests pass.
|
|
"""
|
|
env = {**os.environ, **(extra_env or {})}
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"pytest",
|
|
*test_ids_or_file,
|
|
"-v",
|
|
"--tb=short",
|
|
"--no-header",
|
|
"-p",
|
|
"no:cov",
|
|
"--override-ini=addopts=",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=str(project_root),
|
|
env=env,
|
|
check=False,
|
|
)
|
|
output = (result.stdout + result.stderr).strip()
|
|
if output:
|
|
console.print(output)
|
|
return result.returncode == 0
|
|
|
|
|
|
def _patch_and_test(
|
|
item: CodeItem,
|
|
codebase_path: str,
|
|
user_code: str,
|
|
test_entries: list[tuple[Path, list[str]]],
|
|
console: Console,
|
|
) -> bool:
|
|
"""Replace *item* in source with *user_code*, run tests, then restore.
|
|
|
|
Args:
|
|
item: Code item to patch.
|
|
codebase_path: Absolute codebase root path.
|
|
user_code: User's implementation text.
|
|
test_entries: ``(test_file, node_ids)`` pairs to run.
|
|
console: Rich console for output.
|
|
|
|
Returns:
|
|
``True`` if all tests passed.
|
|
"""
|
|
source_file = Path(codebase_path) / item.file
|
|
original = source_file.read_text(encoding="utf-8")
|
|
orig_lines = original.splitlines()
|
|
|
|
before = orig_lines[: item.start_line - 1]
|
|
after = orig_lines[item.end_line :]
|
|
new_source = "\n".join(before + user_code.splitlines() + after) + "\n"
|
|
|
|
try:
|
|
ast.parse(new_source)
|
|
except SyntaxError as exc:
|
|
console.print(f"[red]Syntax error in your implementation: {exc}[/red]")
|
|
return False
|
|
|
|
test_ids = [f"{tf}::{nid}" for tf, nids in test_entries for nid in nids]
|
|
project_root = _project_root(Path(codebase_path))
|
|
|
|
try:
|
|
source_file.write_text(new_source, encoding="utf-8")
|
|
return _pytest_clean(test_ids, project_root, console)
|
|
finally:
|
|
source_file.write_text(original, encoding="utf-8")
|