File size: 9,260 Bytes
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 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | """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)
|