File size: 4,987 Bytes
63f6453 9438a57 63f6453 9438a57 63f6453 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | """Python: pytest + pytest-cov (coverage.py)."""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Optional
from ..config import settings
from ..models import RunResult, TestKind
from .base import COMPILE_FAIL_RC, LanguageRunner, fill_counts, make_coverage, parse_junit_xml, truncate
from .executor import ExecResult
MODULE = "solution"
class PythonRunner(LanguageRunner):
language = "python"
def image(self) -> str:
return settings.docker_image_python
def _py(self) -> str:
return sys.executable if self.ex.mode == "local" else "python"
def source_filename(self, code: str) -> str:
return f"{MODULE}.py"
def own_files(self) -> set[str]:
return {"conftest.py", "pytest.ini", "setup.cfg", "tox.ini", "pyproject.toml"}
def alias(self, source_name: str):
stem = Path(source_name).stem
# test_*.py / *_test.py adlı bir takma ad pytest tarafından test dosyası diye toplanırdı
if not stem.isidentifier() or stem == MODULE or stem.startswith("test_") or stem.endswith("_test"):
return None
return f"{stem}.py", f"# {source_name} -> solution.py (çok dosyalı girdi)\nfrom {MODULE} import * # noqa\n"
def test_filename(self, code: str, kind: TestKind) -> str:
return "test_interface.py" if kind == "interface" else f"test_{MODULE}.py"
def framework_hint(self, code: str, kind: TestKind) -> str:
base = (
"Framework: pytest (Python 3.12). The code under test is saved as `solution.py` in the same "
"directory as the test file. Import it with `from solution import ...` or `import solution`. "
"Use plain `assert`, `with pytest.raises(SomeError):` in a dedicated test, and `pytest.approx` for floats. "
"Prefer separate test functions over `pytest.mark.parametrize`; never use `isinstance(x, pytest.raises)`. "
"Do not use network, sleep or external files. `unittest.mock` is available."
)
if kind == "interface":
base += (
" These are INTERFACE / CONTRACT tests: verify the public API surface (function and class "
"signatures via `inspect`, return types, raised exception types, documented invariants) and "
"how public units work together end-to-end. Do not test private helpers (names starting with `_`)."
)
return base
def write_files(self, wd: Path, code: str, test_code: str, kind: TestKind, html: Optional[str]) -> None:
(wd / self.source_filename(code)).write_text(code, encoding="utf-8")
(wd / self.test_filename(code, kind)).write_text(test_code, encoding="utf-8")
(wd / "conftest.py").write_text("import sys, os\nsys.path.insert(0, os.path.dirname(__file__))\n")
def command(self, wd: Path, code: str, test_code: str, kind: TestKind, with_coverage: bool) -> str:
py = self._py()
tf = self.test_filename(code, kind)
cov = f"--cov={MODULE} --cov-report=json:.lta/cov.json --cov-report=" if with_coverage else "-p no:cov"
return (
"mkdir -p .lta && "
f"( {py} -m py_compile {MODULE}.py {tf} > .lta/compile.log 2>&1 || "
f"{{ echo __LTA_COMPILE_FAILED__; cat .lta/compile.log; exit {COMPILE_FAIL_RC}; }} ) && "
f"{py} -m pytest -q -p no:cacheprovider --tb=short --no-header -rN "
f"--junitxml=.lta/junit.xml {cov} {tf}"
)
def parse(self, wd: Path, res: ExecResult, code: str, kind: TestKind) -> RunResult:
cases = parse_junit_xml([wd / ".lta" / "junit.xml"])
# Toplama (collection) hatası: test dosyası import edilemedi -> derleme hatası gibi ele al
if not cases and res.returncode not in (0, 5):
return RunResult(compiled=False, compile_output=truncate(res.combined), output=truncate(res.combined))
# Test dosyası import edilemediyse pytest bunu "collection failure" olarak raporlar
coll = [c for c in cases if c.status == "error" and "collection failure" in c.message]
if coll:
msg = "\n".join(c.message for c in coll)
return RunResult(compiled=False, compile_output=truncate(msg), output=truncate(res.combined))
# pytest junit isimleri: "test_solution::test_x" -> sadeleştir
for c in cases:
c.name = c.name.split("::")[-1] if "::" in c.name else c.name
result = fill_counts(RunResult(cases=cases, output=truncate(res.combined)))
cov_file = wd / ".lta" / "cov.json"
if cov_file.exists():
data = json.loads(cov_file.read_text())
for fname, info in data.get("files", {}).items():
if Path(fname).name == f"{MODULE}.py":
result.coverage = make_coverage(set(info.get("executed_lines", [])),
set(info.get("missing_lines", [])))
return result
|