This is a from-clean-slate test sequence for the seven native desktop programs (builder, counter, scanner, viewer, blBuilder, blCounter, blScanner) and the three shared libraries they depend on (builder-core, counter-core, scanner-core). Follow it in order; each step assumes the previous ones succeeded.
This document was written after actually running every step below, on a clean checkout, on 2026-07-21 — see Confirmed results for what that run produced. If a later run of this sequence produces different results, something changed; don't assume this document is still accurate without re-running it.
- JDK 21+ (jlink/jpackage are part of the JDK — no separate install).
- Python 3.9+ with
pyyaml,numpy,Pillowfortest-harness's scripts (pip install -r test-harness/requirements.txt). poppler(brew install poppler/apt install poppler-utils) for PDF rasterization.- A real bBuilder-generated ballot PDF+YAML pair at
~/bBuilder_ballots/(see step 4 below — the full sequence produces one as a byproduct of testingbuilder/blBuilder, so this isn't a separate prerequisite in practice).
builder, counter, scanner (Swing) and blBuilder, blCounter,
blScanner (JavaFX) all depend on one of three shared Maven artifacts —
none of the six apps duplicates this logic:
cd builder-core && mvn -q install -DskipTests && cd ..
cd counter-core && mvn -q install -DskipTests && cd ..
cd scanner-core && mvn -q install -DskipTests && cd ..(package_all_desktop.sh, in step 3, does this for you automatically —
run it standalone here only if you want to run mvn test per-app before
packaging anything.)
Run in each of these nine directories (the three -core libraries, then
the six apps — order doesn't matter here since step 1 already installed
the libraries they depend on):
for m in builder-core counter-core scanner-core \
builder blBuilder counter blCounter scanner blScanner; do
echo "== $m =="
cd "$m"
if [ -f mvnw ]; then ./mvnw -q clean test; else mvn -q clean test; fi
cd ..
doneSee What mvn test covers below for
what's actually being verified in each module, and
Known environment-dependent behavior
for the handful of tests that don't reliably pass/fail the same way on
every machine — that's expected, not a regression.
package_all_desktop.sh (repo root) installs the three -core libraries,
then jlinks + jpackages each app into a real, standalone .app (macOS) /
app-image (Linux) / folder-with-.exe (Windows) — this is the actual
distributable artifact end users run, so building it is part of testing,
not just mvn test passing:
./package_all_desktop.sh # all 7
./package_all_desktop.sh builder counter scanner viewer # just the four recommended onesConfirm each app actually exists afterward:
for m in builder counter scanner viewer blBuilder blCounter blScanner; do
echo -n "$m: "; ls "$m/target/dist/"
doneNote: this script resolves each built jar by glob (target/<artifactId>-*.jar),
not a hardcoded version — the packaging step used to hardcode a jar
filename with the project's version number baked in, which broke the
first time the version changed after the script was written. If a future
version bump ever reintroduces a hardcoded filename (<name>-<version>.jar),
that's a regression of this same bug.
mvn test proves the engine and (where GUI infrastructure exists) the
screens work in isolation. These test-harness/ scripts additionally
prove the whole pipeline — a real ballot design, marked with realistic
voter behavior, geometrically distorted the way a real scan would be, then
actually counted — end to end, against ground truth.
test-harness/run_all.sh builds a full 8-contest, 6-combination election
through a live bBuilder (start it first: ./mvnw spring-boot:run in
bBuilder/), which is what actually produces the ~/bBuilder_ballots/
fixture the rest of this sequence (and builder/blBuilder's own mvn test suites) depend on. This is the real clean-slate test of the ballot
generation path, even though it drives it through bBuilder's REST API
rather than builder's own Swing UI (builder/blBuilder don't currently
have a GUI-click-through pipeline script the way counter/blCounter do
— see the coverage gaps noted below):
cd test-harness
./run_all.sh --reset --copies 1 # --reset wipes bCounter's DB first — required for a valid accuracy numberWatch for the final Accuracy: NN.N% (M/N) line and Missing in DB /
Missing in GT counts — see
Confirmed results for what a clean run should show.
This is the one part of the sequence that drives the actual Swing/JavaFX Start Count button via robot clicks, not just the counting engine:
cd test-harness
./run_desktop_gui_pipeline.sh # regenerates the corpus fresh, then runs both GUI testsThis corpus must be freshly regenerated before each run. Successfully
scanning it renames every image to .counted (so a real restart resumes
instead of re-scanning everything — see CountingService/VoteRecordService);
re-running a GUI test against an already-fully-scanned corpus will fail
fast with "All images in this folder have already been counted" instead of
counting anything. That's the counting engine working correctly, not a
bug — just re-run ./run_desktop_gui_pipeline.sh (or its --skip-corpus
flag if you specifically want to test the "restart on nothing left"
error path).
There is no equivalent scripted GUI pipeline for scanner/blScanner —
see the coverage gap noted below. scanner-core's own mvn test
(step 2) is currently the closest thing to an end-to-end test: it drives
ScanService through a mock command backend (a shell script that writes
fake image files instead of talking to real scanner hardware) exercising
the same start/monitor/count/batch-log path a real naps2/scanimage
backend would.
| Module | What's tested |
|---|---|
builder-core |
No tests of its own — its model/service/repository classes (package com.mjtrac.ballot) are exercised through blBuilder's and builder's test suites below, not directly. |
counter-core |
BallotViewServiceResolveImagePathTest (image-path resolution for the viewer). The bulk of the counting engine (corner detection, barcode reading, marker analysis, RCV tabulation) is tested through bCounter/blCounter's suites, not here. |
scanner-core |
ScanServiceNotesTest (start/end note logging) + ScanServiceMockScanTest (real scan-and-count path via a mock command backend — added 2026-07-21, previously untested). |
builder |
BuilderEndToEndTest (full CRUD → real PDF, headless, through builder-core's own repositories), RegionPartyShortcutTest (the "use single party/region" reassign-then-delete shortcuts), TestElectionBuilderTest (the headless bBuilder-REST fallback used by run_all.sh), plus real AssertJ-Swing screen tests (added 2026-07-21) — MainFrameNavigationGuiTest (every menu item), one CRUD round-trip test per screen (ElectionCrudGuiTest, RegionCrudGuiTest, PartyCrudGuiTest, JurisdictionCrudGuiTest, BallotTypeCrudGuiTest, BallotLanguageCrudGuiTest, BallotCombinationCrudGuiTest, BallotDesignTemplateCrudGuiTest, AdminUserCrudGuiTest), ContestCascadeGuiTest (the save→Candidates→Regions dialog cascade — see below), and PrintPanelGuiTest (real PDF generation). |
blBuilder |
BallotDesignTemplateTest, BallotGenerationServiceTest, BallotGenerationTest, ContestAssignmentServiceTest, ExportServiceTest, MeasurementUtilTest, TemplateReflectedInBallotTest (all headless service-layer), plus real TestFX/Monocle screen tests: AdminScreenTest, BallotCombinationScreenTest, BallotDesignTemplateScreenTest, ContestScreenTest, LoginScreenTest, PartyScreenTest, PrintScreenTest, RegionScreenTest. |
counter |
CountingServiceIntegrationTest (full scan → results report, real fixture images), CountingResumeIntegrationTest (the stop/restart-resumes-correctly regression test — added 2026-07-20), CountingPipelineGuiTest (AssertJ-Swing, real Start Counting button clicks against the desktop_pipeline corpus — see coverage gap below). |
blCounter |
BarcodeReaderServiceTest, CornerDetectionTest (300 DPI precision fixtures), MarkerAnalysisServiceTest, RcvTabulationServiceTest (all headless), plus TestFX/Monocle screen tests: AccountScreenTest, AdminScreenTest, CountingScreenTest, LoginScreenTest, ResultsScreenTest, ViewerScreenTest, and CountingPipelineGuiTest (TestFX, real Start Count clicks against the desktop_pipeline corpus). |
scanner |
Only ScreenshotGenerator (a documentation-screenshot tool, not a test). No functional or GUI test exists in this module — no assertj-swing dependency. |
blScanner |
No test directory exists at all. No TestFX/Monocle dependency has been added to this module's pom.xml. |
viewer |
BallotViewPanelLayoutTest, ContestCandidateWindowTest, HomographyTest (all headless), plus ScreenshotGenerator. |
macOS Accessibility permission gates real Swing/AWT robot input.
counter's CountingPipelineGuiTest uses AssertJ-Swing, which drives a
real window via java.awt.Robot — this requires whatever process
launched the JVM (Terminal, iTerm, a CI runner) to hold Accessibility
permission (macOS: System Settings → Privacy & Security → Accessibility).
Without it, this test skips itself with a clear message rather than
hanging or failing — but the exact behavior is genuinely non-deterministic
across runs on a machine where that permission is inconsistently granted
to background/spawned processes: the same test, run back to back with no
code changes, has been observed to (a) complete a full real scan
successfully, (b) throw ActionFailedException and skip cleanly, and (c)
silently fail to deliver the click/keystroke events at all (caught by this
test's fail-fast check, which reports the actual folder field values and
message-label text rather than hanging for the full 20-minute timeout).
None of these is a bug — grant the permission and re-run if you need a
guaranteed pass, or run under Linux CI with Xvfb, which isn't subject to
this restriction. builder's new AssertJ-Swing screen tests (see the
builder row above) are gated by this exact same permission, via the
identical AbstractBuilderGuiTest.robotAction() skip-cleanly wrapper —
RegionCrudGuiTest's "Use Single Region" test has been observed to skip
this way with the message "The component to click is out of the boundaries
of the screen" rather than the Accessibility-specific message, which is the
same underlying real-Robot-on-real-screen fragility, just a different
symptom of it.
blBuilder/blCounter's TestFX tests do not have this problem — they
render off-screen via Monocle, so they're not gated by OS input focus at
all, and were 100% reliable across every run in this session.
Coverage gaps (not fixed as part of this document — noted so they're not mistaken for accidental omissions):
scannerhas noassertj-swingdependency and no GUI-click-through test at all (builderclosed this gap 2026-07-21 — see thebuilderrow above).blScannerhas zero automated tests of any kind.- There is no scripted end-to-end pipeline for
scanner/blScanneranalogous torun_desktop_gui_pipeline.sh— closing that gap would mean adding TestFX+Monocle toblScannerandassertj-swingtoscanner, which is meaningfully more work than extending the existing mock-backend approach and wasn't done here.
Run on 2026-07-21, clean checkout, macOS, JDK 25 (Homebrew), against the
current main branch:
| Module | Result |
|---|---|
builder-core, counter-core, scanner-core |
Installed cleanly. |
builder |
20/20 tests pass, 1 skip (BuilderEndToEndTest 2, RegionPartyShortcutTest 2, TestElectionBuilderTest 1, plus 15 new AssertJ-Swing screen-test methods across 12 classes — 1 skip in RegionCrudGuiTest's "Use Single Region" test, real-Robot-on-real-screen fragility, see Known environment-dependent behavior). This pass caught and fixed two real bugs nothing had caught before: a ClassCastException on "Manage Candidates"/"Assign Regions"/"Translations" for any already-saved Contest reopened by double-click (ContestPanel cast a Window that could be a Dialog to Frame), and — more seriously — every candidate on a newly created Contest being silently duplicated in the database on the ordinary create-contest→add-candidates→assign-regions workflow (contestRepo.save(contest)'s return value was discarded across the cascade, so JPA merge() re-inserted the same still-transient candidates a second time). |
blBuilder |
79/79 tests pass across all 15 test classes. |
counter |
3/3 tests pass, 1 skip (CountingPipelineGuiTest — Accessibility permission not granted to this shell on this run). |
blCounter |
Full suite passes; CountingPipelineGuiTest passes cleanly (23.03s) against a freshly regenerated corpus. |
scanner-core (mock scan) |
ScanServiceMockScanTest 2/2, ScanServiceNotesTest 4/4. |
scanner, blScanner |
No automated tests to run (see coverage gaps above). |
package_all_desktop.sh (all 7 apps) |
All 7 .app bundles produced successfully after fixing the hardcoded-version bug (see step 3). |
run_all.sh --reset --copies 1 (full 3,060-image election, 300 DPI) |
100.0% accuracy (88/88), 0 mismatches, 0 missing in DB, 0 missing in GT. |
run_desktop_gui_pipeline.sh equivalent (isolated 255-image corpus, 150 DPI, freshly regenerated ballot) |
255/255 counted, 0 flagged for review. |
| Ballot rotation tolerance (dedicated probe, current ballot design) | Improved corner-detection's fallback search, but this search never gets used for ballots with less than seven degrees of rotation. |
A ballot rotated seven degrees, scaled down:
No test-harness material, run from a genuinely clean setup against the ballot designs pbss currently generates, fails to count votes correctly.
When a ballot's corner detection does fail (well outside normal scanning
conditions), the failure mode is fail-safe, not fail-silent:
voteRecord.persist() — the only code path that writes votes to the
database — is only reached when a ballot's corner detection succeeds. A
ballot that fails is excluded from the tally entirely, logged by name to
review_required.txt, and (past scanner.max-review-before-stop) halts
the whole scan rather than continuing silently past it — never a wrong
vote attributed to the wrong candidate.
