Golden data
As of 2026-10-19 (vulture-ios 3176c4cc4, vulture-android 85fd9cc6).
Android’s results must match iOS’s. This is checked against golden data: JSON files that iOS test targets write from the iOS code, and that Android’s unit tests read. The data is never edited by hand. When the iOS analysis changes, export again and commit the new files to vulture-android.
The exporters (vulture-ios)
Section titled “The exporters (vulture-ios)”The exporters are Swift Testing tests that are skipped unless an environment variable switches them on. With xcodebuild, pass each variable with a TEST_RUNNER_ prefix.
| Exporter | File | Switch | Output | Writes |
|---|---|---|---|---|
| Flights | vulture-ios/Packages/Flights/Tests/FlightsTests/Golden/GoldenDataExporter.swift, with GoldenFlightWriter*.swift, GoldenJSON.swift, GoldenReadme.swift | VULTURE_EXPORT_GOLDEN=1 | VULTURE_GOLDEN_DIR, default vulture-android/domain/flight/src/test/resources/golden (an absolute path on the owner’s Mac) | One <fixture>.json per fixture IGC in Packages/Flights/Tests/Flights/: the whole FlightEnricher result. Also a README.md stamped with VULTURE_GOLDEN_COMMIT |
| Units | vulture-ios/Packages/Preferences/Tests/PreferencesTests/UnitGoldenExporter.swift | VULTURE_EXPORT_GOLDEN=1 | Same directory | <fixture>.units.json: every formattable value of the flight JSON, formatted in every unit. It reads the flight files, so run it after the Flights exporter |
| Airspace | vulture-ios/Packages/Airspace/Tests/AirspaceTests/Golden/AirspaceGoldenExporter.swift (added 2026-10-19, a157827b2) | AIRSPACE_GOLDEN_IN and AIRSPACE_GOLDEN_OUT (both required); AIRSPACE_GOLDEN_FULL keeps outlines for files over 20 000 points | AIRSPACE_GOLDEN_OUT, meant for vulture-android/domain/airspace/src/test/resources/golden/ | Per file <file>.json (id, name, class, limits, frequency, transponder code, outline) and vectors.json with the parser vectors |
The Flights exporter never downloads anything. A fixture with a <fixture>.elevation.json cache is analysed with it (elevationSource: "cache"), and the others without elevations ("none"). No default observation zones are set, and the sport is detected.
Export commands (from the README the exporter writes):
cd vulture-ios/Packages/Flightsxcodebuild test -scheme Flights -destination 'platform=iOS Simulator,name=<iPhone>' \ -only-testing:FlightsTests/GoldenDataExporter \ TEST_RUNNER_VULTURE_EXPORT_GOLDEN=1 \ TEST_RUNNER_VULTURE_GOLDEN_COMMIT=$(git rev-parse --short HEAD)cd ../Preferencesxcodebuild test -scheme Preferences -destination 'platform=iOS Simulator,name=<iPhone>' \ -only-testing:PreferencesTests/UnitGoldenExporter \ TEST_RUNNER_VULTURE_EXPORT_GOLDEN=1What Android has now
Section titled “What Android has now”| Set | Path in vulture-android | Taken from | Read by |
|---|---|---|---|
Flight analysis: 14 fixtures × (<name>.json + <name>.units.json). The fixtures are ben300, cylinder_start_no_pev, hang_glider, iv_egc_task6, jelle500, jelle600, koen, para_freeflight, para_marcourt, pev_cylinder_start, slow_glider, tijl1000, wim1000, wim40 | domain/flight/src/test/resources/golden/ | iOS GoldenDataExporter and UnitGoldenExporter. The README names iOS commit 033b43827. Committed to vulture-android on 2026-10-10 (fd906f52) | :domain:flightdetail DetailFixtureTablesTest, which checks the summary and pages of koen, iv_egc_task6, hang_glider, para_freeflight and tijl1000 against the iOS figures and the iOS unit strings |
| Fixture IGCs (plus 3 OBGN 2026 task-2 flights), elevation caches, time zones | domain/flight/src/test/resources/{igc,elevation,timezone} | iOS Packages/Flights/Tests/Flights/ | :domain:flight tests |
Airspace: CTR, Klippeneck and Montlucon .cub, synthetic openAIP and OpenAir files, a Latin-1 CRLF OpenAir file, vectors.json, scan/ | domain/airspace/src/test/resources/golden/ | A Swift copy of the exporter run in a scratch copy of Packages/Airspace (vulture-android/domain/airspace/scripts/GoldenExporter.swift, laid out by scripts/format_golden.py), committed 2026-10-11 (909be6d7). iOS now has its own exporter (see above), which ios-changes A3 asked for | AirspaceGoldenTest, AirspaceVectorsTest, AirspaceInfringementScannerGoldenTest |
| Zone outlines | domain/waypoints/src/test/resources/zone_outlines_ios.txt | iOS | :domain:waypoints tests |
| Sync schema documents | domain/sync/src/test/resources/schema/ | Android (for iOS to match when it joins) | SchemaGoldenTest |
033b43827 could not be resolved in the local vulture-ios checkout. The exporter itself arrived in 361c3486c (2026-10-10). The stamped hash may come from a branch or a history that was later rewritten. Take the next export with VULTURE_GOLDEN_COMMIT set to a commit on main.
The flight analysis itself is also checked against iOS by one-off parity dumps. These are described in vulture-android/domain/flight/README.md (“Parity evidence”), where a throwaway Swift test and ParityDumpTest / ScoringParityDumpTest write the same JSON. A fresh golden export is still to do: ios-changes A1, once iOS has the FIXED(Q…) fixes. Until then FixedQuirksTest and restoreIosQuirks cover the difference.
JSON conventions
Section titled “JSON conventions”From the README the exporter writes:
- Doubles are written with
%.17g, so they round-trip exactly."inf","-inf"and"nan"stand for the non-finite values.nullis a Swiftnil. - Units are the iOS model’s: metres, m/s and seconds. The exceptions are
distanceTraveledper fix (km), and percentages, which run from 0 to 100. rawTimeis seconds since UTC midnight of the flight date, and keeps counting up across midnight.timeis the local time of day.- Indexes refer to arrays in the same file: a fix by its index in
fixes,thermalintothermalCandidates,legintolegs. header.idis the FNV-1a flight identity.
The floating-point rule
Section titled “The floating-point rule”Keep the floating-point operation order identical to iOS. Floating-point addition and multiplication are not associative. Summing in another order, fusing a multiply-add, or using a different library formula changes the last bits. A threshold check (a thermal start, a zone crossing, a landing index) can then flip, and the difference grows from there. Stated in vulture-android/CLAUDE.md:
- Port the Swift expression as it is written: same operand order, same intermediate variables, same loop order.
- Never use
String.formatfor numbers in domain code. Never useandroid.location.Locationdistances, or JTS or Turf geometry. Use:core:geoand:core:swift(Swift rounding andmin/maxsemantics). - Only use Java APIs that Android 10 has. For example,
Math.fmais Android 13+, so usecore.geo.fusedMultiplyAdd. - Copy the numeric quirks marked Copy in
vulture-android/docs/decisions.mdbug for bug. Fixing one is a decision for both platforms (see Port decisions).
Known differences that are allowed:
| Value | Difference | Why |
|---|---|---|
| Ground speed per fix, and values derived from it | At most 4 ulp (about 6e-16 relative) on about 10 % of fixes | The JVM’s sin/cos/atan2 against Apple’s libm. Compare with a tight tolerance |
| Task, speed and comparison doubles | Within 3.4e-11 relative | The same libm differences, amplified by the distance-travelled subtraction |
averageWindSpeed | Last bit, between iOS exports | iOS sums a dictionary in hash order, which changes per run. Android sums in time order. Compare with a tolerance |
| Integers, indexes, events, phases, counts | None: must be identical |
When to export again
Section titled “When to export again”- After any change to
Packages/Flights,Packages/Waypoints,Packages/Airspaceor the unit formatters on iOS. - After iOS applies a
FIXED(…)change from ios-changes. Android then drops the matchingrestoreIosQuirksswitch. - Commit the new files to vulture-android in one change, with the iOS commit in the README, and run
./gradlew testthere.
