""" Go Test Runner Fuehrt Go-Tests aus und parsed die Ergebnisse. """ import subprocess import json from datetime import datetime from pathlib import Path from typing import Dict, List, Optional from dataclasses import dataclass, field @dataclass class GoTestResult: """Ergebnis eines einzelnen Go-Tests""" package: str test_name: str passed: bool duration_seconds: float output: str = "" @dataclass class GoTestSummary: """Zusammenfassung eines Go-Test-Runs""" total: int = 0 passed: int = 0 failed: int = 0 skipped: int = 0 duration_seconds: float = 0.0 coverage_percent: Optional[float] = None results: List[GoTestResult] = field(default_factory=list) raw_output: str = "" class GoTestRunner: """ Runner fuer Go-Tests. Verwendet `go test -json` fuer strukturierte Ausgabe. """ def __init__(self, base_path: Path): self.base_path = base_path async def run(self, with_coverage: bool = True, timeout: int = 300) -> GoTestSummary: """ Fuehrt Go-Tests aus. Args: with_coverage: Coverage erfassen timeout: Timeout in Sekunden Returns: GoTestSummary mit allen Ergebnissen """ if not self.base_path.exists(): return GoTestSummary(raw_output="Pfad existiert nicht") cmd = ["go", "test", "-v", "-json"] if with_coverage: cmd.extend(["-cover", "-coverprofile=coverage.out"]) cmd.append("./...") try: result = subprocess.run( cmd, cwd=str(self.base_path), capture_output=True, text=True, timeout=timeout, ) return self._parse_output(result.stdout, result.stderr) except subprocess.TimeoutExpired: return GoTestSummary(raw_output=f"Timeout nach {timeout} Sekunden") except FileNotFoundError: return GoTestSummary(raw_output="Go nicht installiert") except Exception as e: return GoTestSummary(raw_output=str(e)) def _parse_output(self, stdout: str, stderr: str) -> GoTestSummary: """Parsed die JSON-Ausgabe von go test""" summary = GoTestSummary(raw_output=stdout[:10000] if stdout else stderr[:10000]) current_test: Dict[str, str] = {} test_outputs: Dict[str, List[str]] = {} for line in stdout.split("\n"): if not line.strip(): continue try: event = json.loads(line) action = event.get("Action") package = event.get("Package", "") test = event.get("Test", "") elapsed = event.get("Elapsed", 0) output = event.get("Output", "") # Test-Output sammeln if test and output: key = f"{package}:{test}" if key not in test_outputs: test_outputs[key] = [] test_outputs[key].append(output) # Test-Ergebnis if action == "pass" and test: summary.passed += 1 summary.total += 1 summary.results.append(GoTestResult( package=package, test_name=test, passed=True, duration_seconds=elapsed, output="".join(test_outputs.get(f"{package}:{test}", [])), )) elif action == "fail" and test: summary.failed += 1 summary.total += 1 summary.results.append(GoTestResult( package=package, test_name=test, passed=False, duration_seconds=elapsed, output="".join(test_outputs.get(f"{package}:{test}", [])), )) elif action == "skip" and test: summary.skipped += 1 summary.total += 1 # Package-Ergebnis (Gesamtdauer) elif action in ["pass", "fail"] and not test and elapsed: summary.duration_seconds = max(summary.duration_seconds, elapsed) except json.JSONDecodeError: # Nicht-JSON-Zeilen ignorieren (z.B. Coverage-Output) if "coverage:" in line.lower(): # z.B. "coverage: 75.2% of statements" try: parts = line.split("coverage:") if len(parts) > 1: percent_str = parts[1].strip().split("%")[0] summary.coverage_percent = float(percent_str) except (ValueError, IndexError): pass return summary async def run_single_test(self, test_name: str, timeout: int = 60) -> GoTestResult: """ Fuehrt einen einzelnen Test aus. Args: test_name: Name des Tests (z.B. "TestMyFunction") timeout: Timeout in Sekunden Returns: GoTestResult fuer den spezifischen Test """ cmd = ["go", "test", "-v", "-run", test_name, "./..."] try: result = subprocess.run( cmd, cwd=str(self.base_path), capture_output=True, text=True, timeout=timeout, ) passed = "PASS" in result.stdout return GoTestResult( package=str(self.base_path), test_name=test_name, passed=passed, duration_seconds=0.0, output=result.stdout + result.stderr, ) except Exception as e: return GoTestResult( package=str(self.base_path), test_name=test_name, passed=False, duration_seconds=0.0, output=str(e), ) async def get_coverage_report(self) -> Optional[Dict]: """ Liest den Coverage-Bericht. Returns: Dict mit Coverage-Details oder None """ coverage_file = self.base_path / "coverage.out" if not coverage_file.exists(): return None try: result = subprocess.run( ["go", "tool", "cover", "-func=coverage.out"], cwd=str(self.base_path), capture_output=True, text=True, ) # Parse "total:" Zeile for line in result.stdout.split("\n"): if "total:" in line: parts = line.split() if len(parts) >= 3: percent_str = parts[-1].replace("%", "") return { "total_coverage": float(percent_str), "raw_output": result.stdout, } except Exception: pass return None