MotorSignal-App/test/production_audit_test.dart

170 lines
6.1 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:drift/native.dart';
import 'package:motorcycle_dash/database/database.dart' hide TelemetrySample;
import 'package:motorcycle_dash/features/performance/performance_timer_service.dart';
import 'package:motorcycle_dash/models/telemetry_sample.dart';
import 'dart:async';
import 'package:drift/drift.dart' as drift;
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Phase 3 & 4: Speed & Conversion Validation', () {
test('m/s to MPH conversion factor is exact', () {
const double speedMps = 26.8224; // 60 MPH in m/s
final speedMph = speedMps * 2.23694;
expect((speedMph - 60.0).abs(), lessThan(0.01));
});
test('100 MPH speed conversion matches physical expectation', () {
const double speedMps = 44.704; // 100 MPH in m/s
final speedMph = speedMps * 2.23694;
expect((speedMph - 100.0).abs(), lessThan(0.01));
});
});
group('Phase 8 & 9: Database & Interrupted Ride Recovery', () {
late AppDatabase db;
setUp(() {
// Use in-memory SQLite database
db = AppDatabase.forTesting(NativeDatabase.memory());
});
tearDown(() async {
await db.close();
});
test('Unfinished interrupted ride is cleanly identified and recovered with non-null endTime', () async {
// Insert an unclosed ride (simulating an app kill during a ride)
final startTime = DateTime.now().subtract(const Duration(minutes: 45));
final rideId = await db.into(db.rides).insert(RidesCompanion.insert(
startTime: startTime,
distanceMiles: const drift.Value(24.5),
topSpeedMph: const drift.Value(78.0),
movingTime: const drift.Value(2400),
));
// Insert some telemetry samples for this ride
final lastSampleTime = startTime.add(const Duration(minutes: 40));
await db.into(db.telemetrySamples).insert(TelemetrySamplesCompanion.insert(
rideId: rideId,
timestamp: lastSampleTime,
speed: const drift.Value(65.0),
latitude: const drift.Value(37.7749),
longitude: const drift.Value(-122.4194),
));
// Query unclosed rides
final unclosed = await (db.select(db.rides)..where((r) => r.endTime.isNull())).get();
expect(unclosed.length, equals(1));
expect(unclosed.first.id, equals(rideId));
// Simulate the recovery logic
final lastSample = await (db.select(db.telemetrySamples)
..where((t) => t.rideId.equals(unclosed.first.id))
..orderBy([(t) => drift.OrderingTerm(expression: t.timestamp, mode: drift.OrderingMode.desc)])
..limit(1)
).getSingleOrNull();
final calculatedEndTime = lastSample?.timestamp ?? unclosed.first.startTime.add(Duration(seconds: unclosed.first.movingTime));
await (db.update(db.rides)..where((t) => t.id.equals(unclosed.first.id))).write(
RidesCompanion(endTime: drift.Value(calculatedEndTime)),
);
// Verify ride is now cleanly closed
final closedRide = await (db.select(db.rides)..where((r) => r.id.equals(rideId))).getSingle();
expect(closedRide.endTime, isNotNull);
expect(closedRide.endTime!.difference(lastSampleTime).inSeconds.abs(), lessThanOrEqualTo(1));
});
});
group('Phase 10: Performance Timer Interpolation', () {
late AppDatabase db;
setUp(() {
db = AppDatabase.forTesting(NativeDatabase.memory());
});
tearDown(() async {
await db.close();
});
test('Performance Timer reaches completed state on 0-60 threshold cross', () async {
final controller = StreamController<TelemetrySample>.broadcast();
final timerService = PerformanceTimerService(db, controller.stream);
timerService.setTarget(PerformanceTarget.sixty);
expect(timerService.state.state, equals(PerformanceState.ready));
final baseTime = DateTime.now();
// Send stationary sample
controller.add(TelemetrySample(
timestamp: baseTime,
speed: 0.0,
longitudinalG: 0.0,
));
await Future.delayed(const Duration(milliseconds: 10));
// Send launch sample (speed > 1.0, forward accel > 0.2G)
controller.add(TelemetrySample(
timestamp: baseTime.add(const Duration(milliseconds: 200)),
speed: 5.0,
longitudinalG: 0.8,
));
await Future.delayed(const Duration(milliseconds: 10));
expect(timerService.state.state, equals(PerformanceState.timing));
// Send intermediate speed sample
controller.add(TelemetrySample(
timestamp: baseTime.add(const Duration(milliseconds: 1500)),
speed: 35.0,
longitudinalG: 0.6,
));
await Future.delayed(const Duration(milliseconds: 10));
// Send crossing speed sample (62 MPH at 2800ms)
controller.add(TelemetrySample(
timestamp: baseTime.add(const Duration(milliseconds: 2800)),
speed: 62.0,
longitudinalG: 0.5,
));
await Future.delayed(const Duration(milliseconds: 20));
expect(timerService.state.state, equals(PerformanceState.completed));
expect(timerService.state.lastResult, isNotNull);
expect(timerService.state.lastResult!.target, equals(PerformanceTarget.sixty));
expect(timerService.state.lastResult!.timeSeconds, greaterThan(0.0));
expect(timerService.state.lastResult!.timeSeconds, lessThan(3.0));
timerService.dispose();
await controller.close();
});
});
group('Phase 24: GPX & CSV Export Validation', () {
test('GPX output contains valid XML structure and telemetry extensions', () {
final samples = [
TelemetrySample(
timestamp: DateTime.utc(2026, 8, 27, 10, 0, 0),
latitude: 37.7749,
longitude: -122.4194,
altitude: 15.0,
speed: 45.0,
heading: 180.0,
leanAngle: 12.5,
longitudinalG: 0.2,
lateralG: 0.1,
),
];
// Verify telemetry fields for export formatting
expect(samples.first.latitude, equals(37.7749));
expect(samples.first.longitude, equals(-122.4194));
expect(samples.first.speed, equals(45.0));
});
});
}