Test suite cleanup, JaCoCo coverage report and tests before packaging (#62) (#112)

* docs: add the test-coverage design spec (#62)

Records the state found for issue #62 (JUnit classes under src/main that
never run, test libraries in the shipped app image, dead 2023/2024 scratch
tests, no coverage figure), the decisions taken (delete, move to src/test,
JaCoCo report-only without a threshold, tests before packaging), what is
deliberately left to separate issues, and the definition of done.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: move the JUnit classes out of src/main and drop the dead 2023 tests (#62)

Gradle's test task scans src/test only, so the six JUnit classes under
src/main compiled on every build and were never executed. They move to
src/test of the module that owns the class under test, keeping their
package names: On4KstProtocolTest, On4KstSocketThreadTest and
ChatMemberDirectionOpportunityTest within core; Utils4KSTFrequencyTest,
FrequencyTextParserTest and ChatPreferencesStationMapVisibilityTest from
app-desktop into core, next to the sibling tests that already live there,
because they test core classes only. All 26 of them run and pass.

The fully commented-out MessageBusManagementThreadFrequencyContextTest is
deleted. Eight files under core/src/test/java/kst4contest/test from
2023/2024 go as well: they carry no assertion, five of them annotate a
static main() with @Test, which Jupiter never runs, and some write the real
preferences file, send real UDP broadcasts or open the real user database
when started by hand. The five main() scratch programs under src/main
(ReadUDPByWintestThreadTest, PatternMatcherTest, mapTest, RegexTester,
locatorUtils/TestLocatorUtils) and MockKstServer, which only an unused
import in ChatController still referenced, are removed too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* build: make JUnit and Mockito test-only dependencies (#62)

With no test class left under src/main, the implementation declarations of
junit-jupiter-api and mockito-core in core and app-desktop lose their only
justification. They put junit-jupiter-api, junit-platform-commons,
opentest4j, mockito-core, byte-buddy, byte-buddy-agent and objenesis on the
runtime classpath and into lib/app of every app image, Flatpak and
installer. The duplicate catalog entries junit-jupiter-api,
mockito-core-compile and the mockitoCompile version go with them.

Verified: `:app-desktop:dependencies --configuration runtimeClasspath` no
longer lists any of the seven artifacts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: state that test failures fail the build and tests live under src/test (#62)

AGENTS.md and docs/PROJECT_CONTEXT.md still said the build configuration
had allowed test failures to pass. That was true of the Maven build; under
Gradle a test failure fails the build, and only PMD and SpotBugs run with
ignoreFailures. Both documents now say so, note that test classes live under
src/test only and that JUnit and Mockito are test-only dependencies, and
keep the rule to read the test-result XML rather than the exit code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* build: add a JaCoCo coverage report (#62)

The jacoco plugin is applied to every module from the root build with the
tool version held in the catalog. jacocoTestReport now depends on test and
writes XML and HTML per module; a root task jacocoAggregatedReport merges
core and app-desktop into build/reports/jacoco/aggregated/ (XML plus HTML).
i18n-generator is excluded: it is a build tool and its coverage says nothing
about the shipped application. Kotlin classes are counted, because the
Kotlin plugin adds its output to the main source set the report reads.

Report only, on purpose. There is no jacocoTestCoverageVerification and no
threshold: a minimum set at today's number would trip on the next unrelated
change, and one set at a target would fail every build until tests exist for
the number's sake. The figure is published so it can be read when deciding
what to test next; the reasoning is in
docs/superpowers/specs/2026-10-11-test-coverage-design.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci: run the tests before packaging and publish coverage (#62)

The nightly and tagged-release workflows built seven package formats from
commits nobody had tested on the way: pr-compile-check.yml runs the suite
for pull requests only, and main can be pushed to directly. Both workflows
now carry a copy of that test job, including the libgl1 step and the
XML-based summary, and every packaging job declares `needs: test`. Copied
rather than factored into a reusable workflow on purpose: the comments in
the job explain decisions the packaging workflows should carry with them,
and three copies read better than one indirection. build-docs-pdf runs
pandoc only and is left alone; publish-flatpak-repo and release-tag are
gated through the jobs they already need.

The PR test job additionally runs jacocoTestReport and
jacocoAggregatedReport once the tests have run (not `if: always()`: a
report from an incomplete run would mislead), prints a line/branch coverage
table per module and aggregated into the job summary from the JaCoCo XML,
and uploads the aggregated HTML report as the coverage-report artifact for
14 days whenever it exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: describe the coverage report in the project context (#62)

A short paragraph on JaCoCo in docs/PROJECT_CONTEXT.md: report only and
why, where the per-module and aggregated reports land, which Gradle tasks
write them, that i18n-generator is excluded, how the PR job summary and the
coverage-report artifact show the figure, and that the nightly and
tagged-release workflows run the tests before any packaging job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: round-trip every ChatPreferences field through the XML file (#62)

Drive all public single-argument setters by reflection to non-default
values, write preferences.xml, read it back and compare every getter.
Fields that are not persisted on purpose are listed with their evidence;
two real reader/writer gaps (second-category locator, sniffer words) are
documented with a BUG marker instead of being fixed here. Two further
tests load the bundled template and a legacy file without configVersion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: cover the DBController schema migration from older user_version values (#62)

Open the seeded template database and hand-built release 1.1 and 1.3
schema fixtures with a DBController and verify that PRAGMA user_version
reaches the current level, every column of the current CREATE TABLE
statements exists, legacy callsign keys are merged to base callsigns and
existing rows and flags survive. A further test proves that reopening an
up-to-date database changes neither schema nor rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore: drop the committed JVM crash report and ignore future ones (#62)

hs_err_pid26344.log in the repository root was a HotSpot crash dump that
had been committed by accident. It carries no information about the
project and must not travel with the sources.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: drop the dead QSO sniffer word list that corrupted the callsign list on load (#103)

ChatPreferences carried a second sniffer list, lstNotify_QSOSniffer_sniffedWordsList,
that no UI, no manual and no consumer outside ChatPreferences ever touched. It was
still written to preferences.xml as <snifferWords><w>, and the matching reader block
was a copy of the <snifferCallSigns> block: it cleared the word list and then added
every word, normalised as a callsign, to lstNotify_QSOSniffer_sniffedCallSignList.
Any entry that ever reached the word list therefore leaked into QSO monitoring on
every load.

Remove the field, its accessors, the write block and the read block. A <snifferWords>
element in an older preferences file is now ignored like any unknown element; the
callsign list and its persistence are unchanged.

ChatPreferencesSnifferListTest loads a file that carries both lists and asserts that
only the <snifferCallSigns> entry is monitored. The round-trip test loses its
exclusion entry for the removed setter.

Fixes #103

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: remove the second-category locator that could never be set (#104)

ChatPreferences declared stn_loginLocatorSecondCat with the compiled-in default
"jn49fk". Nothing wrote it to or read it from preferences.xml, no settings tab
exposed it and no caller ever invoked the setter, so the field always held its
default. On4KstConnectionManager.configureMainChat nevertheless compared that
default with the station's locator and, with the second chat enabled, raised the
warning "ON4KST uses one locator per TCP session. The second-chat locator ... is
ignored" whenever the station's locator was not jn49fk, which is practically every
installation.

ON4KST uses one locator per TCP session by design, so a second-category locator can
never become meaningful. Remove the field, its accessors, the comparison and the
warning; the rest of configureMainChat is unchanged. The round-trip test loses its
exclusion entry for the removed setter.

No automated test covers configureMainChat: it is private, works on the manager's
private Session and needs a mocked ChatController plus a live socket handshake, so a
harness would far exceed the scope of this fix. The compile of core and app-desktop
and the shorter exclusion list are the evidence that nothing references the member.

Fixes #104

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rsclub2_2andClaude Fable 5.1 authored and GitHub committed 2026-10-11 12:13:31 +02:00
1 parent 4a90ae7d30
commit 5491cbcf8b
42 files changed
+1205 -2025

No files matched your search

+4 -2
View File
@@ -199,8 +199,10 @@ CR/LF framing, XML framing, ports/transports, callsign normalization and frequen
- Use the repository Gradle wrapper (`.\gradlew.bat` on Windows).
- The project uses Java 21, Kotlin 2.2 and Compose Multiplatform 1.8.x at this context snapshot. JavaFX has been removed; no `org.openjfx` dependency remains.
- JUnit 5/Mockito, PMD and SpotBugs are part of the verification environment.
- Build/test configuration has historically allowed some test/static-analysis failures not to fail the process exit code. Always read actual summaries/reports.
- JUnit 5/Mockito, PMD and SpotBugs are part of the verification environment. JUnit and Mockito are test-only dependencies; nothing of them reaches the shipped app image.
- Test classes live under `src/test` only. Until #62 a few JUnit classes sat under `src/main`, compiled on every build and were never executed; that is the historical reason the two modules once declared JUnit and Mockito as `implementation` dependencies.
- A test failure fails the build. Only PMD and SpotBugs run with `ignoreFailures`, so their findings do not. Still read the test-result XML under `*/build/test-results/test/` rather than inferring a pass from the exit code.
- Coverage is measured with JaCoCo as a report only: no `jacocoTestCoverageVerification`, no threshold (rationale in `docs/superpowers/specs/2026-10-11-test-coverage-design.md`). `./gradlew jacocoTestReport` writes a per-module report under `*/build/reports/jacoco/test/` (XML and HTML); `./gradlew jacocoAggregatedReport` merges `core` and `app-desktop` into `build/reports/jacoco/aggregated/jacoco.xml` and `.../aggregated/html/`. Both run the tests first when needed. `i18n-generator` still gets its per-module report but is left out of the aggregated one: it is build tooling and says nothing about the shipped application. The PR workflow prints line and branch coverage per module and aggregated into the job summary and uploads the aggregated HTML as the `coverage-report` artifact for 14 days. The nightly and tagged-release workflows run the test job first; no packaging job starts before it has passed.
## Documentation Surfaces
@@ -0,0 +1,161 @@
# Konzept: Testabdeckung (#62)
> Status: Konzept, freigegeben am 2026-10-11, umgesetzt im Zweig `feature/62-test-coverage`.
> Alle Zeilenanker beziehen sich auf `main` bei `4a90ae7d`.
Issue #62 verlangt einen Blick auf die Testsuite als Ganzes: Was läuft, was sieht nur so aus,
als liefe es, und wie viel des Codes erreicht die Suite tatsächlich. Dieses Dokument hält den
vorgefundenen Stand fest, die getroffenen Entscheidungen und das, was bewusst eigenen Issues
überlassen wird.
## Das Problem
Vier Befunde, gemessen am Stand `4a90ae7d`.
**Sieben JUnit-Klassen liegen unter `src/main` und werden nie ausgeführt.** Gradles
`test`-Task durchsucht nur `src/test`. Deshalb werden
`core/src/main/java/kst4contest/controller/On4KstProtocolTest.java`,
`On4KstSocketThreadTest.java`, `MessageBusManagementThreadFrequencyContextTest.java`
(vollständig auskommentiert), `core/src/main/java/kst4contest/model/ChatMemberDirectionOpportunityTest.java`
und die drei Klassen unter `app-desktop/src/main/java/kst4contest/test/`
(`Utils4KSTFrequencyTest`, `FrequencyTextParserTest`, `ChatPreferencesStationMapVisibilityTest`)
bei jedem Build kompiliert und zählen für nichts. Der Protokolltest darunter nagelt die
exakten `LOGINC|`-, `SDONE|`- und `ACHAT|`-Frames fest, der Socket-Test genau ein CR/LF je
Frame. Beides sind Invarianten, die `AGENTS.md` ausdrücklich nennt, und keine davon hat seit
der Maven-Zeit ein Build geprüft.
**Wegen dieser Klassen wird JUnit und Mockito mit der Anwendung ausgeliefert.** `core` und
`app-desktop` deklarieren `implementation(libs.junit.jupiter.api)` und
`implementation(libs.mockito.core.compile)`. Damit liegen `junit-jupiter-api`,
`junit-platform-commons`, `opentest4j`, `mockito-core`, `byte-buddy`, `byte-buddy-agent` und
`objenesis` auf dem Laufzeit-Klassenpfad und in `lib/app` jedes App-Image, Flatpak und
Installers. `docs/CONCEPT_IN_APP_UPDATE.md` nennt das bereits als Vorbedingung für einen
signierten Update-Pfad: Ein Bytecode-Manipulations-Agent in einem signierten Produktionsbinary
ist das Falsche, was man erklären müsste.
**Acht Dateien unter `core/src/test/java/kst4contest/test/` sind toter Scratch-Code von
2023/2024.** `TestChatPreferences`, `TestReadUDPASListenerThread`,
`TestReadUDPUCXListenerThread`, `UDPAirScoutSendMessages`, `testTimeGeneration`,
`TestApplicationFileUtils`, `TestDBController` und `TestLocatorUtils` enthalten keine einzige
Assertion. Fünf davon tragen `@Test` an einer `public static void main`, die Jupiter nie
ausführt; die anderen drei haben gar keine Annotation. Manche schreiben beim manuellen Start die
echte Preferences-Datei, senden echten UDP-Broadcast oder öffnen die echte Nutzerdatenbank.
Fünf weitere `main()`-Programme derselben Art liegen unter `src/main`
(`ReadUDPByWintestThreadTest`, `PatternMatcherTest`, `mapTest`, `RegexTester`,
`locatorUtils/TestLocatorUtils`), dazu `MockKstServer`, auf den nur noch ein ungenutzter Import
in `ChatController` verweist.
**Es gibt keine Abdeckungszahl.** Die Suite ist durch die Compose-Migration auf mehrere hundert
Tests gewachsen, aber niemand kann sagen, welchen Teil von `core` sie erreicht. Drei Bereiche
sind bekannt dünn: der XML-Roundtrip von `ChatPreferences` jenseits der Handvoll Felder, die
Einzeltests festnageln, die Schema-Migration in `DBController`, und die Netzwerk-Threads
(`ReadThread`/`WriteThread`, die UDP-Listener), die bestenfalls je einen Loopback-Test haben.
## Entscheidungen
**Tote Tests löschen, nicht retten.** Ein Test ohne Assertion dokumentiert nichts, und die mit
Seiteneffekten sind eine Gefahr, sobald jemand „alle Tests“ aus der IDE startet. Ihre
Gegenstände sind entweder anderswo abgedeckt (`ChatPreferences*Test`,
`DBControllerProfileDatabaseTest`, `DirectionUtilsConsoleOutputTest`) oder werden zu den zwei
neuen Testbereichen unten. Der auskommentierte `MessageBusManagementThreadFrequencyContextTest`
wird ebenfalls gelöscht; was er abgedeckt hätte
(`MessageBusManagementThread.hasExplicitBareFrequencyContext` und `Band.fromPrefix`), ist im
Umsetzungsbericht vermerkt, damit es mit der Frequenz-Parsing-Arbeit aufgegriffen und nicht
stillschweigend verloren wird.
**Tests liegen ausschließlich unter `src/test`.** Die sechs lebenden JUnit-Klassen wandern nach
`src/test` des Moduls, dem die getestete Klasse gehört, mit unveränderten Paketnamen, neben die
Geschwistertests, die dort schon liegen (`FrequencyTextParserRegressionTest`,
`ChatPreferencesStationMapClusteringTest`). Die drei Klassen aus `app-desktop` testen
ausschließlich `core`-Klassen und ziehen deshalb nach `core`. Ohne Testklasse unter `src/main`
entfallen die beiden `implementation`-Deklarationen und die doppelten Katalogeinträge
(`junit-jupiter-api`, `mockito-core-compile`, Version `mockitoCompile`). JUnit und Mockito sind
nur noch `testImplementation`, was die vorhandenen `testImplementation`-Zeilen ohnehin schon
sagten. Nachgewiesen über `:app-desktop:dependencies --configuration runtimeClasspath` ohne die
sieben Artefakte und über die Jar-Liste von `createDistributable`.
**JaCoCo nur als Bericht, ohne Schwellwert.** Das `jacoco`-Plugin wird aus dem Root-Build auf
jedes Modul angewandt, `jacocoTestReport` läuft nach `test` und schreibt XML und HTML je Modul,
und ein Root-Task `jacocoAggregatedReport` führt `core` und `app-desktop` zu einem Bericht unter
`build/reports/jacoco/aggregated/` zusammen. `i18n-generator` bleibt außen vor: Es ist ein
Bauwerkzeug, seine Abdeckung sagt nichts über die ausgelieferte Anwendung. Es gibt bewusst keine
`jacocoTestCoverageVerification` und keinen Mindestprozentsatz. Ein heute gesetzter Schwellwert
stünde bei der Zahl, die zufällig gerade herauskommt, und wäre damit ein Stolperdraht für die
nächste unabhängige Änderung statt einer Aussage über Qualität; ein Schwellwert auf Zielniveau
ließe jeden Build scheitern, bis jemand Tests um der Zahl willen schreibt. Der Bericht existiert,
damit die Zahl in jedem Pull Request sichtbar ist und bei der Entscheidung hilft, was als
Nächstes zu testen ist. Sie zu heben ist Sache der unten gelisteten Issues, nicht der
Build-Konfiguration.
**Tests laufen vor dem Packaging.** Der Nightly- und der Release-Workflow bauen sieben
Paketformate aus einem Commit, den unterwegs niemand getestet hat: `pr-compile-check.yml` führt
die Suite nur für Pull Requests aus, und `main` kann direkte Pushes erhalten. Beide Workflows
bekommen eine Kopie des `test`-Jobs aus `pr-compile-check.yml`, einschließlich des
`libgl1`-Schritts und der XML-basierten Zusammenfassung, und jeder Packaging-Job deklariert
`needs: test`. Der Job wird dupliziert statt in einen wiederverwendbaren Workflow ausgelagert:
Drei Kopien von vierzig Zeilen lesen sich leichter als eine Indirektion, und die Kommentare im
Job erklären Entscheidungen, die die Packaging-Workflows bei sich tragen sollen. Der
PR-Workflow führt zusätzlich `jacocoAggregatedReport` aus, schreibt aus dem JaCoCo-XML eine
Abdeckungstabelle in die Job-Zusammenfassung und lädt den aggregierten HTML-Bericht für
vierzehn Tage als Artefakt `coverage-report` hoch.
**Zwei neue Testbereiche entstehen im selben Zweig, aber in eigenen Commits.** Issue #62 nennt
den `ChatPreferences`-Roundtrip und die `DBController`-Schema-Migration als die zwei
ungetesteten Risikobereiche, die sich jetzt schließen lassen. Es sind gewöhnliche
Testergänzungen, die vom Aufräumen nicht abhängen; sie landen nach dem Aufräumen, damit ihre
Wirkung im aggregierten Bericht sichtbar wird.
## Bewusst außerhalb des Umfangs
Jeder dieser Punkte ist eine echte Lücke, die unterwegs gefunden wurde, und jeder wird ein
eigenes Issue, weil jeder eine Entscheidung braucht, die dieses Aufräumen nicht nebenbei
treffen sollte:
- **Refactoring von `MessageBusManagementThread` für Testbarkeit.** Frequenzextraktion,
Worked-Prüfung und der DX-Cluster-Spot-Pfad sind heute nur über einen konstruierten Thread
mit gemockten Mitspielern erreichbar. Sie in testbare Einheiten zu teilen berührt die
Nachrichten-Pipeline und braucht ein eigenes Konzept.
- **Loopback-Tests für die Netzwerk-Threads.** `On4KstSocketThreadTest` deckt `ReadThread`
und `WriteThread` ab; die UDP-Listener für Win-Test, UCXLog und AirScout und der
PSTRotator-Client haben kein Gegenstück. Ein Loopback-Harness mit Port-0-Socket je Protokoll
ist die richtige Form, aber das Framing jedes Protokolls verdient eine Prüfung, bevor ein Test
es festnagelt.
- **Compose-Screen-Tests.** Die State-Tests und die zwei Settings-Abdeckungsnetze prüfen, dass
Setter aufgerufen werden; nur `runComposeUiTest` kann prüfen, dass ein Wert den Bildschirm
erreicht hat. Welche Screens diesen Aufwand wert sind, ist eine Oberflächenentscheidung.
- **Pfadanalyse-Mathematik.** `PathPropagationAssessment` und die Terrainprofil-Berechnung
haben Layout-Tests, aber keine numerischen Referenzfälle. Referenzwerte brauchen eine Quelle,
die sich zitieren lässt.
- **Entfernen von `Thread.sleep`.** Mehrere Tests und einige Produktionspfade warten noch auf
die Wanduhr. Sie durch injizierbare Uhren oder Latches zu ersetzen ist mechanisch, aber breit.
## Definition von „fertig“ für #62
- Unter keinem `src/main` bleibt eine mit `@Test` annotierte Klasse oder ein Import von
`org.junit` oder `org.mockito`.
- `./gradlew clean build` führt die verschobenen Tests aus; die Ergebnis-XML unter
`*/build/test-results/test/` meldet null Fehlschläge und null Fehler.
- `./gradlew :app-desktop:dependencies --configuration runtimeClasspath` listet keines von
`junit`, `mockito`, `byte-buddy`, `objenesis`, `opentest4j`, und
`app-desktop/build/compose/binaries/main/app/KST4Contest/lib/app` enthält kein Jar dieser
Namen.
- `./gradlew jacocoAggregatedReport` schreibt `build/reports/jacoco/aggregated/jacoco.xml` und
einen HTML-Bericht; die PR-Job-Zusammenfassung zeigt eine Abdeckungstabelle und bietet das
Artefakt `coverage-report` an.
- `nightly-artifacts.yml` und `tagged-release.yml` führen den Test-Job aus, und kein
Packaging-Job startet, bevor er bestanden ist.
- `AGENTS.md` und `docs/PROJECT_CONTEXT.md` behaupten nicht mehr, Testfehler könnten den Build
passieren; sie sagen, dass nur PMD und SpotBugs mit `ignoreFailures` laufen.
- Die zwei neuen Testbereiche (`ChatPreferences`-Roundtrip, `DBController`-Schema-Migration)
liegen im Zweig, und die Punkte außerhalb des Umfangs existieren als Issues.
## Was unverändert bleibt
- Testverhalten: Keine Assertion wird abgeschwächt und kein Test deaktiviert, damit der Umzug
kompiliert.
- Der Produktionscode, bis auf den entfernten `MockKstServer`-Import in `ChatController`.
- PMD und SpotBugs behalten `ignoreFailures`; ihre Befunde werden wie bisher aus den Berichten
gelesen.
- Die Packaging-Jobs selbst: Es kommt nur `needs: test` hinzu.
- Keine neue Laufzeitabhängigkeit. JaCoCo ist ein Gradle-Plugin, das die Test-JVM
instrumentiert; nichts davon erreicht das Jar.