python_env / graders /pytest_runner.py
uvpatel7271's picture
Upload folder using huggingface_hub
c8e832f verified
"""Helpers for deterministic pytest execution in temp sandboxes."""
from __future__ import annotations
import json
import subprocess
import sys
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
@dataclass(frozen=True)
class PytestExecution:
"""Exact pytest execution summary."""
passed: int
failed: int
total: int
timed_out: bool
output: str
def _test_module_source(tests: Iterable[str]) -> str:
"""Build a valid pytest module from expression-style or full test snippets."""
blocks: list[str] = ["from candidate import * # noqa: F401,F403"]
for index, test in enumerate(tests, start=1):
snippet = str(test).strip()
if not snippet:
continue
if snippet.startswith("def test_"):
blocks.append(snippet)
continue
blocks.append(
"\n".join(
[
f"def test_case_{index:03d}():",
f" assert {snippet}",
]
)
)
return "\n\n".join(blocks) or "def test_placeholder():\n assert True\n"
def _runner_script() -> str:
return """import json
import pathlib
import pytest
class Collector:
def __init__(self) -> None:
self.passed = 0
self.failed = 0
def pytest_runtest_logreport(self, report):
if report.when != "call":
return
if report.passed:
self.passed += 1
elif report.failed:
self.failed += 1
collector = Collector()
exit_code = pytest.main(["-q", "test_candidate.py"], plugins=[collector])
payload = {
"passed": collector.passed,
"failed": collector.failed,
"exit_code": int(exit_code),
}
pathlib.Path("pytest_results.json").write_text(json.dumps(payload), encoding="utf-8")
"""
def run_pytest_suite(candidate_code: str, tests: Iterable[str], timeout_s: float = 3.0) -> PytestExecution:
"""Run a pytest suite against candidate.py and return structured results."""
test_cases = list(tests)
try:
with tempfile.TemporaryDirectory(prefix="python-code-review-") as temp_dir:
temp_path = Path(temp_dir)
(temp_path / "candidate.py").write_text(candidate_code, encoding="utf-8")
(temp_path / "test_candidate.py").write_text(_test_module_source(test_cases), encoding="utf-8")
(temp_path / "runner.py").write_text(_runner_script(), encoding="utf-8")
try:
completed = subprocess.run(
[sys.executable, "runner.py"],
cwd=temp_path,
capture_output=True,
text=True,
timeout=timeout_s,
check=False,
)
except subprocess.TimeoutExpired as exc:
output = (exc.stdout or "") + (exc.stderr or "")
return PytestExecution(
passed=0,
failed=max(len(test_cases), 1),
total=max(len(test_cases), 1),
timed_out=True,
output=(output or "pytest timed out").strip(),
)
result_path = temp_path / "pytest_results.json"
if not result_path.exists():
output = (completed.stdout or "") + (completed.stderr or "")
total = max(len(test_cases), 1)
return PytestExecution(
passed=0,
failed=total,
total=total,
timed_out=False,
output=output.strip(),
)
try:
payload = json.loads(result_path.read_text(encoding="utf-8"))
except Exception as exc:
output = ((completed.stdout or "") + (completed.stderr or "")).strip()
return PytestExecution(
passed=0,
failed=max(len(test_cases), 1),
total=max(len(test_cases), 1),
timed_out=False,
output=(output or str(exc)).strip(),
)
passed = int(payload.get("passed", 0))
failed = int(payload.get("failed", 0))
total = max(passed + failed, len(test_cases))
output = ((completed.stdout or "") + (completed.stderr or "")).strip()
return PytestExecution(
passed=passed,
failed=failed,
total=total,
timed_out=False,
output=output,
)
except Exception as exc:
return PytestExecution(
passed=0,
failed=max(len(test_cases), 1),
total=max(len(test_cases), 1),
timed_out=False,
output=str(exc),
)