refactor: replace return/var in castlingTargets with functional style (#4)
Build & Test (NowChessSystems) TeamCity build finished
Build & Test (NowChessSystems) TeamCity build finished
Reviewed-on: #4 Co-authored-by: Janis <janis.e.20@gmx.de> Co-committed-by: Janis <janis.e.20@gmx.de>
This commit was merged in pull request #4.
This commit is contained in:
@@ -1,120 +0,0 @@
|
|||||||
# Claude Code – Working Agreement
|
|
||||||
|
|
||||||
## Workflow: Plan → Write Tests → Implement → Verify
|
|
||||||
|
|
||||||
### 1. Plan First
|
|
||||||
Before writing any code, produce an explicit plan:
|
|
||||||
- Restate the requirement in your own words to confirm understanding.
|
|
||||||
- List every file you intend to create or modify.
|
|
||||||
- Identify risks or unknowns upfront.
|
|
||||||
- Wait for confirmation **only** when the plan reveals an ambiguity that cannot be resolved from context. Otherwise proceed immediately.
|
|
||||||
|
|
||||||
### 2. Write Tests
|
|
||||||
Before implementing, write tests that should cover the new behaviour.
|
|
||||||
Only write tests for the new behaviour.
|
|
||||||
|
|
||||||
### 3. Implement
|
|
||||||
Follow the plan. Do not add scope beyond what was agreed.
|
|
||||||
|
|
||||||
### 4. Verify Every Requirement
|
|
||||||
After implementation, go through each requirement one-by-one and confirm it is satisfied:
|
|
||||||
- Run the relevant tests (unit, integration, or build check) for every changed module.
|
|
||||||
- If a requirement **cannot** be fulfilled, do **not** silently skip it — document it immediately (see *Unresolved Requirements* below).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## No Code Without Verification (Testing)
|
|
||||||
|
|
||||||
- Every new behaviour must be covered by at least one automated test before the task is considered done.
|
|
||||||
- Every bug fix must be accompanied by a regression test that fails before the fix and passes after.
|
|
||||||
- Run `./gradlew :modules:<module>:test` (or the appropriate Gradle task) and confirm a green build before marking work complete.
|
|
||||||
- If a test cannot be written for a legitimate reason, document it in `docs/unresolved.md` with an explanation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Automatic Bug Fixing
|
|
||||||
|
|
||||||
- When a test or build step fails, attempt to fix the root cause immediately — do **not** ask for permission.
|
|
||||||
- Apply the fix, re-run the verification, and continue until green.
|
|
||||||
- If the same failure persists after **three** fix attempts, stop, log the issue in `docs/unresolved.md`, and surface a concise summary.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Unresolved Requirements → `docs/unresolved.md`
|
|
||||||
|
|
||||||
When a requirement or bug cannot be resolved, append an entry to `docs/unresolved.md`:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## [YYYY-MM-DD] <Short title>
|
|
||||||
|
|
||||||
**Requirement / Bug:**
|
|
||||||
<What was requested or what failed>
|
|
||||||
|
|
||||||
**Root Cause (if known):**
|
|
||||||
<Why it cannot be resolved right now>
|
|
||||||
|
|
||||||
**Attempted Fixes:**
|
|
||||||
1. <What was tried>
|
|
||||||
2. …
|
|
||||||
|
|
||||||
**Suggested Next Step:**
|
|
||||||
<What a human engineer should investigate>
|
|
||||||
```
|
|
||||||
|
|
||||||
Create the file if it does not exist. Never delete existing entries.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
. ← Repository root (multi-project Gradle setup)
|
|
||||||
├── build.gradle.kts ← Root build file (shared plugins, dependency versions)
|
|
||||||
├── settings.gradle.kts ← Gradle settings (declares all subprojects)
|
|
||||||
├── modules/ ← One subdirectory per microservice
|
|
||||||
│ └── <service>/
|
|
||||||
│ ├── build.gradle.kts
|
|
||||||
│ └── src/
|
|
||||||
└── docs/ ← Architecture Decision Records, API docs, unresolved issues
|
|
||||||
└── unresolved.md
|
|
||||||
```
|
|
||||||
|
|
||||||
### Conventions
|
|
||||||
- All microservices live under `modules/{service-name}`. Never place service code in the root.
|
|
||||||
- Shared configuration (dependency versions, plugin setup) belongs in the **root** `build.gradle.kts` or in `buildSrc` / a version catalog.
|
|
||||||
- `settings.gradle.kts` must include every module via `include(":modules:<service>")`.
|
|
||||||
- Architecture decisions go in `docs/adr/` as numbered Markdown files (`ADR-001-<title>.md`).
|
|
||||||
- API contracts live in `/docs/api/`.
|
|
||||||
- Unit tests extend `AnyFunSuite with Matchers` — no `@Test` annotations, no `: Unit` requirement
|
|
||||||
- Integration tests use `@QuarkusTest` with JUnit 5 — `@Test` methods must be explicitly typed `: Unit`
|
|
||||||
- Always exclude scala-library from Quarkus deps to avoid Scala 2 conflicts
|
|
||||||
|
|
||||||
## Coverage Conventions
|
|
||||||
- Branch coverage must be at least 90% - unless there is a good reason not to.
|
|
||||||
- Line coverage must be at least 95% - unless there is a good reason not to.
|
|
||||||
- Method coverage must be at least 90% - unless there is a good reason not to.
|
|
||||||
- To check coverage use jacoco-reporter/scoverage_coverage_gaps.py modules/{service}/build/reports/scoverageTest/scoverage.xml
|
|
||||||
- IMPORTANT: modules/{service}/build/reports/scoverage/scoverage.xml is not used for coverage TEST calculation. Do not use it.
|
|
||||||
|
|
||||||
## Agent Routing Rules
|
|
||||||
|
|
||||||
### Use agents in PARALLEL when:
|
|
||||||
- Tasks touch different, independent microservices
|
|
||||||
- No shared files or state between tasks
|
|
||||||
- Example: "implement service-user AND service-orders simultaneously"
|
|
||||||
|
|
||||||
### Use agents SEQUENTIALLY when:
|
|
||||||
- Tasks have dependencies (architect → implementer → test-writer)
|
|
||||||
- Shared API contracts are involved
|
|
||||||
- Example: design API first, then implement, then test
|
|
||||||
|
|
||||||
## Quick-Reference Checklist
|
|
||||||
|
|
||||||
Before considering any task done, confirm:
|
|
||||||
|
|
||||||
- [ ] Plan was written and requirements restated
|
|
||||||
- [ ] All planned files were created / modified
|
|
||||||
- [ ] Automated tests cover the new behaviour
|
|
||||||
- [ ] `./gradlew build` (or scoped task) is green
|
|
||||||
- [ ] Each requirement has been explicitly verified
|
|
||||||
- [ ] Any unresolved items are logged in `docs/unresolved.md`
|
|
||||||
@@ -1,58 +1,54 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md — NowChessSystems
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
## Stack
|
||||||
|
Scala 3.5.x · Quarkus + quarkus-scala3 · Hibernate/Jakarta · Lanterna TUI · K8s + ArgoCD + Kargo · Frontend TBD (Vite/React/Angular/Vue)
|
||||||
|
|
||||||
## Build & Test Commands
|
## Structure
|
||||||
|
```
|
||||||
|
build.gradle.kts / settings.gradle.kts # root; include(":modules:<svc>") per service
|
||||||
|
modules/<svc>/build.gradle.kts + src/
|
||||||
|
docs/adr/ docs/api/ docs/unresolved.md
|
||||||
|
```
|
||||||
|
Versions in root `extra["VERSIONS"]`; modules read via `rootProject.extra["VERSIONS"] as Map<String,String>`.
|
||||||
|
|
||||||
|
## Commands
|
||||||
```bash
|
```bash
|
||||||
# Build everything
|
|
||||||
./gradlew build
|
./gradlew build
|
||||||
|
./gradlew :modules:<svc>:build|test
|
||||||
# Build a single module
|
./gradlew :modules:<svc>:test --tests "de.nowchess.<svc>.<Class>"
|
||||||
./gradlew :modules:<service>:build
|
|
||||||
|
|
||||||
# Run tests for a single module
|
|
||||||
./gradlew :modules:<service>:test
|
|
||||||
|
|
||||||
# Run a specific test class
|
|
||||||
./gradlew :modules:<service>:test --tests "de.nowchess.<service>.<ClassName>"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The only current module is `core` (`modules/core`).
|
## Workflow
|
||||||
|
1. **Plan** — restate requirement, list files, flag risks. Proceed unless genuine ambiguity.
|
||||||
|
2. **Tests first** — cover only new behaviour.
|
||||||
|
3. **Implement** — no scope creep.
|
||||||
|
4. **Verify** — check each requirement; confirm green build.
|
||||||
|
|
||||||
## Architecture
|
## Scala/Quarkus Rules
|
||||||
|
- `given`/`using` only (no `implicit`); `Option`/`Either`/`Try` (no `null`/`.get`)
|
||||||
|
- `jakarta.*` only; reactive I/O (`Uni`/`Multi`), no blocking on event loop
|
||||||
|
- Always exclude `org.scala-lang:scala-library` from Quarkus BOM
|
||||||
|
- Unit tests: `extends AnyFunSuite with Matchers` — no `@Test`, no `: Unit`
|
||||||
|
- Integration tests: `@QuarkusTest` + JUnit 5 — `@Test` methods need explicit `: Unit`
|
||||||
|
|
||||||
**NowChessSystems** is a chess platform built as a Scala 3 + Quarkus microservice system.
|
## Coverage
|
||||||
|
Line ≥ 95% · Branch ≥ 90% · Method ≥ 90% (document exceptions)
|
||||||
|
Check: `jacoco-reporter/scoverage_coverage_gaps.py modules/{svc}/build/reports/scoverageTest/scoverage.xml`
|
||||||
|
⚠️ Use `scoverageTest/`, NOT `scoverage/`.
|
||||||
|
|
||||||
- Multi-module Gradle project; every service lives under `modules/{service-name}`.
|
## Bug Fixing
|
||||||
- Shared dependency versions live in the root `build.gradle.kts` under `extra["VERSIONS"]`.
|
Fix failures immediately without asking. After 3 failed attempts → log in `docs/unresolved.md` + surface summary.
|
||||||
- Each module reads versions via `rootProject.extra["VERSIONS"] as Map<String, String>`.
|
|
||||||
- `settings.gradle.kts` must `include(":modules:<service>")` for every module.
|
|
||||||
|
|
||||||
### Stack (ADR-001)
|
## Agents (new service)
|
||||||
| Layer | Technology |
|
Sequential: architect → scala-implementer → test-writer → gradle-builder → code-reviewer (review only, no self-fix)
|
||||||
|---|---|
|
Parallel: only when services are fully independent (no shared contracts/state).
|
||||||
| Language | Scala 3.5.x |
|
|
||||||
| Backend framework | Quarkus + `quarkus-scala3` extension |
|
|
||||||
| Persistence | Hibernate / Jakarta Persistence |
|
|
||||||
| Frontend (TBD) | Vite; React/Angular/Vue under evaluation |
|
|
||||||
| TUI | Lanterna |
|
|
||||||
| Container orchestration | Kubernetes + ArgoCD + Kargo |
|
|
||||||
|
|
||||||
### Key Scala 3 / Quarkus Rules
|
## Unresolved (`docs/unresolved.md`)
|
||||||
- Use `given`/`using`, not `implicit` (no Scala 2 idioms).
|
Append only, never delete:
|
||||||
- Use `Option`/`Either`/`Try`, never `null` or `.get`.
|
```
|
||||||
- Jakarta annotations only (`jakarta.*`), never `javax.*`.
|
## [YYYY-MM-DD] <title>
|
||||||
- Use reactive types (`Uni`, `Multi`) for I/O; no blocking calls on the event loop.
|
**Requirement/Bug:** **Root Cause:** **Attempted Fixes:** **Next Step:**
|
||||||
- **Always exclude `org.scala-lang:scala-library` from Quarkus BOM** to avoid Scala 2 conflicts.
|
```
|
||||||
- **Unit tests use `extends AnyFunSuite with Matchers`** — ScalaTest DSL, no `@Test` annotations needed.
|
|
||||||
- **Integration tests use `@QuarkusTest` with JUnit 5** — explicit `: Unit` return type still required on `@Test` methods.
|
|
||||||
|
|
||||||
### Agent Workflow (for new services)
|
## Done Checklist
|
||||||
1. **architect** → writes OpenAPI contract to `docs/api/{service}.yaml` and ADR to `docs/adr/`.
|
- [ ] Plan written · files created/modified · tests green · requirements verified · unresolved logged
|
||||||
2. **scala-implementer** → reads contract, implements service under `modules/{service}/`.
|
|
||||||
3. **test-writer** → writes `@QuarkusTest` integration tests and `AnyFunSuite with Matchers` unit tests.
|
|
||||||
4. **gradle-builder** → resolves any build/dependency issues.
|
|
||||||
5. **code-reviewer** → reviews; reports findings back without self-fixing.
|
|
||||||
|
|
||||||
Detailed working agreement (plan/verify/unresolved workflow) is in `.claude/CLAUDE.MD`.
|
|
||||||
|
|||||||
@@ -1,411 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
JaCoCo Coverage Gap Reporter
|
|
||||||
Parses a JaCoCo XML report and outputs missing line & branch (conditional)
|
|
||||||
coverage in a structured format that Claude Code agents can act on directly.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python jacoco_coverage_gaps.py <jacoco-report.xml> [--min-coverage 80]
|
|
||||||
python jacoco_coverage_gaps.py <jacoco-report.xml> --output json
|
|
||||||
python jacoco_coverage_gaps.py <jacoco-report.xml> --output markdown
|
|
||||||
python jacoco_coverage_gaps.py <jacoco-report.xml> --output agent (default)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Data classes
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LineCoverage:
|
|
||||||
line_number: int
|
|
||||||
hits: int # 0 = not executed
|
|
||||||
branch_total: int = 0 # 0 = not a branch point
|
|
||||||
branch_covered: int = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_uncovered(self) -> bool:
|
|
||||||
return self.hits == 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_partial_branch(self) -> bool:
|
|
||||||
return self.branch_total > 0 and self.branch_covered < self.branch_total
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MethodCoverage:
|
|
||||||
name: str
|
|
||||||
descriptor: str
|
|
||||||
first_line: Optional[int]
|
|
||||||
missed_instructions: int
|
|
||||||
covered_instructions: int
|
|
||||||
missed_branches: int
|
|
||||||
covered_branches: int
|
|
||||||
uncovered_lines: list[int] = field(default_factory=list)
|
|
||||||
partial_branch_lines: list[int] = field(default_factory=list)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_branches(self) -> int:
|
|
||||||
return self.missed_branches + self.covered_branches
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_fully_covered(self) -> bool:
|
|
||||||
return self.missed_instructions == 0 and self.missed_branches == 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def branch_coverage_pct(self) -> float:
|
|
||||||
total = self.total_branches
|
|
||||||
return 100.0 * self.covered_branches / total if total else 100.0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def line_coverage_pct(self) -> float:
|
|
||||||
total = self.missed_instructions + self.covered_instructions
|
|
||||||
return 100.0 * self.covered_instructions / total if total else 100.0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ClassCoverage:
|
|
||||||
class_name: str # e.g. com/example/Foo
|
|
||||||
source_file: Optional[str]
|
|
||||||
methods: list[MethodCoverage] = field(default_factory=list)
|
|
||||||
all_lines: list[LineCoverage] = field(default_factory=list)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def java_class_name(self) -> str:
|
|
||||||
return self.class_name.replace("/", ".")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def source_path(self) -> Optional[str]:
|
|
||||||
"""Best-guess relative source path."""
|
|
||||||
if self.source_file:
|
|
||||||
package = "/".join(self.class_name.split("/")[:-1])
|
|
||||||
return f"src/main/java/{package}/{self.source_file}" if package else f"src/main/java/{self.source_file}"
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def uncovered_lines(self) -> list[int]:
|
|
||||||
return sorted({l.line_number for l in self.all_lines if l.is_uncovered})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def partial_branch_lines(self) -> list[int]:
|
|
||||||
return sorted({l.line_number for l in self.all_lines if l.is_partial_branch})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def missed_branches(self) -> int:
|
|
||||||
return sum(max(l.branch_total - l.branch_covered, 0) for l in self.all_lines)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_branches(self) -> int:
|
|
||||||
return sum(l.branch_total for l in self.all_lines)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def covered_branches(self) -> int:
|
|
||||||
return self.total_branches - self.missed_branches
|
|
||||||
|
|
||||||
@property
|
|
||||||
def missed_lines(self) -> int:
|
|
||||||
return len(self.uncovered_lines)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_lines(self) -> int:
|
|
||||||
return len(self.all_lines)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def covered_lines(self) -> int:
|
|
||||||
return self.total_lines - self.missed_lines
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_gaps(self) -> bool:
|
|
||||||
return bool(self.uncovered_lines or self.partial_branch_lines)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Parser
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def parse_jacoco_xml(xml_path: str) -> list[ClassCoverage]:
|
|
||||||
"""Parse a JaCoCo XML report into ClassCoverage objects."""
|
|
||||||
tree = ET.parse(xml_path)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
results: list[ClassCoverage] = []
|
|
||||||
|
|
||||||
for package in root.iter("package"):
|
|
||||||
for cls_elem in package.findall("class"):
|
|
||||||
class_name = cls_elem.get("name", "")
|
|
||||||
source_file = cls_elem.get("sourcefilename")
|
|
||||||
|
|
||||||
# Build method map from <method> children
|
|
||||||
methods: list[MethodCoverage] = []
|
|
||||||
for m in cls_elem.findall("method"):
|
|
||||||
counters = {c.get("type"): c for c in m.findall("counter")}
|
|
||||||
|
|
||||||
def _missed(t): return int(counters[t].get("missed", 0)) if t in counters else 0
|
|
||||||
def _covered(t): return int(counters[t].get("covered", 0)) if t in counters else 0
|
|
||||||
|
|
||||||
methods.append(MethodCoverage(
|
|
||||||
name=m.get("name", ""),
|
|
||||||
descriptor=m.get("desc", ""),
|
|
||||||
first_line=int(m.get("line")) if m.get("line") else None,
|
|
||||||
missed_instructions=_missed("INSTRUCTION"),
|
|
||||||
covered_instructions=_covered("INSTRUCTION"),
|
|
||||||
missed_branches=_missed("BRANCH"),
|
|
||||||
covered_branches=_covered("BRANCH"),
|
|
||||||
))
|
|
||||||
|
|
||||||
cc = ClassCoverage(
|
|
||||||
class_name=class_name,
|
|
||||||
source_file=source_file,
|
|
||||||
methods=methods,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Per-line data lives in the matching <sourcefile> element
|
|
||||||
source_file_elem = package.find(f"sourcefile[@name='{source_file}']") if source_file else None
|
|
||||||
if source_file_elem is not None:
|
|
||||||
for line_elem in source_file_elem.findall("line"):
|
|
||||||
nr = int(line_elem.get("nr", 0))
|
|
||||||
mi = int(line_elem.get("mi", 0)) # missed instructions
|
|
||||||
ci = int(line_elem.get("ci", 0)) # covered instructions
|
|
||||||
mb = int(line_elem.get("mb", 0)) # missed branches
|
|
||||||
cb = int(line_elem.get("cb", 0)) # covered branches
|
|
||||||
hits = ci # ci > 0 means line was executed at least once
|
|
||||||
cc.all_lines.append(LineCoverage(
|
|
||||||
line_number=nr,
|
|
||||||
hits=hits,
|
|
||||||
branch_total=mb + cb,
|
|
||||||
branch_covered=cb,
|
|
||||||
))
|
|
||||||
|
|
||||||
if cc.has_gaps:
|
|
||||||
results.append(cc)
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Formatters
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _compact_ranges(numbers: list[int]) -> str:
|
|
||||||
"""Turn [1,2,3,5,7,8,9] -> '1-3, 5, 7-9'"""
|
|
||||||
if not numbers:
|
|
||||||
return ""
|
|
||||||
ranges = []
|
|
||||||
start = prev = numbers[0]
|
|
||||||
for n in numbers[1:]:
|
|
||||||
if n == prev + 1:
|
|
||||||
prev = n
|
|
||||||
else:
|
|
||||||
ranges.append(f"{start}-{prev}" if start != prev else str(start))
|
|
||||||
start = prev = n
|
|
||||||
ranges.append(f"{start}-{prev}" if start != prev else str(start))
|
|
||||||
return ", ".join(ranges)
|
|
||||||
|
|
||||||
|
|
||||||
def format_agent(classes: list[ClassCoverage]) -> str:
|
|
||||||
"""
|
|
||||||
Output optimised for Claude Code agents:
|
|
||||||
– structured, machine-readable yet human-legible
|
|
||||||
– uses file paths and line numbers agents can act on
|
|
||||||
– groups by file, sorts by severity (most gaps first)
|
|
||||||
"""
|
|
||||||
lines: list[str] = []
|
|
||||||
lines.append("# JaCoCo Coverage Gaps — Agent Action Report")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("## Summary")
|
|
||||||
total_uncovered = sum(c.missed_lines for c in classes)
|
|
||||||
total_partial = sum(len(c.partial_branch_lines) for c in classes)
|
|
||||||
total_missed_branches = sum(c.missed_branches for c in classes)
|
|
||||||
lines.append(f"- Files with gaps : {len(classes)}")
|
|
||||||
lines.append(f"- Uncovered lines : {total_uncovered}")
|
|
||||||
lines.append(f"- Partial branches: {total_partial} lines affected")
|
|
||||||
lines.append(f"- Missed branches : {total_missed_branches} branch paths")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("---")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("## Files Requiring Tests")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("> Each entry lists the SOURCE FILE PATH, the LINE NUMBERS that need")
|
|
||||||
lines.append("> coverage, and the METHODS that contain those gaps.")
|
|
||||||
lines.append("> Write or extend unit/integration tests to exercise these paths.")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
# Sort: most uncovered lines first
|
|
||||||
sorted_classes = sorted(classes, key=lambda c: -(c.missed_lines + len(c.partial_branch_lines)))
|
|
||||||
|
|
||||||
for cls in sorted_classes:
|
|
||||||
source = cls.source_path or f"(source unknown) {cls.java_class_name}"
|
|
||||||
lines.append(f"### `{source}`")
|
|
||||||
lines.append(f"**Class**: `{cls.java_class_name}`")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
if cls.uncovered_lines:
|
|
||||||
lines.append(f"#### ❌ Uncovered Lines")
|
|
||||||
lines.append(f"Lines not executed at all: `{_compact_ranges(cls.uncovered_lines)}`")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("**Methods with uncovered lines:**")
|
|
||||||
for method in cls.methods:
|
|
||||||
uncov = [l for l in cls.uncovered_lines
|
|
||||||
if method.first_line and l >= method.first_line]
|
|
||||||
# heuristic: only attribute if there are uncovered lines near the method start
|
|
||||||
if method.missed_instructions > 0:
|
|
||||||
sig = f"`{method.name}{method.descriptor}`"
|
|
||||||
pct = method.line_coverage_pct
|
|
||||||
lines.append(f" - {sig} — {pct:.0f}% instruction coverage")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
if cls.partial_branch_lines:
|
|
||||||
lines.append(f"#### ⚠️ Partial Branch Coverage (Missing Conditional Paths)")
|
|
||||||
lines.append(f"Lines where not all branches are taken: `{_compact_ranges(cls.partial_branch_lines)}`")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("**Methods with branch gaps:**")
|
|
||||||
for method in cls.methods:
|
|
||||||
if method.missed_branches > 0:
|
|
||||||
sig = f"`{method.name}{method.descriptor}`"
|
|
||||||
pct = method.branch_coverage_pct
|
|
||||||
missing = method.missed_branches
|
|
||||||
lines.append(f" - {sig} — {pct:.0f}% branch coverage ({missing} branch path(s) never taken)")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
lines.append("**Action**: Add tests that exercise the above lines/branches.")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("---")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
lines.append("## Quick Reference: All Uncovered Locations")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("Copy-paste friendly list for IDE navigation or grep:")
|
|
||||||
lines.append("")
|
|
||||||
lines.append("```")
|
|
||||||
for cls in sorted_classes:
|
|
||||||
src = cls.source_path or cls.java_class_name
|
|
||||||
if cls.uncovered_lines:
|
|
||||||
for ln in cls.uncovered_lines:
|
|
||||||
lines.append(f"{src}:{ln} # uncovered line")
|
|
||||||
if cls.partial_branch_lines:
|
|
||||||
for ln in cls.partial_branch_lines:
|
|
||||||
lines.append(f"{src}:{ln} # partial branch")
|
|
||||||
lines.append("```")
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def format_json(classes: list[ClassCoverage]) -> str:
|
|
||||||
out = []
|
|
||||||
for cls in classes:
|
|
||||||
out.append({
|
|
||||||
"class": cls.java_class_name,
|
|
||||||
"source_path": cls.source_path,
|
|
||||||
"uncovered_lines": cls.uncovered_lines,
|
|
||||||
"partial_branch_lines": cls.partial_branch_lines,
|
|
||||||
"missed_branches": cls.missed_branches,
|
|
||||||
"methods": [
|
|
||||||
{
|
|
||||||
"name": m.name,
|
|
||||||
"descriptor": m.descriptor,
|
|
||||||
"first_line": m.first_line,
|
|
||||||
"line_coverage_pct": round(m.line_coverage_pct, 1),
|
|
||||||
"branch_coverage_pct": round(m.branch_coverage_pct, 1),
|
|
||||||
"missed_branches": m.missed_branches,
|
|
||||||
"missed_instructions": m.missed_instructions,
|
|
||||||
}
|
|
||||||
for m in cls.methods
|
|
||||||
if not m.is_fully_covered
|
|
||||||
],
|
|
||||||
})
|
|
||||||
return json.dumps(out, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
def format_markdown(classes: list[ClassCoverage]) -> str:
|
|
||||||
lines: list[str] = []
|
|
||||||
lines.append("# JaCoCo Missing Coverage Report\n")
|
|
||||||
for cls in sorted(classes, key=lambda c: cls.java_class_name):
|
|
||||||
lines.append(f"## {cls.java_class_name}")
|
|
||||||
if cls.source_path:
|
|
||||||
lines.append(f"**File**: `{cls.source_path}`\n")
|
|
||||||
if cls.uncovered_lines:
|
|
||||||
lines.append(f"**Uncovered lines**: {_compact_ranges(cls.uncovered_lines)}\n")
|
|
||||||
if cls.partial_branch_lines:
|
|
||||||
lines.append(f"**Partial branches at lines**: {_compact_ranges(cls.partial_branch_lines)}\n")
|
|
||||||
lines.append("| Method | Line Coverage | Branch Coverage | Missed Branches |")
|
|
||||||
lines.append("|--------|--------------|-----------------|-----------------|")
|
|
||||||
for m in cls.methods:
|
|
||||||
if not m.is_fully_covered:
|
|
||||||
lines.append(
|
|
||||||
f"| `{m.name}` | {m.line_coverage_pct:.0f}% | "
|
|
||||||
f"{m.branch_coverage_pct:.0f}% | {m.missed_branches} |"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Entry point
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Report missing line & branch coverage from a JaCoCo XML report."
|
|
||||||
)
|
|
||||||
parser.add_argument("xml_file", help="Path to jacoco.xml report file")
|
|
||||||
parser.add_argument(
|
|
||||||
"--output", "-o",
|
|
||||||
choices=["agent", "json", "markdown"],
|
|
||||||
default="json",
|
|
||||||
help="Output format (default: agent)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--min-coverage",
|
|
||||||
type=float,
|
|
||||||
default=0.0,
|
|
||||||
help="Only report classes below this %% line coverage (0 = report all gaps)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--package-filter", "-p",
|
|
||||||
default=None,
|
|
||||||
help="Only report classes in this package prefix (e.g. com/example/service)",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
xml_path = Path(args.xml_file)
|
|
||||||
if not xml_path.exists():
|
|
||||||
print(f"ERROR: File not found: {xml_path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
classes = parse_jacoco_xml(str(xml_path))
|
|
||||||
|
|
||||||
# Apply package filter
|
|
||||||
if args.package_filter:
|
|
||||||
prefix = args.package_filter.replace(".", "/")
|
|
||||||
classes = [c for c in classes if c.class_name.startswith(prefix)]
|
|
||||||
|
|
||||||
# Apply min-coverage filter
|
|
||||||
if args.min_coverage > 0:
|
|
||||||
def _line_pct(c: ClassCoverage) -> float:
|
|
||||||
total = c.total_lines
|
|
||||||
return 100.0 * c.covered_lines / total if total else 100.0
|
|
||||||
|
|
||||||
classes = [c for c in classes if _line_pct(c) < args.min_coverage]
|
|
||||||
|
|
||||||
if not classes:
|
|
||||||
print("✅ No coverage gaps found matching the given filters.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if args.output == "agent":
|
|
||||||
print(format_agent(classes))
|
|
||||||
elif args.output == "json":
|
|
||||||
print(format_json(classes))
|
|
||||||
elif args.output == "markdown":
|
|
||||||
print(format_markdown(classes))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -132,29 +132,24 @@ object MoveValidator:
|
|||||||
val kingSq = Square(File.E, rank)
|
val kingSq = Square(File.E, rank)
|
||||||
val enemy = color.opposite
|
val enemy = color.opposite
|
||||||
|
|
||||||
if ctx.board.pieceAt(kingSq) != Some(Piece(color, PieceType.King)) then return Set.empty
|
if !ctx.board.pieceAt(kingSq).contains(Piece(color, PieceType.King)) ||
|
||||||
if GameRules.isInCheck(ctx.board, color) then return Set.empty
|
GameRules.isInCheck(ctx.board, color) then Set.empty
|
||||||
|
else
|
||||||
|
val kingsideSq = Option.when(
|
||||||
|
rights.kingSide &&
|
||||||
|
ctx.board.pieceAt(Square(File.H, rank)).contains(Piece(color, PieceType.Rook)) &&
|
||||||
|
List(Square(File.F, rank), Square(File.G, rank)).forall(s => ctx.board.pieceAt(s).isEmpty) &&
|
||||||
|
!List(Square(File.F, rank), Square(File.G, rank)).exists(s => isAttackedBy(ctx.board, s, enemy))
|
||||||
|
)(Square(File.G, rank))
|
||||||
|
|
||||||
var result = Set.empty[Square]
|
val queensideSq = Option.when(
|
||||||
|
rights.queenSide &&
|
||||||
|
ctx.board.pieceAt(Square(File.A, rank)).contains(Piece(color, PieceType.Rook)) &&
|
||||||
|
List(Square(File.B, rank), Square(File.C, rank), Square(File.D, rank)).forall(s => ctx.board.pieceAt(s).isEmpty) &&
|
||||||
|
!List(Square(File.D, rank), Square(File.C, rank)).exists(s => isAttackedBy(ctx.board, s, enemy))
|
||||||
|
)(Square(File.C, rank))
|
||||||
|
|
||||||
if rights.kingSide then
|
kingsideSq.toSet ++ queensideSq.toSet
|
||||||
val rookSq = Square(File.H, rank)
|
|
||||||
val transit = List(Square(File.F, rank), Square(File.G, rank))
|
|
||||||
if ctx.board.pieceAt(rookSq).contains(Piece(color, PieceType.Rook)) &&
|
|
||||||
transit.forall(s => ctx.board.pieceAt(s).isEmpty) &&
|
|
||||||
!transit.exists(s => isAttackedBy(ctx.board, s, enemy)) then
|
|
||||||
result += Square(File.G, rank)
|
|
||||||
|
|
||||||
if rights.queenSide then
|
|
||||||
val rookSq = Square(File.A, rank)
|
|
||||||
val emptySquares = List(Square(File.B, rank), Square(File.C, rank), Square(File.D, rank))
|
|
||||||
val transitSqs = List(Square(File.D, rank), Square(File.C, rank))
|
|
||||||
if ctx.board.pieceAt(rookSq).contains(Piece(color, PieceType.Rook)) &&
|
|
||||||
emptySquares.forall(s => ctx.board.pieceAt(s).isEmpty) &&
|
|
||||||
!transitSqs.exists(s => isAttackedBy(ctx.board, s, enemy)) then
|
|
||||||
result += Square(File.C, rank)
|
|
||||||
|
|
||||||
result
|
|
||||||
|
|
||||||
def legalTargets(ctx: GameContext, from: Square): Set[Square] =
|
def legalTargets(ctx: GameContext, from: Square): Set[Square] =
|
||||||
ctx.board.pieceAt(from) match
|
ctx.board.pieceAt(from) match
|
||||||
|
|||||||
@@ -11,21 +11,18 @@ object Renderer:
|
|||||||
private val AnsiBlackPiece = "\u001b[30m" // black text
|
private val AnsiBlackPiece = "\u001b[30m" // black text
|
||||||
|
|
||||||
def render(board: Board): String =
|
def render(board: Board): String =
|
||||||
val sb = new StringBuilder
|
val rows = (0 until 8).reverse.map { rank =>
|
||||||
sb.append(" a b c d e f g h\n")
|
val cells = (0 until 8).map { file =>
|
||||||
for rank <- (0 until 8).reverse do
|
val sq = Square(File.values(file), Rank.values(rank))
|
||||||
sb.append(s"${rank + 1} ")
|
val isLightSq = (file + rank) % 2 != 0
|
||||||
for file <- 0 until 8 do
|
val bgColor = if isLightSq then AnsiLightSquare else AnsiDarkSquare
|
||||||
val sq = Square(File.values(file), Rank.values(rank))
|
board.pieceAt(sq) match
|
||||||
val isLightSq = (file + rank) % 2 != 0
|
|
||||||
val bgColor = if isLightSq then AnsiLightSquare else AnsiDarkSquare
|
|
||||||
val cellContent = board.pieceAt(sq) match
|
|
||||||
case Some(piece) =>
|
case Some(piece) =>
|
||||||
val fgColor = if piece.color == Color.White then AnsiWhitePiece else AnsiBlackPiece
|
val fgColor = if piece.color == Color.White then AnsiWhitePiece else AnsiBlackPiece
|
||||||
s"$bgColor$fgColor ${piece.unicode} $AnsiReset"
|
s"$bgColor$fgColor ${piece.unicode} $AnsiReset"
|
||||||
case None =>
|
case None =>
|
||||||
s"$bgColor $AnsiReset"
|
s"$bgColor $AnsiReset"
|
||||||
sb.append(cellContent)
|
}.mkString
|
||||||
sb.append(s" ${rank + 1}\n")
|
s"${rank + 1} $cells ${rank + 1}"
|
||||||
sb.append(" a b c d e f g h\n")
|
}.mkString("\n")
|
||||||
sb.toString
|
s" a b c d e f g h\n$rows\n a b c d e f g h\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user