MotorSignal-App/test/pre_field_test_engineering_...

273 lines
10 KiB
Dart

import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:motorcycle_dash/models/telemetry_sample.dart';
import 'package:motorcycle_dash/services/validation/telemetry_validator.dart';
import 'package:motorcycle_dash/services/simulation/telemetry_simulation_engine.dart';
import 'package:motorcycle_dash/services/calibration_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Pre-Field-Test Telemetry Simulation Suite (18 Scenarios)', () {
late TelemetrySimulationEngine engine;
setUp(() {
engine = TelemetrySimulationEngine();
});
test('Scenario 1: Stationary motorcycle idle vibration passes validation', () {
final samples = engine.generateScenario(SimulationScenarioType.stationaryMotorcycle);
expect(samples.length, equals(100));
final res = engine.runSimulation(samples);
expect(res.validRatio, greaterThanOrEqualTo(0.95));
});
test('Scenario 2: Smooth acceleration 0-60 MPH passes validation', () {
final samples = engine.generateScenario(SimulationScenarioType.smoothAcceleration);
expect(samples.length, equals(100));
final res = engine.runSimulation(samples);
expect(res.validRatio, greaterThanOrEqualTo(0.95));
});
test('Scenario 3: 0-60 MPH sport launch passes validation', () {
final samples = engine.generateScenario(SimulationScenarioType.zeroTo60Run);
expect(samples.length, equals(64));
final res = engine.runSimulation(samples);
expect(res.validRatio, greaterThanOrEqualTo(0.95));
});
test('Scenario 4: 0-100 MPH high-speed run passes validation', () {
final samples = engine.generateScenario(SimulationScenarioType.zeroTo100Run);
expect(samples.length, equals(150));
final res = engine.runSimulation(samples);
expect(res.validRatio, greaterThanOrEqualTo(0.95));
});
test('Scenario 5: Hard braking (1.15G) passes validation', () {
final samples = engine.generateScenario(SimulationScenarioType.hardBraking);
expect(samples.length, equals(56));
final res = engine.runSimulation(samples);
expect(res.validRatio, greaterThanOrEqualTo(0.95));
});
test('Scenario 6: Smooth left corner (35 deg lean) passes validation', () {
final samples = engine.generateScenario(SimulationScenarioType.smoothLeftCorner);
expect(samples.length, equals(80));
final res = engine.runSimulation(samples);
expect(res.validRatio, greaterThanOrEqualTo(0.95));
});
test('Scenario 7: Smooth right corner (25 deg lean) passes validation', () {
final samples = engine.generateScenario(SimulationScenarioType.smoothRightCorner);
expect(samples.length, equals(70));
final res = engine.runSimulation(samples);
expect(res.validRatio, greaterThanOrEqualTo(0.95));
});
test('Scenario 8: High-G acceleration (0.8G) passes validation', () {
final samples = engine.generateScenario(SimulationScenarioType.highGAcceleration);
expect(samples.length, equals(40));
final res = engine.runSimulation(samples);
expect(res.validRatio, greaterThanOrEqualTo(0.95));
});
test('Scenario 9: High-G emergency braking (1.4G) passes validation', () {
final samples = engine.generateScenario(SimulationScenarioType.highGBraking);
expect(samples.length, equals(40));
final res = engine.runSimulation(samples);
expect(res.validRatio, greaterThanOrEqualTo(0.95));
});
test('Scenario 10: GPS dropout (tunnel) is safely handled', () {
final samples = engine.generateScenario(SimulationScenarioType.gpsDropout);
expect(samples.length, equals(300));
final res = engine.runSimulation(samples);
expect(res.totalSamples, equals(300));
});
test('Scenario 11: GPS accuracy degradation flags suspect samples', () {
final samples = engine.generateScenario(SimulationScenarioType.gpsAccuracyDegradation);
final res = engine.runSimulation(samples);
expect(res.suspectSamples, greaterThan(0));
});
test('Scenario 12: GPS position jump rejects anomaly teleportation', () {
final samples = engine.generateScenario(SimulationScenarioType.gpsPositionJump);
final res = engine.runSimulation(samples);
expect(res.suspectSamples + res.rejectedSamples, greaterThan(0));
});
test('Scenario 13: Sensor dropout handled safely', () {
final samples = engine.generateScenario(SimulationScenarioType.sensorDropout);
final res = engine.runSimulation(samples);
expect(res.totalSamples, equals(100));
});
test('Scenario 14: Duplicate timestamp anomaly detected', () {
final samples = engine.generateScenario(SimulationScenarioType.duplicateTimestamp);
final res = engine.runSimulation(samples);
expect(res.suspectSamples + res.rejectedSamples, greaterThan(0));
});
test('Scenario 15: Timestamp regression anomaly detected', () {
final samples = engine.generateScenario(SimulationScenarioType.timestampRegression);
final res = engine.runSimulation(samples);
expect(res.suspectSamples + res.rejectedSamples, greaterThan(0));
});
test('Scenario 16: Irregular 20Hz timing jitter detected', () {
final samples = engine.generateScenario(SimulationScenarioType.irregular20HzTiming);
final res = engine.runSimulation(samples);
expect(res.totalSamples, equals(50));
});
test('Scenario 17: App backgrounding recovery handles large time delta', () {
final samples = engine.generateScenario(SimulationScenarioType.appBackgroundingRecovery);
final res = engine.runSimulation(samples);
expect(res.totalSamples, equals(100));
});
test('Scenario 18: Storage backpressure burst processed smoothly', () {
final samples = engine.generateScenario(SimulationScenarioType.storageBackpressure);
expect(samples.length, equals(500));
final res = engine.runSimulation(samples);
expect(res.validRatio, greaterThanOrEqualTo(0.95));
});
});
group('Calibration & Gyro Zero-Rate Bias Persistence', () {
test('Saves and loads pitch, roll, and gyro biases', () async {
SharedPreferences.setMockInitialValues({});
final cal = CalibrationService();
await cal.saveCalibrationOffsets(
2.5,
-1.8,
gyroBiasX: 0.012,
gyroBiasY: -0.005,
gyroBiasZ: 0.003,
motorcycleId: 42,
);
final loaded = await cal.loadCalibrationOffsets(motorcycleId: 42);
expect(loaded['pitch'], closeTo(2.5, 0.001));
expect(loaded['roll'], closeTo(-1.8, 0.001));
expect(loaded['gyroBiasX'], closeTo(0.012, 0.001));
expect(loaded['gyroBiasY'], closeTo(-0.005, 0.001));
expect(loaded['gyroBiasZ'], closeTo(0.003, 0.001));
});
});
group('Categorical Quality Scoring & Diagnostics', () {
test('Computes multi-dimensional scores across Timing, GPS, IMU, Database, and Filter', () {
final report = RideQualityReport.evaluate(
totalSamples: 1000,
validSamples: 980,
suspectSamples: 20,
rejectedSamples: 0,
timestampGaps: 0,
gpsFixCount: 500,
totalGpsAccuracy: 1500.0, // Avg 3.0m
databaseSamplesLost: 0,
imuSaturationCount: 0,
isMountCalibrated: true,
);
expect(report.overallScore, greaterThanOrEqualTo(90));
expect(report.rating, equals('EXCELLENT'));
expect(report.timingIntegrityScore, equals(100));
expect(report.gpsQualityScore, equals(100));
expect(report.imuContinuityScore, equals(100));
expect(report.databaseIntegrityScore, equals(100));
expect(report.filterHealthScore, equals(98));
expect(report.diagnosticExplanations.length, greaterThanOrEqualTo(5));
});
});
group('Ride #4 Physical Dataset Regression Test', () {
test('Replays all 18,981 samples of Ride #4 with zero crashes and verifies kickstand gating', () async {
final file = File('field_test_export_ride4_unzipped/telemetry.csv');
if (!file.existsSync()) {
return;
}
final lines = await file.readAsLines();
expect(lines.length, equals(18982)); // 1 header + 18981 data rows
final validator = TelemetryValidator();
int valid = 0;
int suspect = 0;
int rejected = 0;
double topSpeed = 0.0;
double maxMovingLeftLean = 0.0;
double maxStationaryLeftLean = 0.0;
for (int i = 1; i < lines.length; i++) {
final cols = lines[i].split(',');
if (cols.length < 8) continue;
final timestamp = DateTime.parse(cols[0]);
final lat = double.tryParse(cols[1]);
final lon = double.tryParse(cols[2]);
final alt = double.tryParse(cols[3]);
final speed = double.tryParse(cols[4]);
final heading = double.tryParse(cols[5]);
final gpsAcc = double.tryParse(cols[6]);
final lean = double.tryParse(cols[7]);
final pitch = double.tryParse(cols[8]);
final longG = double.tryParse(cols[12]);
final latG = double.tryParse(cols[13]);
final sample = TelemetrySample(
timestamp: timestamp,
sequenceNumber: i,
monotonicTimestampMs: i * 50,
latitude: lat,
longitude: lon,
altitude: alt,
speed: speed,
heading: heading,
gpsAccuracy: gpsAcc,
leanAngle: lean,
pitchAngle: pitch,
longitudinalG: longG,
lateralG: latG,
);
final q = validator.validate(sample);
if (q.isValid) {
valid++;
} else if (q.isSuspect) {
suspect++;
} else {
rejected++;
}
if (speed != null && speed > topSpeed) {
topSpeed = speed;
}
if (lean != null && lean < 0) {
if (speed != null && speed >= 3.0) {
if (lean.abs() > maxMovingLeftLean) {
maxMovingLeftLean = lean.abs();
}
} else {
if (lean.abs() > maxStationaryLeftLean) {
maxStationaryLeftLean = lean.abs();
}
}
}
}
expect(suspect, greaterThanOrEqualTo(0));
expect(lines.length - 1, equals(18981));
expect(topSpeed, closeTo(102.09, 0.5));
expect(maxStationaryLeftLean, closeTo(51.53, 0.5)); // Kickstand event at 0 MPH
expect(maxMovingLeftLean, lessThan(45.0)); // Normal moving lean
expect(valid, greaterThan(18000));
expect(rejected, equals(0));
});
});
}