183 lines
6.1 KiB
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));
|
|
});
|
|
});
|
|
}
|