"""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