MotorSignal-App/test/telemetry_validation_test.dart

183 lines
6.1 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:motorcycle_dash/models/telemetry_sample.dart';
import 'package:motorcycle_dash/services/validation/telemetry_validator.dart';
import 'package:motorcycle_dash/services/calibration_service.dart';
import 'package:motorcycle_dash/features/records/records_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Phase 8: Telemetry Validator Tests', () {
late TelemetryValidator validator;
setUp(() {
validator = TelemetryValidator();
});
test('Valid cruising sample passes with high confidence', () {
final now = DateTime.now();
final sample = TelemetrySample(
timestamp: now,
latitude: 37.7749,
longitude: -122.4194,
speed: 65.0,
gpsAccuracy: 3.5,
leanAngle: 12.0,
pitchAngle: 0.5,
accelX: 0.2,
accelY: 0.1,
accelZ: 9.8,
longitudinalG: 0.02,
lateralG: 0.21,
);
final quality = validator.validate(sample);
expect(quality.isValid, isTrue);
expect(quality.status, equals(ValidationStatus.valid));
expect(quality.reasons, isEmpty);
expect(quality.gpsConfidence, equals(1.0));
expect(quality.overallConfidence, greaterThanOrEqualTo(0.90));
expect(quality.isStationary, isFalse);
});
test('Stationary vehicle correctly marked with stationary flag', () {
final sample = TelemetrySample(
timestamp: DateTime.now(),
speed: 0.0,
gpsAccuracy: 4.0,
leanAngle: 0.0,
longitudinalG: 0.0,
lateralG: 0.0,
);
final quality = validator.validate(sample);
expect(quality.isValid, isTrue);
expect(quality.isStationary, isTrue);
expect(quality.isAccelerating, isFalse);
expect(quality.isBraking, isFalse);
});
test('Impossible speed jump > 220 MPH is rejected with reason', () {
final sample = TelemetrySample(
timestamp: DateTime.now(),
speed: 345.0,
gpsAccuracy: 10.0,
);
final quality = validator.validate(sample);
expect(quality.isRejected, isTrue);
expect(quality.status, equals(ValidationStatus.rejected));
expect(quality.reasons.any((r) => r.contains('Impossible vehicle speed')), isTrue);
expect(quality.speedConfidence, equals(0.0));
expect(validator.rejectedCount, equals(1));
});
test('NaN numeric values are rejected immediately', () {
final sample = TelemetrySample(
timestamp: DateTime.now(),
speed: double.nan,
latitude: 37.77,
);
final quality = validator.validate(sample);
expect(quality.isRejected, isTrue);
expect(quality.overallConfidence, equals(0.0));
expect(quality.reasons.any((r) => r.contains('NaN or Infinity')), isTrue);
});
test('Degraded GPS accuracy reduces confidence and marks sample suspect', () {
final sample = TelemetrySample(
timestamp: DateTime.now(),
speed: 45.0,
gpsAccuracy: 48.0, // poor GPS lock
);
final quality = validator.validate(sample);
expect(quality.isSuspect, isTrue);
expect(quality.gpsConfidence, lessThan(0.50));
expect(quality.reasons.any((r) => r.contains('Poor GPS accuracy')), isTrue);
expect(validator.suspectCount, equals(1));
});
test('Extreme roll rate (>180 deg/sec) flags suspect lean', () {
final t0 = DateTime.now();
validator.validate(TelemetrySample(
timestamp: t0,
speed: 50.0,
leanAngle: 0.0,
));
// 50ms later, jump from 0 deg to 30 deg lean (600 deg/sec impossible flick)
final quality = validator.validate(TelemetrySample(
timestamp: t0.add(const Duration(milliseconds: 50)),
speed: 50.0,
leanAngle: 30.0,
));
expect(quality.isSuspect, isTrue);
expect(quality.reasons.any((r) => r.contains('Excessive roll rate')), isTrue);
});
});
group('Phase 8: Motorcycle-Specific Calibration Tests', () {
test('Motorcycle A and Motorcycle B have isolated calibration offsets', () async {
SharedPreferences.setMockInitialValues({});
final calibrationService = CalibrationService();
// Calibrate Bike 1 (e.g. Yamaha R6 in portrait mount)
await calibrationService.saveCalibrationOffsets(2.5, -1.2, motorcycleId: 1);
// Calibrate Bike 2 (e.g. BMW GS in landscape mount)
await calibrationService.saveCalibrationOffsets(-4.0, 3.8, motorcycleId: 2);
// Load Bike 1
final bike1Offsets = await calibrationService.loadCalibrationOffsets(motorcycleId: 1);
expect(bike1Offsets['pitch'], equals(2.5));
expect(bike1Offsets['roll'], equals(-1.2));
// Load Bike 2
final bike2Offsets = await calibrationService.loadCalibrationOffsets(motorcycleId: 2);
expect(bike2Offsets['pitch'], equals(-4.0));
expect(bike2Offsets['roll'], equals(3.8));
});
});
group('Phase 8: Personal Record Integrity Tests', () {
test('GPS multipath glitch is rejected by confidence and sanity limits', () async {
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
final recordsService = RecordsService(prefs);
// Legitimate record
await recordsService.checkAndSaveRecords(
const PersonalRecords(topSpeedMph: 112.0),
null,
confidence: 0.95,
);
expect(recordsService.state.topSpeedMph, equals(112.0));
// Glitched GPS spike with low confidence is discarded
await recordsService.checkAndSaveRecords(
const PersonalRecords(topSpeedMph: 185.0),
null,
confidence: 0.40, // low confidence
);
expect(recordsService.state.topSpeedMph, equals(112.0));
// Impossible speed > 220 is discarded even if confidence claimed high
await recordsService.checkAndSaveRecords(
const PersonalRecords(topSpeedMph: 350.0),
null,
confidence: 1.0,
);
expect(recordsService.state.topSpeedMph, equals(112.0));
});
});
}