T03 — telemetry.dart, format.dart, live_telemetry.dart, plus pure domain models (Trip/Segment/TrackPoint/RideStats) with no persistence dependency, so Drift can map to them in T08 rather than the domain depending on the database. T04 — ride_statistics.dart including ElevationAccumulator, ported structurally faithfully: moving average, reversal hysteresis, gainIncludingPending, and the finish() reconciliation against lastRaw. T07 (early, because T04 forced it) — tool/parity/ drives identical fixtures through the real Kotlin files and the Dart port, then diffs. Result: every value byte-identical, including noisy_gain=38.959594555022136 to the last digit. The sole difference is run_avg_speed, where Kotlin's 32-bit Float widens to double with artefacts Dart's binary64 does not reproduce. Documented, not papered over. That harness settled a real question. The ported elevation test failed at 50.9m against Kotlin's 35m bound, which looked like a porting bug. It was not: Kotlin's and Dart's Random(42) are different streams. On a shared LCG fixture both produce 39.0m -- which would also fail Kotlin's own bound. The native guard passes on seed luck rather than on a property of the algorithm. The Dart test now uses the shared LCG, asserts bit-equality with Kotlin, and sets its bound from measured behaviour (25 seeds spanned 24.7-46.7m). 52 tests passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
112 lines
3.5 KiB
Dart
112 lines
3.5 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:rippr/src/domain/models.dart';
|
|
import 'package:rippr/src/telemetry/telemetry.dart';
|
|
|
|
/// Ported from `com.rippr.TelemetryTest`.
|
|
///
|
|
/// Tolerances are carried over as-is. Note the speed assertions: Kotlin's `Float`
|
|
/// widened to `Double` yields 88.5 only within 1e-4, which is exactly why the original
|
|
/// used that tolerance. Dart's uniform binary64 is exact here, but the tolerance is kept
|
|
/// so the two suites stay comparable line for line.
|
|
void main() {
|
|
test('converts meters per second to kmh', () {
|
|
expect(msToKmh(10), closeTo(36, 0.001));
|
|
expect(msToKmh(0), closeTo(0, 0.001));
|
|
});
|
|
|
|
test('gps jitter below the noise floor reports as zero', () {
|
|
expect(sanitizeSpeedKmh(0.9), closeTo(0, 0.001));
|
|
expect(sanitizeSpeedKmh(double.nan), closeTo(0, 0.001));
|
|
expect(sanitizeSpeedKmh(-5), closeTo(0, 0.001));
|
|
});
|
|
|
|
test('real speeds pass through untouched', () {
|
|
expect(sanitizeSpeedKmh(97.3), closeTo(97.3, 0.001));
|
|
});
|
|
|
|
test('rejects fixes worse than the accuracy budget', () {
|
|
expect(isUsableFix(12), isTrue);
|
|
expect(isUsableFix(0), isTrue,
|
|
reason: 'unknown accuracy must not be discarded');
|
|
expect(isUsableFix(120), isFalse);
|
|
});
|
|
|
|
test('formats duration as hh mm ss', () {
|
|
expect(formatDuration(0), '00:00:00');
|
|
expect(formatDuration(-1), '00:00:00');
|
|
expect(formatDuration(65000), '00:01:05');
|
|
expect(formatDuration(7384000), '02:03:04');
|
|
});
|
|
|
|
test('encodes a batch as the documented payload shape', () {
|
|
final points = [
|
|
const TrackPoint(
|
|
id: 7,
|
|
tripId: 3,
|
|
segmentId: 5,
|
|
timestamp: 1700000000000,
|
|
latitude: 51.0447,
|
|
longitude: -114.0719,
|
|
speedKmh: 88.5,
|
|
altitudeM: 1045.0,
|
|
accuracyM: 4.2,
|
|
bearingDeg: 271.5,
|
|
),
|
|
];
|
|
|
|
final json =
|
|
jsonDecode(encodeBatch('device-abc', points)) as Map<String, dynamic>;
|
|
|
|
expect(json['device_id'], 'device-abc');
|
|
final first = (json['points'] as List).first as Map<String, dynamic>;
|
|
expect(first['id'], 7);
|
|
expect(first['trip_id'], 3);
|
|
expect(first['segment_id'], 5);
|
|
expect(first['ts'], 1700000000000);
|
|
expect(first['lat'] as double, closeTo(51.0447, 1e-6));
|
|
expect(first['lon'] as double, closeTo(-114.0719, 1e-6));
|
|
expect(first['speed_kmh'] as double, closeTo(88.5, 1e-4));
|
|
});
|
|
|
|
test('a batch spanning two segments labels each point individually', () {
|
|
// A batch is drawn by id and can cross a boundary, so identity must travel with
|
|
// the point rather than the batch.
|
|
final points = [
|
|
const TrackPoint(
|
|
id: 1,
|
|
tripId: 3,
|
|
segmentId: 5,
|
|
timestamp: 1,
|
|
latitude: 51.0,
|
|
longitude: -114.0,
|
|
speedKmh: 40,
|
|
altitudeM: 1000.0),
|
|
const TrackPoint(
|
|
id: 2,
|
|
tripId: 3,
|
|
segmentId: 6,
|
|
timestamp: 2,
|
|
latitude: 51.1,
|
|
longitude: -114.0,
|
|
speedKmh: 40,
|
|
altitudeM: 1000.0),
|
|
];
|
|
|
|
final array =
|
|
(jsonDecode(encodeBatch('d', points)) as Map<String, dynamic>)['points']
|
|
as List;
|
|
|
|
expect((array[0] as Map)['segment_id'], 5);
|
|
expect((array[1] as Map)['segment_id'], 6);
|
|
expect((array[0] as Map)['trip_id'], 3);
|
|
});
|
|
|
|
test('encodes an empty batch without failing', () {
|
|
final json =
|
|
jsonDecode(encodeBatch('d', const [])) as Map<String, dynamic>;
|
|
expect((json['points'] as List).length, 0);
|
|
});
|
|
}
|