MotorSignal-App/test/phase11_5_torture_soak_test...

471 lines
19 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:drift/drift.dart' as drift;
import 'package:drift/native.dart';
import 'package:motorcycle_dash/database/database.dart' hide FieldTestEvent, FieldTestSummary, TelemetrySample;
import 'package:motorcycle_dash/models/field_test_event.dart';
import 'package:motorcycle_dash/models/telemetry_sample.dart';
import 'package:motorcycle_dash/services/field_test/field_test_logger.dart';
import 'package:motorcycle_dash/services/field_test/field_test_export_service.dart';
import 'package:motorcycle_dash/services/field_test/field_test_replay_service.dart';
import 'package:motorcycle_dash/services/validation/telemetry_validator.dart';
import 'package:motorcycle_dash/features/performance/performance_timer_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late AppDatabase db;
late FieldTestLogger logger;
late TelemetryValidator validator;
late FieldTestExportService exportService;
late FieldTestReplayService replayService;
setUp(() {
db = AppDatabase.forTesting(NativeDatabase.memory());
logger = FieldTestLogger(db);
validator = TelemetryValidator();
exportService = FieldTestExportService(db);
replayService = FieldTestReplayService(validator: validator);
});
tearDown(() async {
logger.dispose();
await db.close();
});
group('Phase 11.5: Step 3 & 4 — Start/Stop Torment Test (100 Cycles)', () {
test('Executes 100 consecutive start/record/stop cycles without state corruption or leaks', () async {
for (int cycle = 1; cycle <= 100; cycle++) {
final start = DateTime(2026, 8, 28, 8, 0, 0).add(Duration(minutes: cycle * 5));
final rideId = await db.into(db.rides).insert(RidesCompanion.insert(
startTime: start,
));
logger.startSession(rideId);
expect(logger.isSessionActive, isTrue);
expect(logger.activeRideId, rideId);
// Generate 5 valid advancing telemetry samples per cycle
for (int i = 0; i < 5; i++) {
final s = TelemetrySample(
timestamp: start.add(Duration(seconds: i)),
speed: 25.0 + (i * 0.5),
latitude: 37.7749 + (cycle * 0.001) + (i * 0.0001),
longitude: -122.4194,
gpsAccuracy: 3.0,
leanAngle: 5.0,
longitudinalG: 0.05,
lateralG: 0.05,
);
logger.processTelemetrySample(s, validator.validate(s));
await db.into(db.telemetrySamples).insert(TelemetrySamplesCompanion.insert(
rideId: rideId,
timestamp: s.timestamp,
speed: drift.Value(s.speed),
latitude: drift.Value(s.latitude),
longitude: drift.Value(s.longitude),
gpsAccuracy: drift.Value(s.gpsAccuracy),
leanAngle: drift.Value(s.leanAngle),
longitudinalG: drift.Value(s.longitudinalG),
lateralG: drift.Value(s.lateralG),
));
}
await (db.update(db.rides)..where((r) => r.id.equals(rideId))).write(
RidesCompanion(
endTime: drift.Value(start.add(const Duration(seconds: 5))),
distanceMiles: const drift.Value(0.1),
topSpeedMph: const drift.Value(27.0),
),
);
final summary = await logger.stopSession();
expect(logger.isSessionActive, isFalse);
expect(summary.rideId, rideId);
expect(summary.totalSamples, 5);
expect(summary.validSamples, 5);
}
// Verify total database records after 100 cycles
final allRides = await db.select(db.rides).get();
final allSummaries = await db.select(db.fieldTestSummaries).get();
expect(allRides.length, 100);
expect(allSummaries.length, 100);
});
});
group('Phase 11.5: Step 6 — High-Frequency Telemetry Stress (20Hz, 30Hz, 50Hz, 100Hz)', () {
test('Processes 20Hz, 30Hz, 50Hz, and 100Hz pathological sample streams stably', () async {
logger.startSession(201);
final base = DateTime(2026, 8, 28, 12, 0, 0);
// Frequencies to test: 20Hz (50ms), 30Hz (33ms), 50Hz (20ms), 100Hz (10ms)
final frequencies = [
{'name': '20Hz normal', 'intervalMs': 50, 'count': 100},
{'name': '30Hz stress', 'intervalMs': 33, 'count': 100},
{'name': '50Hz stress', 'intervalMs': 20, 'count': 100},
{'name': '100Hz pathological', 'intervalMs': 10, 'count': 100},
];
int totalEmitted = 0;
for (final freq in frequencies) {
final intervalMs = freq['intervalMs'] as int;
final count = freq['count'] as int;
for (int i = 0; i < count; i++) {
final s = TelemetrySample(
timestamp: base.add(Duration(milliseconds: totalEmitted * intervalMs)),
speed: 55.0,
latitude: 37.7749 + (totalEmitted * 0.00001),
longitude: -122.4194,
gpsAccuracy: 2.5,
leanAngle: 10.0,
longitudinalG: 0.05,
lateralG: 0.1,
);
logger.processTelemetrySample(s, validator.validate(s));
totalEmitted++;
}
}
await logger.flush();
expect(totalEmitted, 400);
expect(logger.droppedEventsCount, 0);
expect(logger.failedFlushCount, 0);
});
});
group('Phase 11.5: Step 7 — Queue Saturation & Priority Eviction Test', () {
test('Prioritized drop policy preserves CRITICAL and ERROR events over INFO events', () async {
logger.startSession(301);
// 1. Fill 1,000 capacity with INFO events
for (int i = 0; i < 999; i++) {
logger.log(
eventType: FieldTestEventType.sampleAccepted,
severity: EventSeverity.info,
message: 'Telemetry sample #$i accepted',
);
}
expect(logger.currentQueueDepth, 1000);
// 2. Add WARNING event -> should evict 1 INFO event
logger.log(
eventType: FieldTestEventType.gpsStale,
severity: EventSeverity.warning,
message: 'GPS fix stale warning',
);
expect(logger.currentQueueDepth, 1000);
expect(logger.droppedEventsCount, 1);
// 3. Add ERROR event -> should evict another INFO event
logger.log(
eventType: FieldTestEventType.imuSaturation,
severity: EventSeverity.error,
message: 'IMU saturation error',
);
expect(logger.currentQueueDepth, 1000);
expect(logger.droppedEventsCount, 2);
// 4. Add CRITICAL event -> should evict another INFO event
logger.log(
eventType: FieldTestEventType.databaseRecovery,
severity: EventSeverity.critical,
message: 'Critical DB crash recovery',
);
expect(logger.currentQueueDepth, 1000);
expect(logger.droppedEventsCount, 3);
await logger.flush();
expect(logger.totalFlushed, 1000);
final dbEvents = await db.select(db.fieldTestEvents).get();
expect(dbEvents.any((e) => e.severity == 'CRITICAL'), isTrue);
expect(dbEvents.any((e) => e.severity == 'ERROR'), isTrue);
expect(dbEvents.any((e) => e.severity == 'WARNING'), isTrue);
});
});
group('Phase 11.5: Step 10 & 11 — Export Round-Trip & Destroy-Replay Test', () {
test('Exports complete field test session, verifies all 6 files, and replays offline deterministically', () async {
final start = DateTime(2026, 8, 28, 14, 0, 0);
final rideId = await db.into(db.rides).insert(RidesCompanion.insert(
startTime: start,
endTime: drift.Value(start.add(const Duration(minutes: 45))),
distanceMiles: const drift.Value(24.8),
topSpeedMph: const drift.Value(85.5),
maxLeanLeft: const drift.Value(38.2),
maxLeanRight: const drift.Value(41.0),
averageSpeed: const drift.Value(48.5),
));
logger.startSession(rideId);
final exportValidator = TelemetryValidator();
final sampleList = <TelemetrySample>[];
for (int i = 0; i < 100; i++) {
final s = TelemetrySample(
timestamp: start.add(Duration(seconds: i)),
latitude: 37.7749 + (i * 0.0002),
longitude: -122.4194 + (i * 0.0002),
speed: 45.0 + (i * 0.05),
gpsAccuracy: 2.8,
leanAngle: 12.0,
longitudinalG: 0.1,
lateralG: 0.2,
);
sampleList.add(s);
logger.processTelemetrySample(s, exportValidator.validate(s));
await db.into(db.telemetrySamples).insert(TelemetrySamplesCompanion.insert(
rideId: rideId,
timestamp: s.timestamp,
latitude: drift.Value(s.latitude),
longitude: drift.Value(s.longitude),
speed: drift.Value(s.speed),
gpsAccuracy: drift.Value(s.gpsAccuracy),
leanAngle: drift.Value(s.leanAngle),
longitudinalG: drift.Value(s.longitudinalG),
lateralG: drift.Value(s.lateralG),
));
}
await logger.flush();
final originalSummary = await logger.generateAndSaveSummary(rideId);
// Verify JSON session payload
final jsonContent = await exportService.generateFieldTestSessionJson(rideId);
final Map<String, dynamic> parsedJson = jsonDecode(jsonContent);
expect(parsedJson['exportVersion'], '1.0.0');
expect(parsedJson['rideMetadata']['id'], rideId);
expect(parsedJson['summary']['totalSamples'], 100);
// Verify CSV payload
final csvContent = await exportService.generateFieldTestEventsCsv(rideId);
expect(csvContent.contains('timestamp,ride_id,event_type'), isTrue);
// Replay offline from samples and compare metrics
final replayResult = await replayService.replaySamples(sampleList);
expect(replayResult.totalSamples, originalSummary.totalSamples);
expect(replayResult.validSamples, originalSummary.validSamples);
expect(replayResult.validPercentage, originalSummary.validPercentage);
expect(replayResult.rideQualityScore, originalSummary.rideQualityScore);
expect(replayResult.rideQualityRating, originalSummary.rideQualityRating);
});
});
group('Phase 11.5: Step 12 — Timestamp Torture Matrix', () {
test('Correctly identifies duplicate timestamps, reverse timestamps, and extreme time gaps', () {
final base = DateTime(2026, 8, 28, 15, 0, 0);
validator.reset();
// 1. Nominal sample
final s1 = TelemetrySample(timestamp: base, speed: 45.0, gpsAccuracy: 3.0);
final q1 = validator.validate(s1);
expect(q1.isValid, isTrue);
// 2. Duplicate timestamp (T, T)
final s2 = TelemetrySample(timestamp: base, speed: 45.0, gpsAccuracy: 3.0);
final q2 = validator.validate(s2);
expect(q2.isSuspect, isTrue);
expect(q2.reasons.any((r) => r.contains('Duplicate or reverse timestamp')), isTrue);
// 3. Reverse timestamp (T, T - 50ms)
final s3 = TelemetrySample(timestamp: base.subtract(const Duration(milliseconds: 50)), speed: 45.0, gpsAccuracy: 3.0);
final q3 = validator.validate(s3);
expect(q3.isSuspect, isTrue);
expect(q3.reasons.any((r) => r.contains('Duplicate or reverse timestamp')), isTrue);
// 4. Large gap (T + 5s)
final s4 = TelemetrySample(timestamp: base.add(const Duration(seconds: 5)), speed: 45.0, gpsAccuracy: 3.0);
final q4 = validator.validate(s4);
expect(q4.isSuspect, isTrue);
expect(q4.reasons.any((r) => r.contains('gap')), isTrue);
// 5. Extreme gap (T + 60s)
final s5 = TelemetrySample(timestamp: base.add(const Duration(seconds: 65)), speed: 45.0, gpsAccuracy: 3.0);
final q5 = validator.validate(s5);
expect(q5.isSuspect, isTrue);
});
});
group('Phase 11.5: Step 13 & 14 — GPS & IMU Failure Matrix with De-duplication', () {
test('GPS Failure Matrix: Fresh, Stale, Teleport, and Recovery behaviors', () {
final base = DateTime(2026, 8, 28, 16, 0, 0);
// Fresh @ 0 MPH
validator.reset();
final stationary = TelemetrySample(timestamp: base, speed: 0.0, gpsAccuracy: 3.0, latitude: 37.7749, longitude: -122.4194);
final qStat = validator.validate(stationary);
expect(qStat.isStationary, isTrue);
expect(qStat.isValid, isTrue);
// Fresh @ 60 MPH
validator.reset();
final fresh = TelemetrySample(timestamp: base, speed: 60.0, gpsAccuracy: 3.0, latitude: 37.7750, longitude: -122.4194);
final qFresh = validator.validate(fresh);
expect(qFresh.isValid, isTrue);
// Stale @ 60 MPH (>2000ms age)
final stale = TelemetrySample(timestamp: base.add(const Duration(milliseconds: 50)), speed: 60.0, gpsAccuracy: 3.0, gpsAgeMs: 2500, latitude: 37.7751, longitude: -122.4194);
final qStale = validator.validate(stale);
expect(qStale.isSuspect, isTrue);
expect(qStale.reasons.any((r) => r.contains('Stale')), isTrue);
// GPS Teleport Jump
final teleport = TelemetrySample(timestamp: base.add(const Duration(milliseconds: 100)), speed: 60.0, gpsAccuracy: 3.0, latitude: 38.8888, longitude: -122.4194);
final qTeleport = validator.validate(teleport);
expect(qTeleport.isRejected, isTrue);
expect(qTeleport.reasons.any((r) => r.contains('jump')), isTrue);
});
test('IMU Failure Matrix: Saturation and Extreme Roll-Rate', () {
final base = DateTime(2026, 8, 28, 17, 0, 0);
validator.reset();
// Normal IMU
final normal = TelemetrySample(timestamp: base, speed: 45.0, leanAngle: 25.0, accelX: 2.0, accelY: 1.0, accelZ: 9.8, longitudinalG: 0.2, lateralG: 0.4);
expect(validator.validate(normal).isValid, isTrue);
// Extreme IMU Saturation (>120 m/s² ≈ 12.2g)
final saturated = TelemetrySample(timestamp: base.add(const Duration(milliseconds: 50)), speed: 45.0, leanAngle: 25.0, accelX: 135.0, accelY: 9.8, accelZ: 0.0, longitudinalG: 13.5, lateralG: 0.4);
final qSat = validator.validate(saturated);
expect(qSat.isSensorSaturated, isTrue);
expect(qSat.isSuspect, isTrue);
// Extreme Roll Rate (>180 deg/sec)
final flick1 = TelemetrySample(timestamp: base.add(const Duration(milliseconds: 100)), speed: 45.0, leanAngle: 10.0);
validator.validate(flick1);
final flick2 = TelemetrySample(timestamp: base.add(const Duration(milliseconds: 150)), speed: 45.0, leanAngle: 50.0); // 40 deg in 50ms = 800 deg/s!
final qFlick = validator.validate(flick2);
expect(qFlick.isSuspect, isTrue);
expect(qFlick.reasons.any((r) => r.contains('roll rate')), isTrue);
});
});
group('Phase 11.5: Step 16 & 17 — Personal Record Torture & Interruption Gating', () {
test('Rejects impossible 0-60 sprint times (< 1.8s) and impossible speeds (> 220 MPH)', () {
final base = DateTime(2026, 8, 28, 18, 0, 0);
// Speeds: 100 MPH (valid), 219 MPH (valid), 220.5 MPH (rejected)
validator.reset();
expect(validator.validate(TelemetrySample(timestamp: base, speed: 100.0)).isValid, isTrue);
validator.reset();
expect(validator.validate(TelemetrySample(timestamp: base, speed: 219.0)).isValid, isTrue);
validator.reset();
final impossibleSpeed = TelemetrySample(timestamp: base, speed: 221.0);
final qSpeed = validator.validate(impossibleSpeed);
expect(qSpeed.isRejected, isTrue);
expect(qSpeed.reasons.any((r) => r.contains('Impossible vehicle speed')), isTrue);
// Performance Timer Interruption test
final streamController = StreamController<TelemetrySample>.broadcast();
final timerService = PerformanceTimerService(db, streamController.stream);
timerService.setTarget(PerformanceTarget.sixty);
expect(timerService.state.state, PerformanceState.ready);
// Inject stopped sample then launch
streamController.add(TelemetrySample(timestamp: base, speed: 0.0));
streamController.add(TelemetrySample(timestamp: base.add(const Duration(milliseconds: 50)), speed: 15.0));
// Reset / change target
timerService.setTarget(PerformanceTarget.hundred);
expect(timerService.state.state, PerformanceState.ready);
expect(timerService.state.lastResult, isNull);
streamController.close();
});
});
group('Phase 11.5: Step 5 & 25 — Realistic Long-Duration Multi-Phase Soak Simulation (2 Hours)', () {
test('Simulates 2-hour continuous multi-phase ride with all fault injections and verifies summary consistency', () async {
final start = DateTime(2026, 8, 28, 6, 0, 0);
final rideId = await db.into(db.rides).insert(RidesCompanion.insert(
startTime: start,
endTime: drift.Value(start.add(const Duration(hours: 2))),
distanceMiles: const drift.Value(72.4),
topSpeedMph: const drift.Value(78.5),
));
logger.startSession(rideId);
final soakValidator = TelemetryValidator();
// Multi-phase 2-hour simulation (120 checkpoints spaced by 1-second ticks)
for (int step = 0; step < 120; step++) {
final time = start.add(Duration(seconds: step));
double speed = 55.0;
double lean = 0.0;
double longG = 0.05;
double? gpsAcc = 3.0;
int? gpsAge = 50;
if (step < 10) {
// Urban stop/start
speed = (step % 2 == 0) ? 0.0 : 30.0;
} else if (step < 40) {
// Highway cruise
speed = 68.0;
} else if (step < 60) {
// Mixed
speed = 45.0;
} else if (step < 80) {
// Cornering
speed = 42.0;
lean = (step % 2 == 0) ? -28.0 : 31.5;
} else if (step < 100) {
// Highway
speed = 72.0;
} else if (step < 110) {
// Fault: GPS degradation
gpsAcc = 42.0;
gpsAge = 2500;
} else if (step < 115) {
// Recovery
gpsAcc = 3.5;
gpsAge = 50;
}
final sample = TelemetrySample(
timestamp: time,
latitude: 37.7749 + (step * 0.0001),
longitude: -122.4194 + (step * 0.0001),
speed: speed,
gpsAccuracy: gpsAcc,
gpsAgeMs: gpsAge,
leanAngle: lean,
longitudinalG: longG,
lateralG: 0.1,
);
final quality = soakValidator.validate(sample);
logger.processTelemetrySample(sample, quality);
await db.into(db.telemetrySamples).insert(TelemetrySamplesCompanion.insert(
rideId: rideId,
timestamp: sample.timestamp,
latitude: drift.Value(sample.latitude),
longitude: drift.Value(sample.longitude),
speed: drift.Value(sample.speed),
gpsAccuracy: drift.Value(sample.gpsAccuracy),
leanAngle: drift.Value(sample.leanAngle),
longitudinalG: drift.Value(sample.longitudinalG),
lateralG: drift.Value(sample.lateralG),
));
}
await logger.flush();
final summary = await logger.generateAndSaveSummary(rideId);
expect(summary.totalSamples, 120);
expect(summary.validSamples, greaterThan(100));
expect(summary.rideQualityScore, greaterThanOrEqualTo(80));
expect(summary.exportIntegrity, 'VERIFIED_OK');
expect(summary.gpsStaleEventCount, greaterThan(0));
});
});
}