MotorSignal-App/test/gnss_metrics_integrity_test...

713 lines
25 KiB
Dart

import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart';
import 'package:motorcycle_dash/models/telemetry_sample.dart';
import 'package:motorcycle_dash/features/rides/ride_state_machine.dart';
import 'package:motorcycle_dash/features/records/records_service.dart';
import 'package:motorcycle_dash/features/performance/models/performance_launch_models.dart';
import 'package:motorcycle_dash/features/performance/services/monotonic_clock.dart';
import 'package:motorcycle_dash/features/performance/services/performance_launch_state_machine.dart';
import 'package:motorcycle_dash/features/performance/performance_timer_service.dart';
import 'package:motorcycle_dash/services/telemetry_service.dart';
import 'package:motorcycle_dash/services/location_service.dart';
import 'package:motorcycle_dash/services/sensor_fusion_service.dart';
import 'package:motorcycle_dash/services/compass_service.dart';
import 'package:motorcycle_dash/services/audio_alert_service.dart';
import 'package:motorcycle_dash/services/field_test/field_test_logger.dart';
import 'package:motorcycle_dash/services/validation/telemetry_validator.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:motorcycle_dash/database/database.dart'
hide TelemetrySample, FieldTestSummary, FieldTestEvent;
import 'package:drift/native.dart';
class MockAudioAlertService extends Fake implements AudioAlertService {
@override
Future<void> speak(String text, {bool force = false}) async {}
}
class FakeTelemetryService extends TelemetryService {
final StreamController<TelemetrySample> _controller =
StreamController<TelemetrySample>.broadcast();
FakeTelemetryService(
super.loc,
super.fusion,
super.compass,
super.logger,
super.validator,
);
@override
Stream<TelemetrySample> get telemetryStream => _controller.stream;
void emitSample(TelemetrySample sample) => _controller.add(sample);
@override
void dispose() => _controller.close();
}
class _NullLocationService extends Fake implements LocationService {
@override
void Function(Position position, bool accepted, String? rejectionReason)?
onGpsDiagnostic;
@override
Stream<Position> getFilteredPositionStream() => const Stream.empty();
}
class _NullSensorFusionService extends Fake implements SensorFusionService {
@override
Stream<double> get leanAngleStream => const Stream.empty();
@override
Stream<double> get pitchAngleStream => const Stream.empty();
@override
Stream<GForceData> get gForceStream => const Stream.empty();
@override
double get latestRollRateDegPerSec => 0.0;
@override
double get latestRawAccelX => 0.0;
@override
double get latestRawAccelY => 0.0;
@override
double get latestRawAccelZ => 0.0;
@override
double get latestRawGyroX => 0.0;
@override
double get latestRawGyroY => 0.0;
@override
double get latestRawGyroZ => 0.0;
}
class _NullCompassService extends Fake implements CompassService {
@override
Stream<double> get headingStream => const Stream.empty();
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late AppDatabase db;
late FieldTestLogger logger;
late FakeTelemetryService fakeTelemetry;
late MockAudioAlertService mockAudio;
late TelemetryValidator validator;
late SharedPreferences prefs;
setUp(() async {
SharedPreferences.setMockInitialValues({});
prefs = await SharedPreferences.getInstance();
db = AppDatabase.forTesting(NativeDatabase.memory());
logger = FieldTestLogger(db);
validator = TelemetryValidator();
fakeTelemetry = FakeTelemetryService(
_NullLocationService(),
_NullSensorFusionService(),
_NullCompassService(),
logger,
validator,
);
mockAudio = MockAudioAlertService();
});
tearDown(() async {
fakeTelemetry.dispose();
logger.dispose();
await db.close();
});
ProviderContainer buildContainer() {
final container = ProviderContainer(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
databaseProvider.overrideWithValue(db),
telemetryServiceProvider.overrideWithValue(fakeTelemetry),
audioAlertServiceProvider.overrideWithValue(mockAudio),
fieldTestLoggerProvider.overrideWithValue(logger),
],
);
addTearDown(container.dispose);
return container;
}
final baseTime = DateTime(2026, 9, 19, 12, 0, 0);
TelemetrySample createSample({
required int monotonicMs,
DateTime? gpsTimestamp,
int? gpsAgeMs,
DateTime? imuTimestamp,
double speed = 35.0,
double lat = 34.0,
double lon = -85.0,
double? altitude,
double? leanAngle,
double? longitudinalG,
double? gpsAccuracy = 10.0,
}) {
return TelemetrySample(
timestamp: baseTime.add(Duration(milliseconds: monotonicMs)),
monotonicTimestampMs: monotonicMs,
latitude: lat,
longitude: lon,
altitude: altitude,
speed: speed,
gpsTimestamp: gpsTimestamp,
gpsAgeMs: gpsAgeMs,
imuTimestamp:
imuTimestamp ?? baseTime.add(Duration(milliseconds: monotonicMs)),
leanAngle: leanAngle ?? 0.0,
longitudinalG: longitudinalG ?? 0.0,
gpsAccuracy: gpsAccuracy,
quality: const TelemetryQuality(
status: ValidationStatus.valid,
overallConfidence: 1.0,
),
);
}
group('Phase 1F — GNSS-Derived Metrics Integrity During Stale GPS', () {
test('1. Moving vehicle enters GPS outage: moving time stops accumulating after stale boundary', () async {
final container = buildContainer();
final sm = container.read(rideStateMachineProvider.notifier);
await sm.startRide();
final gpsFixTime = baseTime;
// 100 samples at 20 Hz (5 seconds). First 4s are fresh, last 1s is stale (> 4000ms).
for (int i = 0; i <= 100; i++) {
final monoMs = i * 50;
final sampleTime = baseTime.add(Duration(milliseconds: monoMs));
final ageMs = sampleTime.difference(gpsFixTime).inMilliseconds;
fakeTelemetry.emitSample(createSample(
monotonicMs: monoMs,
gpsTimestamp: gpsFixTime,
gpsAgeMs: ageMs,
speed: 40.0,
));
}
await Future<void>.delayed(const Duration(milliseconds: 50));
final state = container.read(rideStateMachineProvider);
// Fresh duration was 4.0s (80 frames of 50ms). Stale frames after 4000ms should NOT accumulate.
expect(state.movingTimeSeconds, inInclusiveRange(3, 4));
// Moving time must NOT have reached 5 seconds
expect(state.movingTimeSeconds, lessThan(5));
});
test('2. Moving vehicle enters GPS outage: trip distance stops accumulating', () async {
final container = buildContainer();
final sm = container.read(rideStateMachineProvider.notifier);
await sm.startRide();
final gpsFixTime = baseTime;
// First sample
fakeTelemetry.emitSample(createSample(
monotonicMs: 0,
gpsTimestamp: gpsFixTime,
gpsAgeMs: 0,
speed: 40.0,
lat: 34.0000,
lon: -85.0000,
));
// Advance 1 second with advancing coordinates while fresh
for (int i = 1; i <= 20; i++) {
fakeTelemetry.emitSample(createSample(
monotonicMs: i * 50,
gpsTimestamp: gpsFixTime,
gpsAgeMs: i * 50,
speed: 40.0,
lat: 34.0000 + (i * 0.0001),
lon: -85.0000,
));
}
await Future<void>.delayed(const Duration(milliseconds: 30));
final distanceFresh = container.read(rideStateMachineProvider).tripDistanceMiles;
expect(distanceFresh, greaterThan(0.0));
// Now GPS fix stops advancing. We enter stale period (> 4000ms).
// Even if coordinates somehow changed in a stale sample, distance must NOT accumulate.
for (int i = 81; i <= 120; i++) {
fakeTelemetry.emitSample(createSample(
monotonicMs: i * 50,
gpsTimestamp: gpsFixTime,
gpsAgeMs: i * 50, // 4050ms to 6000ms -> STALE
speed: 40.0,
lat: 34.0000 + (i * 0.0001),
lon: -85.0000,
));
}
await Future<void>.delayed(const Duration(milliseconds: 30));
final distanceAfterStale = container.read(rideStateMachineProvider).tripDistanceMiles;
expect(distanceAfterStale, equals(distanceFresh));
});
test('3. Moving vehicle enters GPS outage: top speed does not update on stale samples', () async {
final container = buildContainer();
final sm = container.read(rideStateMachineProvider.notifier);
await sm.startRide();
// Fresh sample at 50 MPH
fakeTelemetry.emitSample(createSample(
monotonicMs: 1000,
gpsTimestamp: baseTime.add(const Duration(milliseconds: 1000)),
gpsAgeMs: 0,
speed: 50.0,
));
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(container.read(rideStateMachineProvider).topSpeedMph, 50.0);
// Stale sample with higher speed (85 MPH) but stale GPS (> 4000ms)
fakeTelemetry.emitSample(createSample(
monotonicMs: 6000,
gpsTimestamp: baseTime.add(const Duration(milliseconds: 1000)),
gpsAgeMs: 5000, // 5000ms > 4000ms -> STALE
speed: 85.0,
));
await Future<void>.delayed(const Duration(milliseconds: 20));
// Top speed must remain 50.0 MPH, NOT 85.0 MPH
expect(container.read(rideStateMachineProvider).topSpeedMph, 50.0);
});
test('4. Boundary condition at 3999ms: sample is fresh, metrics accumulate', () async {
final container = buildContainer();
final sm = container.read(rideStateMachineProvider.notifier);
await sm.startRide();
final gpsFixTime = baseTime;
// Sample at 3950ms
fakeTelemetry.emitSample(createSample(
monotonicMs: 3950,
gpsTimestamp: gpsFixTime,
gpsAgeMs: 3950,
speed: 45.0,
));
// Sample at 3999ms (age 3999ms <= 4000ms -> FRESH)
fakeTelemetry.emitSample(createSample(
monotonicMs: 3999,
gpsTimestamp: gpsFixTime,
gpsAgeMs: 3999,
speed: 65.0,
));
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(container.read(rideStateMachineProvider).topSpeedMph, 65.0);
});
test('5. Boundary condition at 4000ms: sample is fresh, metrics accumulate', () async {
final container = buildContainer();
final sm = container.read(rideStateMachineProvider.notifier);
await sm.startRide();
final gpsFixTime = baseTime;
// Sample at 4000ms (age exactly 4000ms -> FRESH)
fakeTelemetry.emitSample(createSample(
monotonicMs: 4000,
gpsTimestamp: gpsFixTime,
gpsAgeMs: 4000,
speed: 70.0,
));
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(container.read(rideStateMachineProvider).topSpeedMph, 70.0);
});
test('6. Boundary condition at 4001ms: sample is stale, metrics do NOT accumulate', () async {
final container = buildContainer();
final sm = container.read(rideStateMachineProvider.notifier);
await sm.startRide();
final gpsFixTime = baseTime;
// Baseline sample at 4000ms
fakeTelemetry.emitSample(createSample(
monotonicMs: 4000,
gpsTimestamp: gpsFixTime,
gpsAgeMs: 4000,
speed: 55.0,
));
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(container.read(rideStateMachineProvider).topSpeedMph, 55.0);
// Boundary sample at 4001ms (age 4001ms > 4000ms -> STALE)
fakeTelemetry.emitSample(createSample(
monotonicMs: 4001,
gpsTimestamp: gpsFixTime,
gpsAgeMs: 4001,
speed: 95.0,
));
await Future<void>.delayed(const Duration(milliseconds: 20));
// Stale speed must NOT update top speed
expect(container.read(rideStateMachineProvider).topSpeedMph, 55.0);
});
test('7. GNSS recovery after outage: distance does NOT jump across outage', () async {
final container = buildContainer();
final sm = container.read(rideStateMachineProvider.notifier);
await sm.startRide();
// Fix 1 at t=0
fakeTelemetry.emitSample(createSample(
monotonicMs: 0,
gpsTimestamp: baseTime,
gpsAgeMs: 0,
speed: 30.0,
lat: 34.0000,
lon: -85.0000,
));
// Fix 1 frame at t=50ms
fakeTelemetry.emitSample(createSample(
monotonicMs: 50,
gpsTimestamp: baseTime,
gpsAgeMs: 50,
speed: 30.0,
lat: 34.0001,
lon: -85.0000,
));
await Future<void>.delayed(const Duration(milliseconds: 30));
final distanceBeforeOutage = container.read(rideStateMachineProvider).tripDistanceMiles;
expect(distanceBeforeOutage, greaterThan(0.0));
// Outage: 10 seconds of stale samples at old coordinate
for (int i = 81; i <= 200; i++) {
fakeTelemetry.emitSample(createSample(
monotonicMs: i * 50,
gpsTimestamp: baseTime,
gpsAgeMs: i * 50, // 4050ms to 10000ms -> STALE
speed: 30.0,
lat: 34.0001,
lon: -85.0000,
));
}
await Future<void>.delayed(const Duration(milliseconds: 30));
final distanceDuringOutage = container.read(rideStateMachineProvider).tripDistanceMiles;
expect(distanceDuringOutage, equals(distanceBeforeOutage));
// RECOVERY: Frame N (first fresh fix 100m away)
final recoveryFixTime = baseTime.add(const Duration(milliseconds: 10050));
fakeTelemetry.emitSample(createSample(
monotonicMs: 10050,
gpsTimestamp: recoveryFixTime,
gpsAgeMs: 0,
speed: 30.0,
lat: 34.0010, // ~100m jump
lon: -85.0000,
));
await Future<void>.delayed(const Duration(milliseconds: 30));
final distanceAtRecoveryFrame = container.read(rideStateMachineProvider).tripDistanceMiles;
// Recovery frame MUST NOT integrate the 100m gap across the outage!
expect(distanceAtRecoveryFrame, equals(distanceBeforeOutage));
// Frame N+1: Next fresh sample 50ms later (real motion from recovery coordinate)
fakeTelemetry.emitSample(createSample(
monotonicMs: 10100,
gpsTimestamp: recoveryFixTime.add(const Duration(milliseconds: 50)),
gpsAgeMs: 0,
speed: 30.0,
lat: 34.00101, // ~1m move
lon: -85.0000,
));
await Future<void>.delayed(const Duration(milliseconds: 30));
final distanceAfterRecovery = container.read(rideStateMachineProvider).tripDistanceMiles;
// Normal accumulation resumes without the artificial 100m catch-up jump!
expect(distanceAfterRecovery, greaterThan(distanceBeforeOutage));
// Change should be tiny (~1 meter), NOT ~100 meters
final deltaMiles = distanceAfterRecovery - distanceBeforeOutage;
expect(deltaMiles, lessThan(0.01)); // 0.01 miles is 16 meters
});
test('8. Dynamic motorcycle metrics: cannot update max lean / G during GPS outage', () async {
final container = buildContainer();
final sm = container.read(rideStateMachineProvider.notifier);
await sm.startRide();
// Fresh sample while riding normally
fakeTelemetry.emitSample(createSample(
monotonicMs: 1000,
gpsTimestamp: baseTime.add(const Duration(milliseconds: 1000)),
gpsAgeMs: 0,
speed: 30.0,
leanAngle: 15.0,
longitudinalG: 0.2,
));
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(container.read(rideStateMachineProvider).maxLeanRight, 15.0);
expect(container.read(rideStateMachineProvider).maxAccelG, 0.2);
// Bike enters garage / tunnel: GPS fix stops.
// Stale sample (> 4000ms) with cached speed (30.0 MPH) and extreme kickstand lean (45 deg)
fakeTelemetry.emitSample(createSample(
monotonicMs: 6000,
gpsTimestamp: baseTime.add(const Duration(milliseconds: 1000)),
gpsAgeMs: 5000,
speed: 30.0, // cached raw speed
leanAngle: 45.0, // kickstand lean
longitudinalG: 1.5, // dropped phone / shake
));
await Future<void>.delayed(const Duration(milliseconds: 20));
// Stale speed must NOT satisfy dynamic moving gate; records must remain unchanged
expect(container.read(rideStateMachineProvider).maxLeanRight, 15.0);
expect(container.read(rideStateMachineProvider).maxAccelG, 0.2);
});
test('9. High Speed event: not triggered repeatedly during GPS outage', () async {
final container = buildContainer();
final sm = container.read(rideStateMachineProvider.notifier);
await sm.startRide();
// Fresh sample at 105 MPH -> triggers High Speed event
fakeTelemetry.emitSample(createSample(
monotonicMs: 1000,
gpsTimestamp: baseTime.add(const Duration(milliseconds: 1000)),
gpsAgeMs: 0,
speed: 105.0,
));
await Future<void>.delayed(const Duration(milliseconds: 50));
final eventsInitial = await db.select(db.rideEvents).get();
expect(eventsInitial, hasLength(1));
expect(eventsInitial.first.eventType, 'High Speed');
// 10 seconds later, GPS is stale (> 4000ms), cached speed still 105 MPH
fakeTelemetry.emitSample(createSample(
monotonicMs: 11000,
gpsTimestamp: baseTime.add(const Duration(milliseconds: 1000)),
gpsAgeMs: 10000,
speed: 105.0,
));
await Future<void>.delayed(const Duration(milliseconds: 50));
final eventsAfterStale = await db.select(db.rideEvents).get();
// Must NOT add another High Speed event from stale speed!
expect(eventsAfterStale, hasLength(1));
});
});
group('Phase 1F — PerformanceLaunchStateMachine Stale GPS Gating', () {
late StreamController<TelemetrySample> perfController;
late FakeMonotonicClock clock;
PerformanceLaunchStateMachine? perfSm;
setUp(() {
perfController = StreamController<TelemetrySample>.broadcast();
clock = FakeMonotonicClock(
initialElapsedMs: 10000,
initialDateTime: baseTime,
);
});
tearDown(() async {
perfSm?.dispose();
await perfController.close();
});
test('10. PerformanceLaunchStateMachine in launched: aborts with gpsLost when GPS becomes stale', () async {
perfSm = PerformanceLaunchStateMachine(
db: db,
telemetryStream: perfController.stream,
clock: clock,
);
perfSm!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty);
// Stationary fresh sample -> READY
perfController.add(TelemetrySample(
timestamp: baseTime,
monotonicTimestampMs: 1000,
speed: 0.0,
gpsTimestamp: baseTime,
gpsAgeMs: 0,
));
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(perfSm!.state.state, PerformanceLaunchState.ready);
// Launch with fresh sample -> LAUNCHED
perfController.add(TelemetrySample(
timestamp: baseTime.add(const Duration(milliseconds: 100)),
monotonicTimestampMs: 1100,
speed: 5.0,
gpsTimestamp: baseTime.add(const Duration(milliseconds: 100)),
gpsAgeMs: 0,
));
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(perfSm!.state.state, PerformanceLaunchState.launched);
// Accelerating fresh sample
perfController.add(TelemetrySample(
timestamp: baseTime.add(const Duration(milliseconds: 1000)),
monotonicTimestampMs: 2000,
speed: 40.0,
gpsTimestamp: baseTime.add(const Duration(milliseconds: 1000)),
gpsAgeMs: 0,
));
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(perfSm!.state.state, PerformanceLaunchState.launched);
// GPS stops advancing. Sample arrives with stale GPS (> 4000ms)
perfController.add(TelemetrySample(
timestamp: baseTime.add(const Duration(milliseconds: 6000)),
monotonicTimestampMs: 7000,
speed: 40.0,
gpsTimestamp: baseTime.add(const Duration(milliseconds: 1000)),
gpsAgeMs: 5000, // STALE
));
await Future<void>.delayed(const Duration(milliseconds: 20));
// MUST abort with gpsLost!
expect(perfSm!.state.state, PerformanceLaunchState.aborted);
expect(perfSm!.state.abortReason, PerformanceAbortReason.gpsLost);
});
test('11. PerformanceLaunchStateMachine in ready: does not launch from stale speed', () async {
perfSm = PerformanceLaunchStateMachine(
db: db,
telemetryStream: perfController.stream,
clock: clock,
);
perfSm!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty);
// Stationary fresh sample -> READY
perfController.add(TelemetrySample(
timestamp: baseTime,
monotonicTimestampMs: 1000,
speed: 0.0,
gpsTimestamp: baseTime,
gpsAgeMs: 0,
));
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(perfSm!.state.state, PerformanceLaunchState.ready);
// Stale sample with speed 15 MPH (> 1.0 MPH movement threshold) arrives
perfController.add(TelemetrySample(
timestamp: baseTime.add(const Duration(milliseconds: 5000)),
monotonicTimestampMs: 6000,
speed: 15.0,
gpsTimestamp: baseTime,
gpsAgeMs: 5000, // STALE
));
await Future<void>.delayed(const Duration(milliseconds: 20));
// Must NOT launch! Should revert to idle on stale GPS
expect(perfSm!.state.state, isNot(PerformanceLaunchState.launched));
expect(perfSm!.state.state, PerformanceLaunchState.idle);
});
test('12. PerformanceLaunchStateMachine in countdown: aborts with gpsLost when GPS becomes stale', () async {
perfSm = PerformanceLaunchStateMachine(
db: db,
telemetryStream: perfController.stream,
clock: clock,
);
perfSm!.arm(mode: PerformanceLaunchMode.countdown, target: PerformanceLaunchTarget.sixty);
// Stationary fresh sample -> READY
perfController.add(TelemetrySample(
timestamp: baseTime,
monotonicTimestampMs: 1000,
speed: 0.0,
gpsTimestamp: baseTime,
gpsAgeMs: 0,
));
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(perfSm!.state.state, PerformanceLaunchState.ready);
// Start countdown
perfSm!.startCountdown();
expect(perfSm!.state.state, PerformanceLaunchState.countdown);
// Stale sample arrives during countdown
perfController.add(TelemetrySample(
timestamp: baseTime.add(const Duration(milliseconds: 5000)),
monotonicTimestampMs: 6000,
speed: 0.0,
gpsTimestamp: baseTime,
gpsAgeMs: 5000, // STALE
));
await Future<void>.delayed(const Duration(milliseconds: 20));
// Must abort with gpsLost!
expect(perfSm!.state.state, PerformanceLaunchState.aborted);
expect(perfSm!.state.abortReason, PerformanceAbortReason.gpsLost);
});
});
group('Phase 1F — PerformanceTimerService Stale GPS Gating', () {
late StreamController<TelemetrySample> timerController;
PerformanceTimerService? timerSm;
setUp(() {
timerController = StreamController<TelemetrySample>.broadcast();
});
tearDown(() async {
timerSm?.dispose();
await timerController.close();
});
test('13. PerformanceTimerService in timing: aborts when GPS becomes stale', () async {
timerSm = PerformanceTimerService(
db,
timerController.stream,
);
timerSm!.setTarget(PerformanceTarget.sixty);
// Stationary fresh sample -> READY
timerController.add(TelemetrySample(
timestamp: baseTime,
speed: 0.0,
gpsTimestamp: baseTime,
gpsAgeMs: 0,
));
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(timerSm!.state.state, PerformanceState.ready);
// Launch with fresh sample -> TIMING
timerController.add(TelemetrySample(
timestamp: baseTime.add(const Duration(milliseconds: 100)),
speed: 5.0,
gpsTimestamp: baseTime.add(const Duration(milliseconds: 100)),
gpsAgeMs: 0,
));
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(timerSm!.state.state, PerformanceState.timing);
// Stale sample arrives (> 4000ms)
timerController.add(TelemetrySample(
timestamp: baseTime.add(const Duration(milliseconds: 6000)),
speed: 40.0,
gpsTimestamp: baseTime.add(const Duration(milliseconds: 100)),
gpsAgeMs: 5900, // STALE
));
await Future<void>.delayed(const Duration(milliseconds: 20));
// Must abort!
expect(timerSm!.state.state, PerformanceState.aborted);
// And no run should be saved in the database
final runs = await db.select(db.performanceRuns).get();
expect(runs, isEmpty);
});
});
}