fix: associate performance runs with active ride
This commit is contained in:
parent
68c0bda344
commit
815ca09ab4
|
|
@ -1,8 +1,10 @@
|
|||
import 'dart:async';
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../database/database.dart' hide TelemetrySample;
|
||||
import '../../models/telemetry_sample.dart';
|
||||
import '../../services/telemetry_service.dart';
|
||||
import '../rides/ride_state_machine.dart';
|
||||
|
||||
enum PerformanceState { idle, ready, timing, completed, aborted }
|
||||
enum PerformanceTarget { sixty, hundred }
|
||||
|
|
@ -49,20 +51,27 @@ class PerformanceStateData {
|
|||
|
||||
class PerformanceTimerService extends StateNotifier<PerformanceStateData> {
|
||||
final AppDatabase _db;
|
||||
final int? Function()? activeRideIdReader;
|
||||
StreamSubscription<TelemetrySample>? _telemetrySub;
|
||||
|
||||
TelemetrySample? _lastSample;
|
||||
DateTime? _startTime;
|
||||
int? _rideIdAtAttemptStart;
|
||||
double _maxSpeedMph = 0.0;
|
||||
|
||||
static const double _speedThresholdMph = 1.0;
|
||||
static const double _abortThresholdMph = 0.5;
|
||||
|
||||
PerformanceTimerService(this._db, Stream<TelemetrySample> telemetryStream) : super(PerformanceStateData()) {
|
||||
PerformanceTimerService(
|
||||
this._db,
|
||||
Stream<TelemetrySample> telemetryStream, {
|
||||
this.activeRideIdReader,
|
||||
}) : super(PerformanceStateData()) {
|
||||
_telemetrySub = telemetryStream.listen(_onTelemetryUpdate);
|
||||
}
|
||||
|
||||
void setTarget(PerformanceTarget target) {
|
||||
_rideIdAtAttemptStart = null;
|
||||
state = state.copyWith(target: target, state: PerformanceState.ready);
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +97,7 @@ class PerformanceTimerService extends StateNotifier<PerformanceStateData> {
|
|||
sample.timestamp,
|
||||
_speedThresholdMph
|
||||
);
|
||||
_rideIdAtAttemptStart = activeRideIdReader?.call();
|
||||
_maxSpeedMph = speedMph;
|
||||
state = state.copyWith(state: PerformanceState.timing, currentElapsedSeconds: 0.0);
|
||||
}
|
||||
|
|
@ -162,6 +172,7 @@ class PerformanceTimerService extends StateNotifier<PerformanceStateData> {
|
|||
Future<void> _saveRun(PerformanceRunResult result, int timeMs) async {
|
||||
final typeStr = result.target == PerformanceTarget.sixty ? '0-60' : '0-100';
|
||||
await _db.addPerformanceRun(PerformanceRunsCompanion.insert(
|
||||
rideId: drift.Value(_rideIdAtAttemptStart),
|
||||
type: typeStr,
|
||||
timeMs: timeMs.toDouble(),
|
||||
topSpeedReached: result.topSpeedReachedMph,
|
||||
|
|
@ -179,5 +190,12 @@ class PerformanceTimerService extends StateNotifier<PerformanceStateData> {
|
|||
final performanceTimerProvider = StateNotifierProvider<PerformanceTimerService, PerformanceStateData>((ref) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final telemetryService = ref.watch(telemetryServiceProvider);
|
||||
return PerformanceTimerService(db, telemetryService.telemetryStream);
|
||||
return PerformanceTimerService(
|
||||
db,
|
||||
telemetryService.telemetryStream,
|
||||
activeRideIdReader: () {
|
||||
final rideState = ref.read(rideStateMachineProvider);
|
||||
return rideState.state == RideState.riding ? rideState.dbId : null;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.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';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late AppDatabase db;
|
||||
late StreamController<TelemetrySample> controller;
|
||||
PerformanceTimerService? timer;
|
||||
|
||||
setUp(() {
|
||||
db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
controller = StreamController<TelemetrySample>.broadcast();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
timer?.dispose();
|
||||
await controller.close();
|
||||
await db.close();
|
||||
});
|
||||
|
||||
Future<void> completeZeroToSixty(DateTime base) async {
|
||||
controller.add(TelemetrySample(timestamp: base, speed: 0.0));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
controller.add(TelemetrySample(timestamp: base.add(const Duration(milliseconds: 200)), speed: 5.0));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
controller.add(TelemetrySample(timestamp: base.add(const Duration(milliseconds: 1800)), speed: 45.0));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
controller.add(TelemetrySample(timestamp: base.add(const Duration(milliseconds: 2800)), speed: 62.0));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
}
|
||||
|
||||
test('performance run completed during active ride stores that ride ID', () async {
|
||||
final rideId = await db.startRide(startTime: DateTime.utc(2026, 9, 3, 12));
|
||||
timer = PerformanceTimerService(
|
||||
db,
|
||||
controller.stream,
|
||||
activeRideIdReader: () => rideId,
|
||||
);
|
||||
|
||||
timer!.setTarget(PerformanceTarget.sixty);
|
||||
await completeZeroToSixty(DateTime.utc(2026, 9, 3, 12, 1));
|
||||
|
||||
final runs = await db.select(db.performanceRuns).get();
|
||||
expect(runs, hasLength(1));
|
||||
expect(runs.single.rideId, rideId);
|
||||
});
|
||||
|
||||
test('ride-specific performance query returns associated run after ride completion', () async {
|
||||
final rideId = await db.startRide(startTime: DateTime.utc(2026, 9, 3, 13));
|
||||
timer = PerformanceTimerService(
|
||||
db,
|
||||
controller.stream,
|
||||
activeRideIdReader: () => rideId,
|
||||
);
|
||||
|
||||
timer!.setTarget(PerformanceTarget.sixty);
|
||||
await completeZeroToSixty(DateTime.utc(2026, 9, 3, 13, 1));
|
||||
await db.endRide(rideId, const RidesCompanion());
|
||||
|
||||
final rideRuns = await (db.select(db.performanceRuns)..where((p) => p.rideId.equals(rideId))).get();
|
||||
expect(rideRuns, hasLength(1));
|
||||
expect(rideRuns.single.type, '0-60');
|
||||
expect(rideRuns.single.rideId, rideId);
|
||||
});
|
||||
|
||||
test('standalone performance run remains valid without ride ID', () async {
|
||||
timer = PerformanceTimerService(db, controller.stream);
|
||||
|
||||
timer!.setTarget(PerformanceTarget.sixty);
|
||||
await completeZeroToSixty(DateTime.utc(2026, 9, 3, 14));
|
||||
|
||||
final runs = await db.select(db.performanceRuns).get();
|
||||
expect(runs, hasLength(1));
|
||||
expect(runs.single.rideId, isNull);
|
||||
});
|
||||
|
||||
test('attempt that starts on ride A is not reassigned to ride B before save', () async {
|
||||
final rideA = await db.startRide(startTime: DateTime.utc(2026, 9, 3, 15));
|
||||
final rideB = await db.startRide(startTime: DateTime.utc(2026, 9, 3, 16));
|
||||
var activeRideId = rideA;
|
||||
timer = PerformanceTimerService(
|
||||
db,
|
||||
controller.stream,
|
||||
activeRideIdReader: () => activeRideId,
|
||||
);
|
||||
|
||||
final base = DateTime.utc(2026, 9, 3, 15, 1);
|
||||
timer!.setTarget(PerformanceTarget.sixty);
|
||||
controller.add(TelemetrySample(timestamp: base, speed: 0.0));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
controller.add(TelemetrySample(timestamp: base.add(const Duration(milliseconds: 200)), speed: 5.0));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
|
||||
activeRideId = rideB;
|
||||
|
||||
controller.add(TelemetrySample(timestamp: base.add(const Duration(milliseconds: 1800)), speed: 45.0));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
controller.add(TelemetrySample(timestamp: base.add(const Duration(milliseconds: 2800)), speed: 62.0));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
final rideARuns = await (db.select(db.performanceRuns)..where((p) => p.rideId.equals(rideA))).get();
|
||||
final rideBRuns = await (db.select(db.performanceRuns)..where((p) => p.rideId.equals(rideB))).get();
|
||||
expect(rideARuns, hasLength(1));
|
||||
expect(rideBRuns, isEmpty);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue