Complete Phase 1: accumulator, exports, and full parity coverage
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>
This commit is contained in:
@@ -3,6 +3,9 @@ import com.rippr.data.Segment
|
||||
import com.rippr.geo.Geo
|
||||
import com.rippr.geo.LatLon
|
||||
import com.rippr.stats.RideStatistics
|
||||
import com.rippr.data.Trip
|
||||
import com.rippr.data.TripState
|
||||
import com.rippr.export.RideExport
|
||||
|
||||
// A deterministic LCG implemented identically in Kotlin and Dart, so both languages
|
||||
// see the SAME noise sequence. Neither language's built-in Random can do this.
|
||||
@@ -14,6 +17,14 @@ class Lcg(private var s: Long) {
|
||||
}
|
||||
}
|
||||
|
||||
// FNV-1a 32-bit. String.hashCode() differs between Java and Dart, so it cannot be used
|
||||
// to compare text across languages -- this can.
|
||||
fun fnv1a(s: String): Long {
|
||||
var h = 2166136261L
|
||||
for (ch in s) { h = h xor ch.code.toLong(); h = (h * 16777619L) and 0xFFFFFFFFL }
|
||||
return h
|
||||
}
|
||||
|
||||
fun p(seg: Long, ts: Long, lat: Double, lon: Double, sp: Float, alt: Double, id: Long) =
|
||||
TrackPoint(id = id, tripId = 1, segmentId = seg, timestamp = ts,
|
||||
latitude = lat, longitude = lon, speedKmh = sp, altitudeM = alt)
|
||||
@@ -55,5 +66,19 @@ fun main() {
|
||||
|
||||
val hist = RideStatistics.speedHistogram(run, 10)
|
||||
out("hist_buckets", hist.size)
|
||||
|
||||
// --- export byte output -------------------------------------------------
|
||||
val etrip = Trip(id = 1, startedAt = 1_700_000_000_000L, endedAt = 1_700_000_600_000L,
|
||||
state = TripState.COMPLETED, distanceM = 12_345.6, maxSpeedKmh = 118.4f)
|
||||
val esegs = listOf(Segment(id = 1, tripId = 1, startedAt = 0, endedAt = 1),
|
||||
Segment(id = 2, tripId = 1, startedAt = 2, endedAt = 3))
|
||||
val epts = esegs.flatMap { sg -> (0 until 5).map {
|
||||
p(sg.id, 1_700_000_000_000L + it * 1000L, 51.0447 + it * 0.0001, -114.0719, 72f, 1045.0, it.toLong() + 1) } }
|
||||
val g = RideExport.gpx(etrip, esegs, epts, "Sam & Dave's <ride>")
|
||||
val j = RideExport.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))
|
||||
out("hist_first_millis", hist.first().millis)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// 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';
|
||||
|
||||
@@ -18,6 +20,17 @@ class Lcg {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
@@ -61,4 +74,29 @@ void main() {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -25,13 +25,22 @@ fi
|
||||
# The files actually under test are copied verbatim from the native repo — never
|
||||
# reimplemented. Only the Room-annotated holders and one Telemetry constant are stubbed,
|
||||
# so the algorithm files compile without androidx or org.json on the classpath.
|
||||
cp "$NATIVE/app/src/main/java/com/rippr/geo/Geo.kt" "$WORK/"
|
||||
cp "$NATIVE/app/src/main/java/com/rippr/geo/Geo.kt" "$WORK/"
|
||||
cp "$NATIVE/app/src/main/java/com/rippr/stats/RideStatistics.kt" "$WORK/"
|
||||
cp "$NATIVE/app/src/main/java/com/rippr/export/RideExport.kt" "$WORK/"
|
||||
cp "$HERE/stubs.kt" "$HERE/telemetry_stub.kt" "$HERE/main.kt" "$WORK/"
|
||||
|
||||
echo "== compiling Kotlin oracle =="
|
||||
( cd "$WORK" && kotlinc Geo.kt RideStatistics.kt stubs.kt telemetry_stub.kt main.kt \
|
||||
-include-runtime -d parity.jar 2>&1 | grep -v '^warning:' || true )
|
||||
( cd "$WORK" && kotlinc Geo.kt RideStatistics.kt RideExport.kt stubs.kt \
|
||||
telemetry_stub.kt main.kt -include-runtime -d parity.jar 2>&1 \
|
||||
| grep -v '^warning:' || true )
|
||||
|
||||
# A missing jar means compilation failed. Never let that pass as a green run -- a v2
|
||||
# sweep hid exactly this behind a redirect and reported four identical results.
|
||||
if [ ! -f "$WORK/parity.jar" ]; then
|
||||
echo "Kotlin oracle failed to compile (see errors above)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAVA_BIN="${JAVA_HOME:+$JAVA_HOME/bin/}java"
|
||||
"$JAVA_BIN" -jar "$WORK/parity.jar" | grep '=' | sort > "$WORK/kotlin.txt"
|
||||
|
||||
@@ -8,3 +8,12 @@ data class TrackPoint(
|
||||
data class Segment(
|
||||
val id: Long = 0, val tripId: Long, val startedAt: Long, val endedAt: Long? = null,
|
||||
) { val isOpen: Boolean get() = endedAt == null }
|
||||
|
||||
enum class TripState { RECORDING, PAUSED, COMPLETED }
|
||||
data class Trip(
|
||||
val id: Long = 0, val startedAt: Long, val endedAt: Long? = null,
|
||||
val name: String? = null, val state: TripState = TripState.RECORDING,
|
||||
val distanceM: Double = 0.0, val movingMillis: Long = 0,
|
||||
val maxSpeedKmh: Float = 0f, val elevationGainM: Double = 0.0,
|
||||
val pointCount: Int = 0,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user