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