T05 — ride_accumulator.dart with the cross-batch anchor intact (12 tests). T06 — ride_export.dart, GPX 1.1 and GeoJSON (15 tests), parsed with real parsers rather than substring matching. T07 — parity harness extended to cover export byte output. GPX and GeoJSON are byte-identical across Kotlin and Dart: same length, same FNV hash, including the escaped hostile name and all %.7f/%.1f formatting. Two harness bugs found and fixed while building it. String.hashCode is not comparable across Java and Dart, so text comparison used FNV-1a instead. And a missing jar let a failed Kotlin compile pass as a green run -- run.sh now checks for the artifact and exits non-zero, the same failure mode as the v2 sweep that hid a compile error behind /dev/null. Phase 1 done: 79 tests passing, analyze clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
3.7 KiB
Dart
103 lines
3.7 KiB
Dart
// ignore_for_file: avoid_print -- this is a command-line harness; stdout is the output.
|
|
// Dart side of the cross-language parity harness (T07).
|
|
// Must stay fixture-for-fixture identical to tool/parity/main.kt.
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:rippr/src/domain/models.dart';
|
|
import 'package:rippr/src/export/ride_export.dart';
|
|
import 'package:rippr/src/geo/geo.dart';
|
|
import 'package:rippr/src/stats/ride_statistics.dart';
|
|
|
|
/// Same LCG as the Kotlin oracle. Dart ints are 64-bit two's complement on the VM and
|
|
/// multiplication wraps, matching Kotlin's Long.
|
|
class Lcg {
|
|
Lcg(this._s);
|
|
int _s;
|
|
double nextDouble() {
|
|
_s = _s * 6364136223846793005 + 1442695040888963407;
|
|
final bits = (_s >>> 11) & ((1 << 53) - 1);
|
|
return bits / (1 << 53);
|
|
}
|
|
}
|
|
|
|
/// FNV-1a 32-bit. Dart's String.hashCode differs from Java's, so it cannot be used to
|
|
/// compare text across languages -- this can.
|
|
int fnv1a(String s) {
|
|
var h = 2166136261;
|
|
for (final c in s.codeUnits) {
|
|
h ^= c;
|
|
h = (h * 16777619) & 0xFFFFFFFF;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
TrackPoint p(int seg, int ts, double lat, double lon, double sp, double alt, int id) =>
|
|
TrackPoint(id: id, tripId: 1, segmentId: seg, timestamp: ts,
|
|
latitude: lat, longitude: lon, speedKmh: sp, altitudeM: alt);
|
|
|
|
void main() {
|
|
void out(String k, Object v) => print('$k=$v');
|
|
|
|
out('haversine_calgary_edmonton', haversineMeters(51.0447, -114.0719, 53.5461, -113.4938));
|
|
out('haversine_short_hop', haversineMeters(51.0447, -114.0719, 51.04480, -114.0719));
|
|
out('perp_beyond_end', perpendicularDistanceMeters(
|
|
const LatLon(51.003, -114.0), const LatLon(51.000, -114.0), const LatLon(51.002, -114.0)));
|
|
|
|
final ride = List.generate(21600,
|
|
(i) => LatLon(51.0 + i * 0.00001, -114.0 + math.sin(i / 100.0) * 0.001));
|
|
final simplified = simplify(ride, 5.0);
|
|
out('simplify_count', simplified.length);
|
|
out('simplify_last_lat', simplified.last.lat);
|
|
out('path_length_ride', pathLengthMeters(ride));
|
|
|
|
final rng = Lcg(42);
|
|
final noisy = List.generate(600,
|
|
(i) => p(1, i * 1000, 51.0, -114.0, 0.0, 1000.0 + (rng.nextDouble() * 16.0 - 8.0), i + 1));
|
|
out('noisy_gain', computeSummary(noisy).elevationGainM);
|
|
out('noisy_loss', computeSummary(noisy).elevationLossM);
|
|
|
|
final climb = List.generate(101,
|
|
(i) => p(1, i * 1000, 51.0 + i * 0.0001, -114.0, 50.0, 1000.0 + i, i + 1));
|
|
out('climb_gain', computeSummary(climb).elevationGainM);
|
|
|
|
final run = List.generate(100,
|
|
(i) => p(1, i * 1000, 51.0 + i * 0.0001, -114.0, 40.0, 1000.0, i + 1));
|
|
final s = computeSummary(run);
|
|
out('run_distance', s.distanceM);
|
|
out('run_moving_millis', s.movingMillis);
|
|
out('run_avg_speed', s.avgMovingSpeedKmh);
|
|
|
|
final prof = elevationProfile(run, maxSamples: 200);
|
|
out('profile_size', prof.length);
|
|
out('profile_last_distance', prof.last.distanceM);
|
|
|
|
final hist = speedHistogram(run, bucketKmh: 10);
|
|
out('hist_buckets', hist.length);
|
|
out('hist_first_millis', hist.first.millis);
|
|
|
|
// --- export byte output -------------------------------------------------
|
|
const etrip = Trip(
|
|
id: 1,
|
|
startedAt: 1700000000000,
|
|
endedAt: 1700000600000,
|
|
state: TripState.completed,
|
|
distanceM: 12345.6,
|
|
maxSpeedKmh: 118.4);
|
|
const esegs = [
|
|
Segment(id: 1, tripId: 1, startedAt: 0, endedAt: 1),
|
|
Segment(id: 2, tripId: 1, startedAt: 2, endedAt: 3),
|
|
];
|
|
final epts = <TrackPoint>[
|
|
for (final sg in esegs)
|
|
for (var i = 0; i < 5; i++)
|
|
p(sg.id, 1700000000000 + i * 1000, 51.0447 + i * 0.0001, -114.0719, 72.0,
|
|
1045.0, i + 1),
|
|
];
|
|
final g = gpx(etrip, esegs, epts, "Sam & Dave's <ride>");
|
|
final j = geoJson(etrip, esegs, epts, "Sam & Dave's <ride>");
|
|
out('gpx_len', g.length);
|
|
out('gpx_fnv', fnv1a(g));
|
|
out('geojson_len', j.length);
|
|
out('geojson_fnv', fnv1a(j));
|
|
}
|