"""Java: javac + JUnit 5 (console launcher) + JaCoCo. Docker imajında JUnit 5 ve JaCoCo hazırdır. Yerel modda `JAVA_LIB_DIR` içinde `junit-platform-console-standalone.jar` (+ isteğe bağlı `jacocoagent.jar`, `jacococli.jar`) aranır; yoksa JUnit 4 (`junit.jar` + `hamcrest-core.jar`) ile kapsama olmadan çalışılır. """ from __future__ import annotations import re import xml.etree.ElementTree as ET from pathlib import Path from typing import Optional from ..config import settings from ..models import RunResult, TestCaseResult, TestKind from .base import COMPILE_FAIL_RC, LanguageRunner, fill_counts, make_coverage, parse_junit_xml, truncate from .executor import ExecResult PKG_RE = re.compile(r"^\s*package\s+([\w.]+)\s*;", re.M) CLASS_RE = re.compile(r"^\s*public\s+(?:final\s+|abstract\s+)*(?:class|interface|enum|record)\s+(\w+)", re.M) ANY_CLASS_RE = re.compile(r"^\s*(?:public\s+)?(?:final\s+|abstract\s+)*(?:class|interface|enum|record)\s+(\w+)", re.M) def java_package(code: str) -> str: m = PKG_RE.search(code) return m.group(1) if m else "" def java_class(code: str, default: str = "Solution") -> str: m = CLASS_RE.search(code) or ANY_CLASS_RE.search(code) return m.group(1) if m else default class JavaRunner(LanguageRunner): language = "java" def image(self) -> str: return settings.docker_image_java # --- Framework tespiti --- def framework(self) -> str: if self.ex.mode != "local": return "junit5" lib = Path(self.ex.java_lib) if (lib / "junit-platform-console-standalone.jar").exists(): return "junit5" if (lib / "junit.jar").exists(): return "junit4" return "missing" def has_jacoco(self) -> bool: if self.ex.mode != "local": return True lib = Path(self.ex.java_lib) return (lib / "jacocoagent.jar").exists() and (lib / "jacococli.jar").exists() @property def supports_coverage(self) -> bool: # type: ignore[override] return self.has_jacoco() def source_filename(self, code: str) -> str: return f"{java_class(code)}.java" def test_filename(self, code: str, kind: TestKind) -> str: suffix = "InterfaceTest" if kind == "interface" else "Test" return f"{java_class(code)}{suffix}.java" def framework_hint(self, code: str, kind: TestKind) -> str: cls, pkg = java_class(code), java_package(code) test_cls = Path(self.test_filename(code, kind)).stem pkg_line = f"Put `package {pkg};` at the top of the test (same package as the code). " if pkg else \ "The code is in the default package; do NOT add a package declaration. " if self.framework() == "junit4": fw = ("Framework: JUnit 4.13 (`import org.junit.Test; import static org.junit.Assert.*;`). " "Use `@Test(expected = X.class)` or `assertThrows` for exceptions.") else: fw = ("Framework: JUnit 5 Jupiter (`import org.junit.jupiter.api.*; import static " "org.junit.jupiter.api.Assertions.*;`, `@ParameterizedTest` with `@CsvSource` is available " "via `org.junit.jupiter.params`). Use `assertThrows` for exceptions and `assertEquals(expected, " "actual, delta)` for doubles.") hint = (f"{fw} Java 21. The class under test is `{cls}`. {pkg_line}The test class MUST be named " f"`{test_cls}` (file `{test_cls}.java`). Only the JDK standard library and JUnit are available; " "no Mockito.") if kind == "interface": hint += (" These are INTERFACE / CONTRACT tests: check the public API via reflection where useful " "(public methods, their parameter/return types, implemented interfaces), the exceptions " "declared/thrown by public methods, and end-to-end collaboration between public methods.") return hint def extra_path(self, wd: Path, rel: Path, content: str) -> Path: if rel.suffix != ".java": return wd / "src" / "main" / "resources" / rel pkg = java_package(content) # Java'da public sınıf kendi adını taşıyan dosyada olmalı: Test02.java içindeki "public class InventoryManager" # InventoryManager.java olarak yazılır (yoksa tüm derleme başarısız olur). name = f"{java_class(content, rel.stem)}.java" return wd / "src" / "main" / (Path(*pkg.split(".")) if pkg else Path()) / name def write_files(self, wd: Path, code: str, test_code: str, kind: TestKind, html: Optional[str]) -> None: pkg = java_package(code) rel = Path(*pkg.split(".")) if pkg else Path() main = wd / "src" / "main" / rel test = wd / "src" / "test" / rel main.mkdir(parents=True, exist_ok=True) test.mkdir(parents=True, exist_ok=True) (main / self.source_filename(code)).write_text(code, encoding="utf-8") test_name = f"{java_class(test_code, Path(self.test_filename(code, kind)).stem)}.java" (test / test_name).write_text(test_code, encoding="utf-8") def command(self, wd: Path, code: str, test_code: str, kind: TestKind, with_coverage: bool) -> str: lib = self.ex.java_lib fw = self.framework() if fw == "missing": return (f"echo '__LTA_COMPILE_FAILED__ JUnit bulunamadı: {lib} dizinine " f"junit-platform-console-standalone.jar koyun (README).'; exit {COMPILE_FAIL_RC}") test_cp = f"{lib}/junit-platform-console-standalone.jar" if fw == "junit5" else \ f"{lib}/junit.jar:{lib}/hamcrest-core.jar" compile_ = ( "unset JAVA_TOOL_OPTIONS; mkdir -p .lta out/main out/test && " "( { javac -nowarn -encoding UTF-8 -d out/main $(find src/main -name '*.java') && " f"javac -nowarn -encoding UTF-8 -d out/test -cp out/main:{test_cp} $(find src/test -name '*.java'); }} " f"> .lta/compile.log 2>&1 || {{ echo __LTA_COMPILE_FAILED__; cat .lta/compile.log; exit {COMPILE_FAIL_RC}; }} )" ) if fw == "junit4": pkg = java_package(code) test_cls = java_class(test_code, Path(self.test_filename(code, kind)).stem) fq = f"{pkg}.{test_cls}" if pkg else test_cls return f"{compile_} && java -cp out/main:out/test:{test_cp} org.junit.runner.JUnitCore {fq}" agent = "" if with_coverage and self.has_jacoco(): agent = f"-javaagent:{lib}/jacocoagent.jar=destfile=.lta/jacoco.exec" run = (f"java {agent} -jar {lib}/junit-platform-console-standalone.jar execute " "--class-path out/main:out/test --scan-class-path --disable-banner --disable-ansi-colors " "--details=none --reports-dir=.lta/reports; rc=$?") report = "" if agent: report = (f"; [ -f .lta/jacoco.exec ] && java -jar {lib}/jacococli.jar report .lta/jacoco.exec " "--classfiles out/main --sourcefiles src/main --xml .lta/jacoco.xml > /dev/null 2>&1") return f"{compile_} && {{ {run}{report}; exit $rc; }}" def parse(self, wd: Path, res: ExecResult, code: str, kind: TestKind) -> RunResult: if self.framework() == "junit4": return self._parse_junit4(res) reports = list((wd / ".lta" / "reports").glob("*.xml")) cases = parse_junit_xml(reports) for c in cases: # "pkg.FooTest::addsNumbers()" -> "FooTest.addsNumbers" cls, _, name = c.name.partition("::") c.name = f"{cls.split('.')[-1]}.{name.rstrip('()')}" if name else c.name result = fill_counts(RunResult(cases=cases, output=truncate(res.combined))) jx = wd / ".lta" / "jacoco.xml" if jx.exists(): result.coverage = _jacoco_lines(jx, self.source_filename(code)) return result def _parse_junit4(self, res: ExecResult) -> RunResult: out = res.combined cases: list[TestCaseResult] = [] failed_names = re.findall(r"^\d+\) (\w+)\(([\w.$]+)\)", out, re.M) m_ok = re.search(r"OK \((\d+) tests?\)", out) m_fail = re.search(r"Tests run: (\d+),\s+Failures: (\d+)", out) total = int(m_ok.group(1)) if m_ok else int(m_fail.group(1)) if m_fail else 0 for name, cls in failed_names: block = out.split(f") {name}({cls})", 1)[-1][:800] cases.append(TestCaseResult(name=f"{cls.split('.')[-1]}.{name}", status="failed", message=block.strip())) for i in range(total - len(failed_names)): cases.append(TestCaseResult(name=f"passed_{i + 1}", status="passed")) return fill_counts(RunResult(cases=cases, output=truncate(out))) def _jacoco_lines(path: Path, filename: str): covered: set[int] = set() missed: set[int] = set() root = ET.parse(path).getroot() for sf in root.iter("sourcefile"): if sf.get("name") != filename: continue for ln in sf.iter("line"): nr, ci, mi = int(ln.get("nr")), int(ln.get("ci", 0)), int(ln.get("mi", 0)) if ci > 0: covered.add(nr) elif mi > 0: missed.add(nr) return make_coverage(covered, missed)