MotorSignal-App/test/forensic_persistence_crash_...

94 lines
3.5 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:drift/native.dart';
import 'package:drift/drift.dart' as drift;
import 'package:motorcycle_dash/database/database.dart';
void main() {
group('Phase 9 Forensic Validation: Persistence, Database & Export Integrity', () {
late AppDatabase db;
setUp(() {
db = AppDatabase.forTesting(NativeDatabase.memory());
});
tearDown(() async {
await db.close();
});
test('Telemetry samples batch insert and query accurately in chronological order', () async {
final now = DateTime(2026, 8, 28, 12, 0, 0);
final rideId = await db.into(db.rides).insert(RidesCompanion.insert(
startTime: now,
));
final batchSamples = List.generate(50, (i) {
return TelemetrySamplesCompanion(
rideId: drift.Value(rideId),
timestamp: drift.Value(now.add(Duration(milliseconds: i * 50))),
latitude: drift.Value(37.7749 + (i * 0.0001)),
longitude: drift.Value(-122.4194),
speed: drift.Value(45.0 + (i * 0.2)),
heading: drift.Value(90.0),
gpsAccuracy: drift.Value(3.2),
leanAngle: drift.Value(12.5),
longitudinalG: drift.Value(0.1),
lateralG: drift.Value(0.2),
);
});
await db.batch((batch) {
batch.insertAll(db.telemetrySamples, batchSamples);
});
final retrieved = await (db.select(db.telemetrySamples)
..where((t) => t.rideId.equals(rideId))
..orderBy([(t) => drift.OrderingTerm(expression: t.timestamp)])
).get();
expect(retrieved.length, 50);
expect(retrieved.first.speed, 45.0);
expect(retrieved.last.speed, closeTo(54.8, 0.01));
expect(retrieved.first.gpsAccuracy, 3.2);
});
test('Crash Recovery: Incomplete ride with null endTime is recovered with correct timestamp', () async {
final startTime = DateTime(2026, 8, 28, 14, 0, 0);
final rideId = await db.into(db.rides).insert(RidesCompanion.insert(
startTime: startTime,
movingTime: const drift.Value(120),
stoppedTime: const drift.Value(30),
));
// Simulate crash by adding breadcrumbs then leaving endTime null
final lastBreadcrumbTime = startTime.add(const Duration(seconds: 150));
await db.into(db.telemetrySamples).insert(TelemetrySamplesCompanion.insert(
rideId: rideId,
timestamp: lastBreadcrumbTime,
latitude: const drift.Value(37.7749),
longitude: const drift.Value(-122.4194),
));
// Execute crash recovery logic
final unclosedRides = await (db.select(db.rides)..where((r) => r.endTime.isNull())).get();
expect(unclosedRides.length, 1);
for (final r in unclosedRides) {
final lastSample = await (db.select(db.telemetrySamples)
..where((t) => t.rideId.equals(r.id))
..orderBy([(t) => drift.OrderingTerm(expression: t.timestamp, mode: drift.OrderingMode.desc)])
..limit(1)
).getSingleOrNull();
final calculatedEndTime = lastSample?.timestamp ?? r.startTime.add(Duration(seconds: r.movingTime + r.stoppedTime));
await (db.update(db.rides)..where((t) => t.id.equals(r.id))).write(
RidesCompanion(endTime: drift.Value(calculatedEndTime)),
);
}
final updatedRide = await (db.select(db.rides)..where((r) => r.id.equals(rideId))).getSingle();
expect(updatedRide.endTime, lastBreadcrumbTime);
expect(updatedRide.endTime!.difference(updatedRide.startTime).inSeconds, 150);
});
});
}